Skip to content

Commit 5f02ee8

Browse files
feat(stdlib): Support span streaming (getsentry#6154)
Port `http` and `subprocess` instrumentation to support the streaming lifecycle. Drop the `reason` and `subprocess.cwd` attributes in streaming lifecycle mode. Replace deprecated `http.method` and `url` attributes with `http.request.method` and `url.full` attributes in the streaming lifecycle mode. Use `url.query` and `url.fragment` instead of `http.query` and `http.fragment` in the streaming mode. Spans for some subprocess operations did not have a description. The value of the `sentry.op` attribute is used as the span name for the streaming path in these cases.
1 parent 086a231 commit 5f02ee8

4 files changed

Lines changed: 1136 additions & 414 deletions

File tree

sentry_sdk/consts.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,12 @@ class SPANDATA:
881881
Example: "tcp", "udp", "unix"
882882
"""
883883

884+
PROCESS_PID = "process.pid"
885+
"""
886+
The process ID of the running process.
887+
Example: 12345
888+
"""
889+
884890
PROFILER_ID = "profiler_id"
885891
"""
886892
Label identifying the profiler id that the span occurred in. This should be a string.
@@ -924,6 +930,24 @@ class SPANDATA:
924930
Example: "MainThread"
925931
"""
926932

933+
URL_FULL = "url.full"
934+
"""
935+
The URL of the resource that was fetched.
936+
Example: "https://example.com/test?foo=bar#buzz"
937+
"""
938+
939+
URL_FRAGMENT = "url.fragment"
940+
"""
941+
The fragments present in the URI. Note that this does not contain the leading # character, while the `http.fragment` attribute does.
942+
Example: "details"
943+
"""
944+
945+
URL_QUERY = "url.query"
946+
"""
947+
The query string present in the URL. Note that this does not contain the leading ? character, while the `http.query` attribute does.
948+
Example: "foo=bar&bar=baz"
949+
"""
950+
927951
MCP_TOOL_NAME = "mcp.tool.name"
928952
"""
929953
The name of the MCP tool being called.

sentry_sdk/integrations/stdlib.py

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
from sentry_sdk.consts import OP, SPANDATA
99
from sentry_sdk.integrations import Integration
1010
from sentry_sdk.scope import add_global_event_processor
11+
from sentry_sdk.tracing import Span
12+
from sentry_sdk.traces import StreamedSpan
1113
from sentry_sdk.tracing_utils import (
1214
EnvironHeaders,
1315
should_propagate_trace,
1416
add_http_request_source,
17+
has_span_streaming_enabled,
1518
)
1619
from sentry_sdk.utils import (
1720
SENSITIVE_DATA_SUBSTITUTE,
@@ -31,6 +34,7 @@
3134
from typing import Dict
3235
from typing import Optional
3336
from typing import List
37+
from typing import Union
3438

3539
from sentry_sdk._types import Event, Hint
3640

@@ -99,22 +103,46 @@ def putrequest(
99103
with capture_internal_exceptions():
100104
parsed_url = parse_url(real_url, sanitize=False)
101105

102-
span = sentry_sdk.start_span(
103-
op=OP.HTTP_CLIENT,
104-
name="%s %s"
105-
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
106-
origin="auto.http.stdlib.httplib",
107-
)
108-
span.set_data(SPANDATA.HTTP_METHOD, method)
109-
if parsed_url is not None:
110-
span.set_data("url", parsed_url.url)
111-
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
112-
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
106+
span_streaming = has_span_streaming_enabled(client.options)
107+
span: "Union[Span, StreamedSpan]"
108+
if span_streaming:
109+
span = sentry_sdk.traces.start_span(
110+
name="%s %s"
111+
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
112+
attributes={
113+
"sentry.origin": "auto.http.stdlib.httplib",
114+
"sentry.op": OP.HTTP_CLIENT,
115+
SPANDATA.HTTP_REQUEST_METHOD: method,
116+
},
117+
)
118+
119+
if parsed_url is not None:
120+
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
121+
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
122+
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
123+
124+
set_on_span = span.set_attribute
125+
126+
else:
127+
span = sentry_sdk.start_span(
128+
op=OP.HTTP_CLIENT,
129+
name="%s %s"
130+
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
131+
origin="auto.http.stdlib.httplib",
132+
)
133+
134+
span.set_data(SPANDATA.HTTP_METHOD, method)
135+
if parsed_url is not None:
136+
span.set_data("url", parsed_url.url)
137+
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
138+
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
139+
140+
set_on_span = span.set_data
113141

114142
# for proxies, these point to the proxy host/port
115143
if tunnel_host:
116-
span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
117-
span.set_data(SPANDATA.NETWORK_PEER_PORT, self.port)
144+
set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
145+
set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port)
118146

119147
rv = real_putrequest(self, method, url, *args, **kwargs)
120148

@@ -145,13 +173,23 @@ def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any":
145173
try:
146174
rv = real_getresponse(self, *args, **kwargs)
147175

148-
span.set_http_status(int(rv.status))
149-
span.set_data("reason", rv.reason)
176+
if isinstance(span, StreamedSpan):
177+
status_code = int(rv.status)
178+
span.status = "error" if status_code >= 400 else "ok"
179+
span.set_attribute("http.response.status_code", status_code)
180+
else:
181+
span.set_http_status(int(rv.status))
182+
span.set_data("reason", rv.reason)
150183
finally:
151-
span.finish()
184+
if isinstance(span, StreamedSpan):
185+
with capture_internal_exceptions():
186+
add_http_request_source(span)
187+
span.end()
188+
else:
189+
span.finish()
152190

153-
with capture_internal_exceptions():
154-
add_http_request_source(span)
191+
with capture_internal_exceptions():
192+
add_http_request_source(span)
155193

156194
return rv
157195

@@ -226,11 +264,24 @@ def sentry_patched_popen_init(
226264

227265
env = None
228266

229-
with sentry_sdk.start_span(
230-
op=OP.SUBPROCESS,
231-
name=description,
232-
origin="auto.subprocess.stdlib.subprocess",
233-
) as span:
267+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
268+
span: "Union[Span, StreamedSpan]"
269+
if span_streaming:
270+
span = sentry_sdk.traces.start_span(
271+
name=description,
272+
attributes={
273+
"sentry.op": OP.SUBPROCESS,
274+
"sentry.origin": "auto.subprocess.stdlib.subprocess",
275+
},
276+
)
277+
else:
278+
span = sentry_sdk.start_span(
279+
op=OP.SUBPROCESS,
280+
name=description,
281+
origin="auto.subprocess.stdlib.subprocess",
282+
)
283+
284+
with span:
234285
for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
235286
span=span
236287
):
@@ -244,12 +295,16 @@ def sentry_patched_popen_init(
244295
)
245296
env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
246297

247-
if cwd:
298+
if cwd and isinstance(span, Span):
248299
span.set_data("subprocess.cwd", cwd)
249300

250301
rv = old_popen_init(self, *a, **kw)
251302

252-
span.set_tag("subprocess.pid", self.pid)
303+
if isinstance(span, StreamedSpan):
304+
span.set_attribute(SPANDATA.PROCESS_PID, self.pid)
305+
else:
306+
span.set_tag("subprocess.pid", self.pid)
307+
253308
return rv
254309

255310
subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore
@@ -260,12 +315,24 @@ def sentry_patched_popen_init(
260315
def sentry_patched_popen_wait(
261316
self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any"
262317
) -> "Any":
263-
with sentry_sdk.start_span(
264-
op=OP.SUBPROCESS_WAIT,
265-
origin="auto.subprocess.stdlib.subprocess",
266-
) as span:
267-
span.set_tag("subprocess.pid", self.pid)
268-
return old_popen_wait(self, *a, **kw)
318+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
319+
if span_streaming:
320+
with sentry_sdk.traces.start_span(
321+
name=OP.SUBPROCESS_WAIT,
322+
attributes={
323+
"sentry.op": OP.SUBPROCESS_WAIT,
324+
"sentry.origin": "auto.subprocess.stdlib.subprocess",
325+
},
326+
) as span:
327+
span.set_attribute(SPANDATA.PROCESS_PID, self.pid)
328+
return old_popen_wait(self, *a, **kw)
329+
else:
330+
with sentry_sdk.start_span(
331+
op=OP.SUBPROCESS_WAIT,
332+
origin="auto.subprocess.stdlib.subprocess",
333+
) as span:
334+
span.set_tag("subprocess.pid", self.pid)
335+
return old_popen_wait(self, *a, **kw)
269336

270337
subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore
271338

@@ -275,12 +342,24 @@ def sentry_patched_popen_wait(
275342
def sentry_patched_popen_communicate(
276343
self: "subprocess.Popen[Any]", *a: "Any", **kw: "Any"
277344
) -> "Any":
278-
with sentry_sdk.start_span(
279-
op=OP.SUBPROCESS_COMMUNICATE,
280-
origin="auto.subprocess.stdlib.subprocess",
281-
) as span:
282-
span.set_tag("subprocess.pid", self.pid)
283-
return old_popen_communicate(self, *a, **kw)
345+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
346+
if span_streaming:
347+
with sentry_sdk.traces.start_span(
348+
name=OP.SUBPROCESS_COMMUNICATE,
349+
attributes={
350+
"sentry.op": OP.SUBPROCESS_COMMUNICATE,
351+
"sentry.origin": "auto.subprocess.stdlib.subprocess",
352+
},
353+
) as span:
354+
span.set_attribute(SPANDATA.PROCESS_PID, self.pid)
355+
return old_popen_communicate(self, *a, **kw)
356+
else:
357+
with sentry_sdk.start_span(
358+
op=OP.SUBPROCESS_COMMUNICATE,
359+
origin="auto.subprocess.stdlib.subprocess",
360+
) as span:
361+
span.set_tag("subprocess.pid", self.pid)
362+
return old_popen_communicate(self, *a, **kw)
284363

285364
subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore
286365

0 commit comments

Comments
 (0)