Skip to content

Commit d70dd77

Browse files
committed
Merge branch 'potel-base' into potel-base-run-all-tests
2 parents e93fa53 + aa9c5ca commit d70dd77

File tree

28 files changed

+1456
-258
lines changed

28 files changed

+1456
-258
lines changed

MIGRATION_GUIDE.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
1010

1111
- The SDK now supports Python 3.7 and higher.
1212
- `sentry_sdk.start_span` now only takes keyword arguments.
13-
- `sentry_sdk.start_span` no longer takes an explicit `span` argument.
14-
- `sentry_sdk.start_span` no longer takes explicit `trace_id`, `span_id` or `parent_span_id` arguments.
13+
- `sentry_sdk.start_transaction`/`sentry_sdk.start_span` no longer takes the following arguments: `span`, `parent_sampled`, `trace_id`, `span_id` or `parent_span_id`.
14+
- You can no longer change the sampled status of a span with `span.sampled = False` after starting it.
1515
- The `Span()` constructor does not accept a `hub` parameter anymore.
1616
- `Span.finish()` does not accept a `hub` parameter anymore.
1717
- The `Profile()` constructor does not accept a `hub` parameter anymore.
@@ -21,6 +21,55 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh
2121
- clickhouse-driver integration: The query is now available under the `db.query.text` span attribute (only if `send_default_pii` is `True`).
2222
- `sentry_sdk.init` now returns `None` instead of a context manager.
2323
- The `sampling_context` argument of `traces_sampler` now additionally contains all span attributes known at span start.
24+
- If you're using the AIOHTTP integration, the `sampling_context` argument of `traces_sampler` doesn't contain the `aiohttp_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:
25+
26+
| Request property | Sampling context key(s) |
27+
| ---------------- | ------------------------------- |
28+
| `path` | `url.path` |
29+
| `query_string` | `url.query` |
30+
| `method` | `http.request.method` |
31+
| `host` | `server.address`, `server.port` |
32+
| `scheme` | `url.scheme` |
33+
| full URL | `url.full` |
34+
35+
- If you're using the Tornado integration, the `sampling_context` argument of `traces_sampler` doesn't contain the `tornado_request` object anymore. Instead, some of the individual properties of the request are accessible, if available, as follows:
36+
37+
| Request property | Sampling context key(s) |
38+
| ---------------- | --------------------------------------------------- |
39+
| `path` | `url.path` |
40+
| `query` | `url.query` |
41+
| `protocol` | `url.scheme` |
42+
| `method` | `http.request.method` |
43+
| `host` | `server.address`, `server.port` |
44+
| `version` | `network.protocol.name`, `network.protocol.version` |
45+
| full URL | `url.full` |
46+
47+
- If you're using the generic WSGI integration, the `sampling_context` argument of `traces_sampler` doesn't contain the `wsgi_environ` object anymore. Instead, the individual properties of the environment are accessible, if available, as follows:
48+
49+
| Env property | Sampling context key(s) |
50+
| ----------------- | ------------------------------------------------- |
51+
| `PATH_INFO` | `url.path` |
52+
| `QUERY_STRING` | `url.query` |
53+
| `REQUEST_METHOD` | `http.request.method` |
54+
| `SERVER_NAME` | `server.address` |
55+
| `SERVER_PORT` | `server.port` |
56+
| `SERVER_PROTOCOL` | `server.protocol.name`, `server.protocol.version` |
57+
| `wsgi.url_scheme` | `url.scheme` |
58+
| full URL | `url.full` |
59+
60+
- If you're using the generic ASGI integration, the `sampling_context` argument of `traces_sampler` doesn't contain the `asgi_scope` object anymore. Instead, the individual properties of the scope, if available, are accessible as follows:
61+
62+
| Scope property | Sampling context key(s) |
63+
| -------------- | ------------------------------- |
64+
| `type` | `network.protocol.name` |
65+
| `scheme` | `url.scheme` |
66+
| `path` | `url.path` |
67+
| `query` | `url.query` |
68+
| `http_version` | `network.protocol.version` |
69+
| `method` | `http.request.method` |
70+
| `server` | `server.address`, `server.port` |
71+
| `client` | `client.address`, `client.port` |
72+
| full URL | `url.full` |
2473

2574
### Removed
2675

sentry_sdk/integrations/_asgi_common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def _get_headers(asgi_scope):
3232
return headers
3333

3434

35-
def _get_url(asgi_scope, default_scheme, host):
36-
# type: (Dict[str, Any], Literal["ws", "http"], Optional[Union[AnnotatedValue, str]]) -> str
35+
def _get_url(asgi_scope, default_scheme=None, host=None):
36+
# type: (Dict[str, Any], Optional[Literal["ws", "http"]], Optional[Union[AnnotatedValue, str]]) -> str
3737
"""
3838
Extract URL from the ASGI scope, without also including the querystring.
3939
"""

sentry_sdk/integrations/aiohttp.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@
6565

6666
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
6767

68+
REQUEST_PROPERTY_TO_ATTRIBUTE = {
69+
"query_string": "url.query",
70+
"method": "http.request.method",
71+
"scheme": "url.scheme",
72+
"path": "url.path",
73+
}
74+
6875

6976
class AioHttpIntegration(Integration):
7077
identifier = "aiohttp"
@@ -127,19 +134,19 @@ async def sentry_app_handle(self, request, *args, **kwargs):
127134

128135
headers = dict(request.headers)
129136
with sentry_sdk.continue_trace(headers):
130-
with sentry_sdk.start_transaction(
137+
with sentry_sdk.start_span(
131138
op=OP.HTTP_SERVER,
132139
# If this transaction name makes it to the UI, AIOHTTP's
133140
# URL resolver did not find a route or died trying.
134141
name="generic AIOHTTP request",
135142
source=TRANSACTION_SOURCE_ROUTE,
136143
origin=AioHttpIntegration.origin,
137-
custom_sampling_context={"aiohttp_request": request},
138-
) as transaction:
144+
attributes=_prepopulate_attributes(request),
145+
) as span:
139146
try:
140147
response = await old_handle(self, request)
141148
except HTTPException as e:
142-
transaction.set_http_status(e.status_code)
149+
span.set_http_status(e.status_code)
143150

144151
if (
145152
e.status_code
@@ -149,14 +156,14 @@ async def sentry_app_handle(self, request, *args, **kwargs):
149156

150157
raise
151158
except (asyncio.CancelledError, ConnectionResetError):
152-
transaction.set_status(SPANSTATUS.CANCELLED)
159+
span.set_status(SPANSTATUS.CANCELLED)
153160
raise
154161
except Exception:
155162
# This will probably map to a 500 but seems like we
156163
# have no way to tell. Do not set span status.
157164
reraise(*_capture_exception())
158165

159-
transaction.set_http_status(response.status)
166+
span.set_http_status(response.status)
160167
return response
161168

162169
Application._handle = sentry_app_handle
@@ -363,3 +370,30 @@ def get_aiohttp_request_data(request):
363370

364371
# request has no body
365372
return None
373+
374+
375+
def _prepopulate_attributes(request):
376+
# type: (Request) -> dict[str, Any]
377+
"""Construct initial span attributes that can be used in traces sampler."""
378+
attributes = {}
379+
380+
for prop, attr in REQUEST_PROPERTY_TO_ATTRIBUTE.items():
381+
if getattr(request, prop, None) is not None:
382+
attributes[attr] = getattr(request, prop)
383+
384+
if getattr(request, "host", None) is not None:
385+
try:
386+
host, port = request.host.split(":")
387+
attributes["server.address"] = host
388+
attributes["server.port"] = port
389+
except ValueError:
390+
attributes["server.address"] = request.host
391+
392+
try:
393+
url = f"{request.scheme}://{request.host}{request.path}" # noqa: E231
394+
if request.query_string:
395+
attributes["url.full"] = f"{url}?{request.query_string}"
396+
except Exception:
397+
pass
398+
399+
return attributes

sentry_sdk/integrations/arq.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,17 @@ def _sentry_create_worker(*args, **kwargs):
206206
# type: (*Any, **Any) -> Worker
207207
settings_cls = args[0]
208208

209+
if isinstance(settings_cls, dict):
210+
if "functions" in settings_cls:
211+
settings_cls["functions"] = [
212+
_get_arq_function(func) for func in settings_cls["functions"]
213+
]
214+
if "cron_jobs" in settings_cls:
215+
settings_cls["cron_jobs"] = [
216+
_get_arq_cron_job(cron_job)
217+
for cron_job in settings_cls["cron_jobs"]
218+
]
219+
209220
if hasattr(settings_cls, "functions"):
210221
settings_cls.functions = [
211222
_get_arq_function(func) for func in settings_cls.functions

sentry_sdk/integrations/asgi.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from sentry_sdk.integrations._asgi_common import (
1717
_get_headers,
18+
_get_query,
1819
_get_request_data,
1920
_get_url,
2021
)
@@ -57,6 +58,14 @@
5758

5859
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
5960

61+
ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE = {
62+
"http_version": "network.protocol.version",
63+
"method": "http.request.method",
64+
"path": "url.path",
65+
"scheme": "url.scheme",
66+
"type": "network.protocol.name",
67+
}
68+
6069

6170
def _capture_exception(exc, mechanism_type="asgi"):
6271
# type: (Any, str) -> None
@@ -209,27 +218,25 @@ async def _run_app(self, scope, receive, send, asgi_version):
209218
name=transaction_name,
210219
source=transaction_source,
211220
origin=self.span_origin,
212-
custom_sampling_context={"asgi_scope": scope},
221+
attributes=_prepopulate_attributes(scope),
213222
)
214223
if should_trace
215224
else nullcontext()
216-
) as transaction:
217-
if transaction is not None:
218-
logger.debug(
219-
"[ASGI] Started transaction: %s", transaction
220-
)
221-
transaction.set_tag("asgi.type", ty)
225+
) as span:
226+
if span is not None:
227+
logger.debug("[ASGI] Started transaction: %s", span)
228+
span.set_tag("asgi.type", ty)
222229
try:
223230

224231
async def _sentry_wrapped_send(event):
225232
# type: (Dict[str, Any]) -> Any
226233
is_http_response = (
227234
event.get("type") == "http.response.start"
228-
and transaction is not None
235+
and span is not None
229236
and "status" in event
230237
)
231238
if is_http_response:
232-
transaction.set_http_status(event["status"])
239+
span.set_http_status(event["status"])
233240

234241
return await send(event)
235242

@@ -324,3 +331,36 @@ def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
324331
return name, source
325332

326333
return name, source
334+
335+
336+
def _prepopulate_attributes(scope):
337+
# type: (Any) -> dict[str, Any]
338+
"""Unpack ASGI scope into serializable OTel attributes."""
339+
scope = scope or {}
340+
341+
attributes = {}
342+
for attr, key in ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE.items():
343+
if scope.get(attr):
344+
attributes[key] = scope[attr]
345+
346+
for attr in ("client", "server"):
347+
if scope.get(attr):
348+
try:
349+
host, port = scope[attr]
350+
attributes[f"{attr}.address"] = host
351+
attributes[f"{attr}.port"] = port
352+
except Exception:
353+
pass
354+
355+
try:
356+
full_url = _get_url(scope)
357+
query = _get_query(scope)
358+
if query:
359+
attributes["url.query"] = query
360+
full_url = f"{full_url}?{query}"
361+
362+
attributes["url.full"] = full_url
363+
except Exception:
364+
pass
365+
366+
return attributes

sentry_sdk/integrations/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def _emit(self, record):
202202
client_options=client_options,
203203
mechanism={"type": "logging", "handled": True},
204204
)
205-
elif record.exc_info and record.exc_info[0] is None:
205+
elif (record.exc_info and record.exc_info[0] is None) or record.stack_info:
206206
event = {}
207207
hint = {}
208208
with capture_internal_exceptions():

sentry_sdk/integrations/opentelemetry/sampler.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,30 +120,39 @@ def should_sample(
120120
if not has_tracing_enabled(client.options):
121121
return dropped_result(parent_span_context, attributes)
122122

123+
# parent_span_context.is_valid means this span has a parent, remote or local
124+
is_root_span = not parent_span_context.is_valid or parent_span_context.is_remote
125+
123126
# Explicit sampled value provided at start_span
124127
if attributes.get(SentrySpanAttribute.CUSTOM_SAMPLED) is not None:
125-
sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED])
126-
if sample_rate > 0:
127-
return sampled_result(parent_span_context, attributes, sample_rate)
128+
if is_root_span:
129+
sample_rate = float(attributes[SentrySpanAttribute.CUSTOM_SAMPLED])
130+
if sample_rate > 0:
131+
return sampled_result(parent_span_context, attributes, sample_rate)
132+
else:
133+
return dropped_result(parent_span_context, attributes)
128134
else:
129-
return dropped_result(parent_span_context, attributes)
135+
logger.debug(
136+
f"[Tracing] Ignoring sampled param for non-root span {name}"
137+
)
130138

131139
sample_rate = None
132140

133141
# Check if there is a traces_sampler
134142
# Traces_sampler is responsible to check parent sampled to have full transactions.
135143
has_traces_sampler = callable(client.options.get("traces_sampler"))
136-
if has_traces_sampler:
144+
145+
if is_root_span and has_traces_sampler:
137146
sampling_context = {
138147
"transaction_context": {
139148
"name": name,
140149
"op": attributes.get(SentrySpanAttribute.OP),
150+
"source": attributes.get(SentrySpanAttribute.SOURCE),
141151
},
142152
"parent_sampled": get_parent_sampled(parent_span_context, trace_id),
143153
}
144154
sampling_context.update(attributes)
145155
sample_rate = client.options["traces_sampler"](sampling_context)
146-
147156
else:
148157
# Check if there is a parent with a sampling decision
149158
parent_sampled = get_parent_sampled(parent_span_context, trace_id)
@@ -161,8 +170,7 @@ def should_sample(
161170
return dropped_result(parent_span_context, attributes)
162171

163172
# Down-sample in case of back pressure monitor says so
164-
# TODO: this should only be done for transactions (aka root spans)
165-
if client.monitor:
173+
if is_root_span and client.monitor:
166174
sample_rate /= 2**client.monitor.downsample_factor
167175

168176
# Roll the dice on sample rate

0 commit comments

Comments
 (0)