Skip to content

Commit a3f7747

Browse files
feat(stdlib): Support span streaming
1 parent 7d378a8 commit a3f7747

2 files changed

Lines changed: 102 additions & 56 deletions

File tree

sentry_sdk/integrations/stdlib.py

Lines changed: 59 additions & 19 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,38 @@ 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)
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+
},
116+
)
117+
set_on_span = span.set_attribute
118+
119+
else:
120+
span = sentry_sdk.start_span(
121+
op=OP.HTTP_CLIENT,
122+
name="%s %s"
123+
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
124+
origin="auto.http.stdlib.httplib",
125+
)
126+
set_on_span = span.set_data
127+
128+
set_on_span(SPANDATA.HTTP_METHOD, method)
109129
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)
130+
set_on_span("url", parsed_url.url)
131+
set_on_span(SPANDATA.HTTP_QUERY, parsed_url.query)
132+
set_on_span(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
113133

114134
# for proxies, these point to the proxy host/port
115135
if tunnel_host:
116-
span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
117-
span.set_data(SPANDATA.NETWORK_PEER_PORT, self.port)
136+
set_on_span(SPANDATA.NETWORK_PEER_ADDRESS, self.host)
137+
set_on_span(SPANDATA.NETWORK_PEER_PORT, self.port)
118138

119139
rv = real_putrequest(self, method, url, *args, **kwargs)
120140

@@ -139,14 +159,23 @@ def putrequest(
139159
def getresponse(self: "HTTPConnection", *args: "Any", **kwargs: "Any") -> "Any":
140160
span = getattr(self, "_sentrysdk_span", None)
141161

162+
print("span is here")
163+
142164
if span is None:
143165
return real_getresponse(self, *args, **kwargs)
144166

145167
try:
146168
rv = real_getresponse(self, *args, **kwargs)
147169

148-
span.set_http_status(int(rv.status))
149-
span.set_data("reason", rv.reason)
170+
if isinstance(span, StreamedSpan):
171+
span.set_attribute("reason", rv.reason)
172+
173+
status_code = int(rv.status)
174+
span.status = "error" if status_code >= 400 else "ok"
175+
span.set_attribute("http.response.status_code", status_code)
176+
else:
177+
span.set_http_status(int(rv.status))
178+
span.set_data("reason", rv.reason)
150179
finally:
151180
span.finish()
152181

@@ -226,11 +255,22 @@ def sentry_patched_popen_init(
226255

227256
env = None
228257

229-
with sentry_sdk.start_span(
230-
op=OP.SUBPROCESS,
231-
name=description,
232-
origin="auto.subprocess.stdlib.subprocess",
233-
) as span:
258+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
259+
span: "Union[Span, StreamedSpan]"
260+
if span_streaming:
261+
span = sentry_sdk.start_span(
262+
op=OP.SUBPROCESS,
263+
name=description,
264+
origin="auto.subprocess.stdlib.subprocess",
265+
)
266+
else:
267+
span = sentry_sdk.start_span(
268+
op=OP.SUBPROCESS,
269+
name=description,
270+
origin="auto.subprocess.stdlib.subprocess",
271+
)
272+
273+
with span:
234274
for k, v in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
235275
span=span
236276
):

tests/integrations/stdlib/test_httplib.py

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,15 @@ def test_httplib_misuse(sentry_init, capture_events, request):
202202
)
203203

204204

205-
def test_outgoing_trace_headers(sentry_init, monkeypatch):
205+
def test_outgoing_trace_headers(sentry_init, capture_events, monkeypatch):
206206
# HTTPSConnection.send is passed a string containing (among other things)
207207
# the headers on the request. Mock it so we can check the headers, and also
208208
# so it doesn't try to actually talk to the internet.
209209
mock_send = mock.Mock()
210210
monkeypatch.setattr(HTTPSConnection, "send", mock_send)
211211

212212
sentry_init(traces_sample_rate=1.0)
213+
events = capture_events()
213214

214215
headers = {
215216
"sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-1",
@@ -237,38 +238,42 @@ def test_outgoing_trace_headers(sentry_init, monkeypatch):
237238
key, val = line.split(": ")
238239
request_headers[key] = val
239240

240-
request_span = transaction._span_recorder.spans[-1]
241-
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
242-
trace_id=transaction.trace_id,
243-
parent_span_id=request_span.span_id,
244-
sampled=1,
245-
)
246-
assert request_headers["sentry-trace"] == expected_sentry_trace
247-
248-
expected_outgoing_baggage = (
249-
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
250-
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
251-
"sentry-sample_rate=1.0,"
252-
"sentry-user_id=Am%C3%A9lie,"
253-
"sentry-sample_rand=0.132521102938283"
254-
)
241+
(event,) = events
242+
request_span = event["spans"][-1]
243+
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
244+
trace_id=transaction.trace_id,
245+
parent_span_id=request_span.span_id,
246+
sampled=1,
247+
)
248+
assert request_headers["sentry-trace"] == expected_sentry_trace
249+
250+
expected_outgoing_baggage = (
251+
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
252+
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
253+
"sentry-sample_rate=1.0,"
254+
"sentry-user_id=Am%C3%A9lie,"
255+
"sentry-sample_rand=0.132521102938283"
256+
)
255257

256-
assert request_headers["baggage"] == expected_outgoing_baggage
258+
assert request_headers["baggage"] == expected_outgoing_baggage
257259

258260

259-
def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
261+
def test_outgoing_trace_headers_head_sdk(sentry_init, capture_events, monkeypatch):
260262
# HTTPSConnection.send is passed a string containing (among other things)
261263
# the headers on the request. Mock it so we can check the headers, and also
262264
# so it doesn't try to actually talk to the internet.
263265
mock_send = mock.Mock()
264266
monkeypatch.setattr(HTTPSConnection, "send", mock_send)
265267

266268
sentry_init(traces_sample_rate=0.5, release="foo")
269+
events = capture_events()
270+
267271
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000):
268272
transaction = continue_trace({})
269273

270274
with start_transaction(transaction=transaction, name="Head SDK tx") as transaction:
271-
HTTPSConnection("www.squirrelchasers.com").request("GET", "/top-chasers")
275+
connection = HTTPSConnection("www.squirrelchasers.com")
276+
connection.request("GET", "/top-chasers")
272277

273278
(request_str,) = mock_send.call_args[0]
274279
request_headers = {}
@@ -277,24 +282,25 @@ def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
277282
key, val = line.split(": ")
278283
request_headers[key] = val
279284

280-
request_span = transaction._span_recorder.spans[-1]
281-
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
282-
trace_id=transaction.trace_id,
283-
parent_span_id=request_span.span_id,
284-
sampled=1,
285-
)
286-
assert request_headers["sentry-trace"] == expected_sentry_trace
287-
288-
expected_outgoing_baggage = (
289-
"sentry-trace_id=%s,"
290-
"sentry-sample_rand=0.250000,"
291-
"sentry-environment=production,"
292-
"sentry-release=foo,"
293-
"sentry-sample_rate=0.5,"
294-
"sentry-sampled=%s"
295-
) % (transaction.trace_id, "true" if transaction.sampled else "false")
296-
297-
assert request_headers["baggage"] == expected_outgoing_baggage
285+
(event,) = events
286+
request_span = event["spans"][0]
287+
expected_sentry_trace = "{trace_id}-{parent_span_id}-{sampled}".format(
288+
trace_id=transaction.trace_id,
289+
parent_span_id=request_span.span_id,
290+
sampled=1,
291+
)
292+
assert request_headers["sentry-trace"] == expected_sentry_trace
293+
294+
expected_outgoing_baggage = (
295+
"sentry-trace_id=%s,"
296+
"sentry-sample_rand=0.250000,"
297+
"sentry-environment=production,"
298+
"sentry-release=foo,"
299+
"sentry-sample_rate=0.5,"
300+
"sentry-sampled=%s"
301+
) % (transaction.trace_id, "true" if transaction.sampled else "false")
302+
303+
assert request_headers["baggage"] == expected_outgoing_baggage
298304

299305

300306
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)