-
Notifications
You must be signed in to change notification settings - Fork 306
/
api_client.py
290 lines (245 loc) · 10.1 KB
/
api_client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License.
# This product includes software developed at Datadog (https://www.datadoghq.com/).
# Copyright 2015-Present Datadog, Inc
# stdlib
import json
import logging
import time
import zlib
# datadog
from datadog.api import _api_version, _max_timeouts, _backoff_period
from datadog.api.exceptions import ClientError, ApiError, HttpBackoff, HttpTimeout, ApiNotInitialized
from datadog.api.http_client import resolve_http_client
from datadog.util.compat import is_p3k
from datadog.util.format import construct_url, normalize_tags
log = logging.getLogger("datadog.api")
class APIClient(object):
"""
Datadog API client: format and submit API calls to Datadog.
Embeds a HTTP client.
"""
# HTTP transport parameters
_backoff_period = _backoff_period
_max_timeouts = _max_timeouts
_backoff_timestamp = None
_timeout_counter = 0
_sort_keys = False
# Plugged HTTP client
_http_client = None
@classmethod
def _get_http_client(cls):
"""
Getter for the embedded HTTP client.
"""
if not cls._http_client:
cls._http_client = resolve_http_client()
return cls._http_client
@classmethod
def submit(
cls,
method,
path,
api_version=None,
body=None,
attach_host_name=False,
response_formatter=None,
error_formatter=None,
suppress_response_errors_on_codes=None,
compress_payload=False,
**params
):
"""
Make an HTTP API request
:param method: HTTP method to use to contact API endpoint
:type method: HTTP method string
:param path: API endpoint url
:type path: url
:param api_version: The API version used
:param body: dictionary to be sent in the body of the request
:type body: dictionary
:param response_formatter: function to format JSON response from HTTP API request
:type response_formatter: JSON input function
:param error_formatter: function to format JSON error response from HTTP API request
:type error_formatter: JSON input function
:param attach_host_name: link the new resource object to the host name
:type attach_host_name: bool
:param suppress_response_errors_on_codes: suppress ApiError on `errors` key in the response for the given HTTP
status codes
:type suppress_response_errors_on_codes: None|list(int)
:param compress_payload: compress the payload using zlib
:type compress_payload: bool
:param params: dictionary to be sent in the query string of the request
:type params: dictionary
:returns: JSON or formatted response from HTTP API request
"""
try:
# Check if it's ok to submit
if not cls._should_submit():
_, backoff_time_left = cls._backoff_status()
raise HttpBackoff(backoff_time_left)
# Import API, User and HTTP settings
from datadog.api import (
_api_key,
_application_key,
_api_host,
_mute,
_host_name,
_proxies,
_max_retries,
_timeout,
_cacert,
_return_raw_response,
)
# Check keys and add then to params
if _api_key is None:
raise ApiNotInitialized("API key is not set." " Please run 'initialize' method first.")
# Set api and app keys in headers
headers = {}
headers["DD-API-KEY"] = _api_key
if _application_key:
headers["DD-APPLICATION-KEY"] = _application_key
# Check if the api_version is provided
if not api_version:
api_version = _api_version
# Attach host name to body
if attach_host_name and body:
# Is it a 'series' list of objects ?
if "series" in body:
# Adding the host name to all objects
for obj_params in body["series"]:
if obj_params.get("host", "") == "":
obj_params["host"] = _host_name
else:
if body.get("host", "") == "":
body["host"] = _host_name
# If defined, make sure tags are defined as a comma-separated string
if "tags" in params and isinstance(params["tags"], list):
tag_list = normalize_tags(params["tags"])
params["tags"] = ",".join(tag_list)
# If defined, make sure monitor_ids are defined as a comma-separated string
if "monitor_ids" in params and isinstance(params["monitor_ids"], list):
params["monitor_ids"] = ",".join(str(i) for i in params["monitor_ids"])
# Process the body, if necessary
if isinstance(body, dict):
body = json.dumps(body, sort_keys=cls._sort_keys)
headers["Content-Type"] = "application/json"
if compress_payload:
body = zlib.compress(body.encode("utf-8"))
headers["Content-Encoding"] = "deflate"
# Construct the URL
url = construct_url(_api_host, api_version, path)
# Process requesting
start_time = time.time()
result = cls._get_http_client().request(
method=method,
url=url,
headers=headers,
params=params,
data=body,
timeout=_timeout,
max_retries=_max_retries,
proxies=_proxies,
verify=_cacert,
)
# Request succeeded: log it and reset the timeout counter
duration = round((time.time() - start_time) * 1000.0, 4)
log.info("%s %s %s (%sms)" % (result.status_code, method, url, duration))
cls._timeout_counter = 0
# Format response content
content = result.content
if content:
try:
if is_p3k():
response_obj = json.loads(content.decode("utf-8"))
else:
response_obj = json.loads(content)
except ValueError:
raise ValueError("Invalid JSON response: {0}".format(content))
# response_obj can be a bool and not a dict
if isinstance(response_obj, dict):
if response_obj and "errors" in response_obj:
# suppress ApiError when specified and just return the response
if not (
suppress_response_errors_on_codes
and result.status_code in suppress_response_errors_on_codes
):
raise ApiError(response_obj)
else:
response_obj = None
if response_formatter is not None:
response_obj = response_formatter(response_obj)
if _return_raw_response:
return response_obj, result
else:
return response_obj
except HttpTimeout:
cls._timeout_counter += 1
raise
except ClientError as e:
if _mute:
log.error(str(e))
if error_formatter is None:
return {"errors": e.args[0]}
else:
return error_formatter({"errors": e.args[0]})
else:
raise
except ApiError as e:
if _mute:
for error in e.args[0].get("errors") or []:
log.error(error)
if error_formatter is None:
return e.args[0]
else:
return error_formatter(e.args[0])
else:
raise
@classmethod
def _should_submit(cls):
"""
Returns True if we're in a state where we should make a request
(backoff expired, no backoff in effect), false otherwise.
"""
now = time.time()
should_submit = False
# If we're not backing off, but the timeout counter exceeds the max
# number of timeouts, then enter the backoff state, recording the time
# we started backing off
if not cls._backoff_timestamp and cls._timeout_counter >= cls._max_timeouts:
log.info(
"Max number of datadog timeouts exceeded, backing off for %s seconds",
cls._backoff_period,
)
cls._backoff_timestamp = now
should_submit = False
# If we are backing off but the we've waiting sufficiently long enough
# (backoff_retry_age), exit the backoff state and reset the timeout
# counter so that we try submitting metrics again
elif cls._backoff_timestamp:
backed_off_time, backoff_time_left = cls._backoff_status()
if backoff_time_left < 0:
log.info(
"Exiting backoff state after %s seconds, will try to submit metrics again",
backed_off_time,
)
cls._backoff_timestamp = None
cls._timeout_counter = 0
should_submit = True
else:
log.info(
"In backoff state, won't submit metrics for another %s seconds",
backoff_time_left,
)
should_submit = False
else:
should_submit = True
return should_submit
@classmethod
def _backoff_status(cls):
"""
Get a backoff report, i.e. backoff total and remaining time.
"""
now = time.time()
backed_off_time = now - cls._backoff_timestamp
backoff_time_left = cls._backoff_period - backed_off_time
return round(backed_off_time, 2), round(backoff_time_left, 2)