Skip to content

Commit 4a150d6

Browse files
authored
feat(profiling): enable agent upload by default (#1494)
The upcoming Datadog agent release will support uploading profiles. This enables its usage by default when configuration the profiler with no DD_API_KEY set in the env.
1 parent 3a03a9e commit 4a150d6

File tree

5 files changed

+173
-97
lines changed

5 files changed

+173
-97
lines changed

ddtrace/profiling/exporter/http.py

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import tenacity
99

10-
from ddtrace.utils import deprecation
1110
from ddtrace.utils.formats import parse_tags_str
1211
from ddtrace.vendor import six
1312
from ddtrace.vendor.six.moves import http_client
@@ -28,50 +27,22 @@
2827
PYTHON_VERSION = platform.python_version().encode()
2928

3029

31-
class InvalidEndpoint(ValueError):
32-
pass
33-
34-
3530
class UploadFailed(exporter.ExportError):
3631
"""Upload failure."""
3732

38-
def __init__(self, exception):
33+
def __init__(self, exception, msg=None):
3934
"""Create a failed upload error based on raised exceptions."""
4035
self.exception = exception
41-
super(UploadFailed, self).__init__("Unable to upload profile: " + _traceback.format_exception(exception))
42-
43-
44-
def _get_api_key():
45-
legacy = _attr.from_env("DD_PROFILING_API_KEY", "", str)()
46-
if legacy:
47-
deprecation.deprecation("DD_PROFILING_API_KEY", "Use DD_API_KEY")
48-
return legacy
49-
return _attr.from_env("DD_API_KEY", "", str)()
50-
51-
52-
ENDPOINT_TEMPLATE = "https://intake.profile.{}/v1/input"
53-
54-
55-
def _get_endpoint():
56-
legacy = _attr.from_env("DD_PROFILING_API_URL", "", str)()
57-
if legacy:
58-
deprecation.deprecation("DD_PROFILING_API_URL", "Use DD_SITE")
59-
return legacy
60-
site = _attr.from_env("DD_SITE", "datadoghq.com", str)()
61-
return ENDPOINT_TEMPLATE.format(site)
62-
63-
64-
def _validate_enpoint(instance, attribute, value):
65-
if not value:
66-
raise InvalidEndpoint("Endpoint is empty")
36+
msg = _traceback.format_exception(exception) if msg is None else msg
37+
super(UploadFailed, self).__init__("Unable to upload profile: " + msg)
6738

6839

6940
@attr.s
7041
class PprofHTTPExporter(pprof.PprofExporter):
7142
"""PProf HTTP exporter."""
7243

73-
endpoint = attr.ib(factory=_get_endpoint, type=str, validator=_validate_enpoint)
74-
api_key = attr.ib(factory=_get_api_key, type=str)
44+
endpoint = attr.ib()
45+
api_key = attr.ib(default=None)
7546
timeout = attr.ib(factory=_attr.from_env("DD_PROFILING_API_TIMEOUT", 10, float), type=float)
7647
service = attr.ib(default=None)
7748
env = attr.ib(default=None)
@@ -138,16 +109,25 @@ def _get_tags(self, service):
138109
tags.update({k: six.ensure_binary(v) for k, v in user_tags.items()})
139110
return tags
140111

112+
@staticmethod
113+
def _retry_on_http_5xx(exception):
114+
if isinstance(exception, error.HTTPError):
115+
return 500 <= exception.code < 600
116+
return True
117+
141118
def export(self, events, start_time_ns, end_time_ns):
142119
"""Export events to an HTTP endpoint.
143120
144121
:param events: The event dictionary from a `ddtrace.profiling.recorder.Recorder`.
145122
:param start_time_ns: The start time of recording.
146123
:param end_time_ns: The end time of recording.
147124
"""
148-
common_headers = {
149-
"DD-API-KEY": self.api_key.encode(),
150-
}
125+
if self.api_key:
126+
headers = {
127+
"DD-API-KEY": self.api_key.encode(),
128+
}
129+
else:
130+
headers = {}
151131

152132
profile = super(PprofHTTPExporter, self).export(events, start_time_ns, end_time_ns)
153133
s = six.BytesIO()
@@ -170,7 +150,6 @@ def export(self, events, start_time_ns, end_time_ns):
170150
service = self.service or os.path.basename(profile.string_table[profile.mapping[0].filename])
171151

172152
content_type, body = self._encode_multipart_formdata(fields, tags=self._get_tags(service),)
173-
headers = common_headers.copy()
174153
headers["Content-Type"] = content_type
175154

176155
# urllib uses `POST` if `data` is supplied (Python 2 version does not handle `method` kwarg)
@@ -180,12 +159,20 @@ def export(self, events, start_time_ns, end_time_ns):
180159
# Retry after 1s, 2s, 4s, 8s with some randomness
181160
wait=tenacity.wait_random_exponential(multiplier=0.5),
182161
stop=tenacity.stop_after_delay(self.max_retry_delay),
183-
retry=tenacity.retry_if_exception_type(
184-
(error.HTTPError, error.URLError, http_client.HTTPException, OSError, IOError)
185-
),
162+
retry=tenacity.retry_if_exception_type((http_client.HTTPException, OSError, IOError))
163+
& tenacity.retry_if_exception(self._retry_on_http_5xx),
186164
)
187165

188166
try:
189167
retry(request.urlopen, req, timeout=self.timeout)
190168
except tenacity.RetryError as e:
191169
raise UploadFailed(e.last_attempt.exception())
170+
except error.HTTPError as e:
171+
if e.code == 404 and not self.api_key:
172+
msg = (
173+
"Datadog Agent is not accepting profiles. "
174+
"Agent-based profiling deployments require Datadog Agent >= 7.20"
175+
)
176+
else:
177+
msg = None
178+
raise UploadFailed(e, msg)

ddtrace/profiling/profiler.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ddtrace.profiling import recorder
66
from ddtrace.profiling import scheduler
7+
from ddtrace.utils import deprecation
78
from ddtrace.vendor import attr
89
from ddtrace.profiling.collector import exceptions
910
from ddtrace.profiling.collector import memory
@@ -16,19 +17,45 @@
1617
LOG = logging.getLogger(__name__)
1718

1819

19-
def _build_default_exporters(service, env, version):
20-
exporters = []
21-
if "DD_PROFILING_API_KEY" in os.environ or "DD_API_KEY" in os.environ:
22-
exporters.append(http.PprofHTTPExporter(service=service, env=env, version=version))
20+
ENDPOINT_TEMPLATE = "https://intake.profile.{}/v1/input"
21+
22+
23+
def _get_endpoint():
24+
legacy = os.environ.get("DD_PROFILING_API_URL")
25+
if legacy:
26+
deprecation.deprecation("DD_PROFILING_API_URL", "Use DD_SITE")
27+
return legacy
28+
site = os.environ.get("DD_SITE", "datadoghq.com")
29+
return ENDPOINT_TEMPLATE.format(site)
30+
31+
32+
def _get_api_key():
33+
legacy = os.environ.get("DD_PROFILING_API_KEY")
34+
if legacy:
35+
deprecation.deprecation("DD_PROFILING_API_KEY", "Use DD_API_KEY")
36+
return legacy
37+
return os.environ.get("DD_API_KEY")
2338

39+
40+
def _build_default_exporters(service, env, version):
2441
_OUTPUT_PPROF = os.environ.get("DD_PROFILING_OUTPUT_PPROF")
2542
if _OUTPUT_PPROF:
26-
exporters.append(file.PprofFileExporter(_OUTPUT_PPROF))
27-
28-
if not exporters:
29-
LOG.warning("No exporters are configured, no profile will be output")
43+
return [
44+
file.PprofFileExporter(_OUTPUT_PPROF),
45+
]
3046

31-
return exporters
47+
api_key = _get_api_key()
48+
if api_key:
49+
# Agentless mode
50+
endpoint = _get_endpoint()
51+
else:
52+
hostname = os.environ.get("DD_AGENT_HOST", os.environ.get("DATADOG_TRACE_AGENT_HOSTNAME", "localhost"))
53+
port = int(os.environ.get("DD_TRACE_AGENT_PORT", 8126))
54+
endpoint = os.environ.get("DD_TRACE_AGENT_URL", "http://%s:%d" % (hostname, port)) + "/profiling/v1/input"
55+
56+
return [
57+
http.PprofHTTPExporter(service=service, env=env, version=version, api_key=api_key, endpoint=endpoint),
58+
]
3259

3360

3461
def _get_service_name():

0 commit comments

Comments
 (0)