sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/integrations/wsgi.py
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
import sentry_sdk
|
|
5
|
+
from sentry_sdk._werkzeug import get_host, _get_headers
|
|
6
|
+
from sentry_sdk.api import continue_trace
|
|
7
|
+
from sentry_sdk.consts import OP
|
|
8
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
9
|
+
from sentry_sdk.integrations._wsgi_common import (
|
|
10
|
+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
|
|
11
|
+
_filter_headers,
|
|
12
|
+
nullcontext,
|
|
13
|
+
)
|
|
14
|
+
from sentry_sdk.sessions import track_session
|
|
15
|
+
from sentry_sdk.scope import use_isolation_scope
|
|
16
|
+
from sentry_sdk.tracing import Transaction, TransactionSource
|
|
5
17
|
from sentry_sdk.utils import (
|
|
6
18
|
ContextVar,
|
|
7
19
|
capture_internal_exceptions,
|
|
8
20
|
event_from_exception,
|
|
21
|
+
reraise,
|
|
9
22
|
)
|
|
10
|
-
from sentry_sdk._compat import PY2, reraise, iteritems
|
|
11
|
-
from sentry_sdk.tracing import Transaction
|
|
12
|
-
from sentry_sdk.sessions import auto_session_tracking
|
|
13
|
-
from sentry_sdk.integrations._wsgi_common import _filter_headers
|
|
14
23
|
|
|
15
|
-
from
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
16
25
|
|
|
17
|
-
if
|
|
26
|
+
if TYPE_CHECKING:
|
|
18
27
|
from typing import Callable
|
|
19
28
|
from typing import Dict
|
|
20
29
|
from typing import Iterator
|
|
@@ -25,14 +34,14 @@ if MYPY:
|
|
|
25
34
|
from typing import Protocol
|
|
26
35
|
|
|
27
36
|
from sentry_sdk.utils import ExcInfo
|
|
28
|
-
from sentry_sdk._types import EventProcessor
|
|
37
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
29
38
|
|
|
30
39
|
WsgiResponseIter = TypeVar("WsgiResponseIter")
|
|
31
40
|
WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
|
|
32
41
|
WsgiExcInfo = TypeVar("WsgiExcInfo")
|
|
33
42
|
|
|
34
43
|
class StartResponse(Protocol):
|
|
35
|
-
def __call__(self, status, response_headers, exc_info=None):
|
|
44
|
+
def __call__(self, status, response_headers, exc_info=None): # type: ignore
|
|
36
45
|
# type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
|
|
37
46
|
pass
|
|
38
47
|
|
|
@@ -40,60 +49,46 @@ if MYPY:
|
|
|
40
49
|
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
|
|
41
50
|
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# type: (str, str, str) -> str
|
|
47
|
-
return s.decode(charset, errors)
|
|
48
|
-
|
|
52
|
+
def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
|
|
53
|
+
# type: (str, str, str) -> str
|
|
54
|
+
return s.encode("latin1").decode(charset, errors)
|
|
49
55
|
|
|
50
|
-
else:
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return s.encode("latin1").decode(charset, errors)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_host(environ):
|
|
58
|
-
# type: (Dict[str, str]) -> str
|
|
59
|
-
"""Return the host for the given WSGI environment. Yanked from Werkzeug."""
|
|
60
|
-
if environ.get("HTTP_HOST"):
|
|
61
|
-
rv = environ["HTTP_HOST"]
|
|
62
|
-
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
|
|
63
|
-
rv = rv[:-3]
|
|
64
|
-
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
|
|
65
|
-
rv = rv[:-4]
|
|
66
|
-
elif environ.get("SERVER_NAME"):
|
|
67
|
-
rv = environ["SERVER_NAME"]
|
|
68
|
-
if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
|
|
69
|
-
("https", "443"),
|
|
70
|
-
("http", "80"),
|
|
71
|
-
):
|
|
72
|
-
rv += ":" + environ["SERVER_PORT"]
|
|
73
|
-
else:
|
|
74
|
-
# In spite of the WSGI spec, SERVER_NAME might not be present.
|
|
75
|
-
rv = "unknown"
|
|
76
|
-
|
|
77
|
-
return rv
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def get_request_url(environ):
|
|
81
|
-
# type: (Dict[str, str]) -> str
|
|
57
|
+
def get_request_url(environ, use_x_forwarded_for=False):
|
|
58
|
+
# type: (Dict[str, str], bool) -> str
|
|
82
59
|
"""Return the absolute URL without query string for the given WSGI
|
|
83
60
|
environment."""
|
|
61
|
+
script_name = environ.get("SCRIPT_NAME", "").rstrip("/")
|
|
62
|
+
path_info = environ.get("PATH_INFO", "").lstrip("/")
|
|
63
|
+
path = f"{script_name}/{path_info}"
|
|
64
|
+
|
|
84
65
|
return "%s://%s/%s" % (
|
|
85
66
|
environ.get("wsgi.url_scheme"),
|
|
86
|
-
get_host(environ),
|
|
87
|
-
wsgi_decoding_dance(
|
|
67
|
+
get_host(environ, use_x_forwarded_for),
|
|
68
|
+
wsgi_decoding_dance(path).lstrip("/"),
|
|
88
69
|
)
|
|
89
70
|
|
|
90
71
|
|
|
91
|
-
class SentryWsgiMiddleware
|
|
92
|
-
__slots__ = (
|
|
72
|
+
class SentryWsgiMiddleware:
|
|
73
|
+
__slots__ = (
|
|
74
|
+
"app",
|
|
75
|
+
"use_x_forwarded_for",
|
|
76
|
+
"span_origin",
|
|
77
|
+
"http_methods_to_capture",
|
|
78
|
+
)
|
|
93
79
|
|
|
94
|
-
def __init__(
|
|
95
|
-
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
|
|
83
|
+
use_x_forwarded_for=False, # type: bool
|
|
84
|
+
span_origin="manual", # type: str
|
|
85
|
+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
|
|
86
|
+
):
|
|
87
|
+
# type: (...) -> None
|
|
96
88
|
self.app = app
|
|
89
|
+
self.use_x_forwarded_for = use_x_forwarded_for
|
|
90
|
+
self.span_origin = span_origin
|
|
91
|
+
self.http_methods_to_capture = http_methods_to_capture
|
|
97
92
|
|
|
98
93
|
def __call__(self, environ, start_response):
|
|
99
94
|
# type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
|
|
@@ -102,40 +97,55 @@ class SentryWsgiMiddleware(object):
|
|
|
102
97
|
|
|
103
98
|
_wsgi_middleware_applied.set(True)
|
|
104
99
|
try:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
with hub:
|
|
100
|
+
with sentry_sdk.isolation_scope() as scope:
|
|
101
|
+
with track_session(scope, session_mode="request"):
|
|
108
102
|
with capture_internal_exceptions():
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
scope.clear_breadcrumbs()
|
|
104
|
+
scope._name = "wsgi"
|
|
105
|
+
scope.add_event_processor(
|
|
106
|
+
_make_wsgi_event_processor(
|
|
107
|
+
environ, self.use_x_forwarded_for
|
|
114
108
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
method = environ.get("REQUEST_METHOD", "").upper()
|
|
112
|
+
transaction = None
|
|
113
|
+
if method in self.http_methods_to_capture:
|
|
114
|
+
transaction = continue_trace(
|
|
115
|
+
environ,
|
|
116
|
+
op=OP.HTTP_SERVER,
|
|
117
|
+
name="generic WSGI request",
|
|
118
|
+
source=TransactionSource.ROUTE,
|
|
119
|
+
origin=self.span_origin,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
transaction_context = (
|
|
123
|
+
sentry_sdk.start_transaction(
|
|
124
|
+
transaction,
|
|
125
|
+
custom_sampling_context={"wsgi_environ": environ},
|
|
126
|
+
)
|
|
127
|
+
if transaction is not None
|
|
128
|
+
else nullcontext()
|
|
118
129
|
)
|
|
119
|
-
|
|
120
|
-
with hub.start_transaction(transaction):
|
|
130
|
+
with transaction_context:
|
|
121
131
|
try:
|
|
122
|
-
|
|
132
|
+
response = self.app(
|
|
123
133
|
environ,
|
|
124
134
|
partial(
|
|
125
135
|
_sentry_start_response, start_response, transaction
|
|
126
136
|
),
|
|
127
137
|
)
|
|
128
138
|
except BaseException:
|
|
129
|
-
reraise(*_capture_exception(
|
|
139
|
+
reraise(*_capture_exception())
|
|
130
140
|
finally:
|
|
131
141
|
_wsgi_middleware_applied.set(False)
|
|
132
142
|
|
|
133
|
-
return _ScopedResponse(
|
|
143
|
+
return _ScopedResponse(scope, response)
|
|
134
144
|
|
|
135
145
|
|
|
136
|
-
def _sentry_start_response(
|
|
146
|
+
def _sentry_start_response( # type: ignore
|
|
137
147
|
old_start_response, # type: StartResponse
|
|
138
|
-
transaction, # type: Transaction
|
|
148
|
+
transaction, # type: Optional[Transaction]
|
|
139
149
|
status, # type: str
|
|
140
150
|
response_headers, # type: WsgiResponseHeaders
|
|
141
151
|
exc_info=None, # type: Optional[WsgiExcInfo]
|
|
@@ -143,7 +153,8 @@ def _sentry_start_response(
|
|
|
143
153
|
# type: (...) -> WsgiResponseIter
|
|
144
154
|
with capture_internal_exceptions():
|
|
145
155
|
status_int = int(status.split(" ", 1)[0])
|
|
146
|
-
transaction
|
|
156
|
+
if transaction is not None:
|
|
157
|
+
transaction.set_http_status(status_int)
|
|
147
158
|
|
|
148
159
|
if exc_info is None:
|
|
149
160
|
# The Django Rest Framework WSGI test client, and likely other
|
|
@@ -161,7 +172,7 @@ def _get_environ(environ):
|
|
|
161
172
|
capture (server name, port and remote addr if pii is enabled).
|
|
162
173
|
"""
|
|
163
174
|
keys = ["SERVER_NAME", "SERVER_PORT"]
|
|
164
|
-
if
|
|
175
|
+
if should_send_default_pii():
|
|
165
176
|
# make debugging of proxy setup easier. Proxy headers are
|
|
166
177
|
# in headers.
|
|
167
178
|
keys += ["REMOTE_ADDR"]
|
|
@@ -171,27 +182,6 @@ def _get_environ(environ):
|
|
|
171
182
|
yield key, environ[key]
|
|
172
183
|
|
|
173
184
|
|
|
174
|
-
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
|
|
175
|
-
#
|
|
176
|
-
# We need this function because Django does not give us a "pure" http header
|
|
177
|
-
# dict. So we might as well use it for all WSGI integrations.
|
|
178
|
-
def _get_headers(environ):
|
|
179
|
-
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
|
|
180
|
-
"""
|
|
181
|
-
Returns only proper HTTP headers.
|
|
182
|
-
|
|
183
|
-
"""
|
|
184
|
-
for key, value in iteritems(environ):
|
|
185
|
-
key = str(key)
|
|
186
|
-
if key.startswith("HTTP_") and key not in (
|
|
187
|
-
"HTTP_CONTENT_TYPE",
|
|
188
|
-
"HTTP_CONTENT_LENGTH",
|
|
189
|
-
):
|
|
190
|
-
yield key[5:].replace("_", "-").title(), value
|
|
191
|
-
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
|
|
192
|
-
yield key.replace("_", "-").title(), value
|
|
193
|
-
|
|
194
|
-
|
|
195
185
|
def get_client_ip(environ):
|
|
196
186
|
# type: (Dict[str, str]) -> Optional[Any]
|
|
197
187
|
"""
|
|
@@ -212,33 +202,44 @@ def get_client_ip(environ):
|
|
|
212
202
|
return environ.get("REMOTE_ADDR")
|
|
213
203
|
|
|
214
204
|
|
|
215
|
-
def _capture_exception(
|
|
216
|
-
# type: (
|
|
205
|
+
def _capture_exception():
|
|
206
|
+
# type: () -> ExcInfo
|
|
207
|
+
"""
|
|
208
|
+
Captures the current exception and sends it to Sentry.
|
|
209
|
+
Returns the ExcInfo tuple to it can be reraised afterwards.
|
|
210
|
+
"""
|
|
217
211
|
exc_info = sys.exc_info()
|
|
212
|
+
e = exc_info[1]
|
|
213
|
+
|
|
214
|
+
# SystemExit(0) is the only uncaught exception that is expected behavior
|
|
215
|
+
should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
|
|
216
|
+
if not should_skip_capture:
|
|
217
|
+
event, hint = event_from_exception(
|
|
218
|
+
exc_info,
|
|
219
|
+
client_options=sentry_sdk.get_client().options,
|
|
220
|
+
mechanism={"type": "wsgi", "handled": False},
|
|
221
|
+
)
|
|
222
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
if hub.client is not None:
|
|
221
|
-
e = exc_info[1]
|
|
224
|
+
return exc_info
|
|
222
225
|
|
|
223
|
-
# SystemExit(0) is the only uncaught exception that is expected behavior
|
|
224
|
-
should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
|
|
225
|
-
if not should_skip_capture:
|
|
226
|
-
event, hint = event_from_exception(
|
|
227
|
-
exc_info,
|
|
228
|
-
client_options=hub.client.options,
|
|
229
|
-
mechanism={"type": "wsgi", "handled": False},
|
|
230
|
-
)
|
|
231
|
-
hub.capture_event(event, hint=hint)
|
|
232
226
|
|
|
233
|
-
|
|
227
|
+
class _ScopedResponse:
|
|
228
|
+
"""
|
|
229
|
+
Users a separate scope for each response chunk.
|
|
234
230
|
|
|
231
|
+
This will make WSGI apps more tolerant against:
|
|
232
|
+
- WSGI servers streaming responses from a different thread/from
|
|
233
|
+
different threads than the one that called start_response
|
|
234
|
+
- close() not being called
|
|
235
|
+
- WSGI servers streaming responses interleaved from the same thread
|
|
236
|
+
"""
|
|
235
237
|
|
|
236
|
-
|
|
237
|
-
__slots__ = ("_response", "_hub")
|
|
238
|
+
__slots__ = ("_response", "_scope")
|
|
238
239
|
|
|
239
|
-
def __init__(self,
|
|
240
|
-
# type: (
|
|
241
|
-
self.
|
|
240
|
+
def __init__(self, scope, response):
|
|
241
|
+
# type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
|
|
242
|
+
self._scope = scope
|
|
242
243
|
self._response = response
|
|
243
244
|
|
|
244
245
|
def __iter__(self):
|
|
@@ -246,33 +247,33 @@ class _ScopedResponse(object):
|
|
|
246
247
|
iterator = iter(self._response)
|
|
247
248
|
|
|
248
249
|
while True:
|
|
249
|
-
with self.
|
|
250
|
+
with use_isolation_scope(self._scope):
|
|
250
251
|
try:
|
|
251
252
|
chunk = next(iterator)
|
|
252
253
|
except StopIteration:
|
|
253
254
|
break
|
|
254
255
|
except BaseException:
|
|
255
|
-
reraise(*_capture_exception(
|
|
256
|
+
reraise(*_capture_exception())
|
|
256
257
|
|
|
257
258
|
yield chunk
|
|
258
259
|
|
|
259
260
|
def close(self):
|
|
260
261
|
# type: () -> None
|
|
261
|
-
with self.
|
|
262
|
+
with use_isolation_scope(self._scope):
|
|
262
263
|
try:
|
|
263
264
|
self._response.close() # type: ignore
|
|
264
265
|
except AttributeError:
|
|
265
266
|
pass
|
|
266
267
|
except BaseException:
|
|
267
|
-
reraise(*_capture_exception(
|
|
268
|
+
reraise(*_capture_exception())
|
|
268
269
|
|
|
269
270
|
|
|
270
|
-
def _make_wsgi_event_processor(environ):
|
|
271
|
-
# type: (Dict[str, str]) -> EventProcessor
|
|
271
|
+
def _make_wsgi_event_processor(environ, use_x_forwarded_for):
|
|
272
|
+
# type: (Dict[str, str], bool) -> EventProcessor
|
|
272
273
|
# It's a bit unfortunate that we have to extract and parse the request data
|
|
273
274
|
# from the environ so eagerly, but there are a few good reasons for this.
|
|
274
275
|
#
|
|
275
|
-
# We might be in a situation where the scope
|
|
276
|
+
# We might be in a situation where the scope never gets torn down
|
|
276
277
|
# properly. In that case we will have an unnecessary strong reference to
|
|
277
278
|
# all objects in the environ (some of which may take a lot of memory) when
|
|
278
279
|
# we're really just interested in a few of them.
|
|
@@ -282,19 +283,19 @@ def _make_wsgi_event_processor(environ):
|
|
|
282
283
|
# https://github.com/unbit/uwsgi/issues/1950
|
|
283
284
|
|
|
284
285
|
client_ip = get_client_ip(environ)
|
|
285
|
-
request_url = get_request_url(environ)
|
|
286
|
+
request_url = get_request_url(environ, use_x_forwarded_for)
|
|
286
287
|
query_string = environ.get("QUERY_STRING")
|
|
287
288
|
method = environ.get("REQUEST_METHOD")
|
|
288
289
|
env = dict(_get_environ(environ))
|
|
289
290
|
headers = _filter_headers(dict(_get_headers(environ)))
|
|
290
291
|
|
|
291
292
|
def event_processor(event, hint):
|
|
292
|
-
# type: (
|
|
293
|
+
# type: (Event, Dict[str, Any]) -> Event
|
|
293
294
|
with capture_internal_exceptions():
|
|
294
295
|
# if the code below fails halfway through we at least have some data
|
|
295
296
|
request_info = event.setdefault("request", {})
|
|
296
297
|
|
|
297
|
-
if
|
|
298
|
+
if should_send_default_pii():
|
|
298
299
|
user_info = event.setdefault("user", {})
|
|
299
300
|
if client_ip:
|
|
300
301
|
user_info.setdefault("ip_address", client_ip)
|
sentry_sdk/logger.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# NOTE: this is the logger sentry exposes to users, not some generic logger.
|
|
2
|
+
import functools
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sentry_sdk import get_client
|
|
7
|
+
from sentry_sdk.utils import safe_repr, capture_internal_exceptions
|
|
8
|
+
|
|
9
|
+
OTEL_RANGES = [
|
|
10
|
+
# ((severity level range), severity text)
|
|
11
|
+
# https://opentelemetry.io/docs/specs/otel/logs/data-model
|
|
12
|
+
((1, 4), "trace"),
|
|
13
|
+
((5, 8), "debug"),
|
|
14
|
+
((9, 12), "info"),
|
|
15
|
+
((13, 16), "warn"),
|
|
16
|
+
((17, 20), "error"),
|
|
17
|
+
((21, 24), "fatal"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _dict_default_key(dict): # type: ignore[type-arg]
|
|
22
|
+
"""dict that returns the key if missing."""
|
|
23
|
+
|
|
24
|
+
def __missing__(self, key):
|
|
25
|
+
# type: (str) -> str
|
|
26
|
+
return "{" + key + "}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _capture_log(severity_text, severity_number, template, **kwargs):
|
|
30
|
+
# type: (str, int, str, **Any) -> None
|
|
31
|
+
client = get_client()
|
|
32
|
+
|
|
33
|
+
body = template
|
|
34
|
+
attrs = {} # type: dict[str, str | bool | float | int]
|
|
35
|
+
if "attributes" in kwargs:
|
|
36
|
+
attrs.update(kwargs.pop("attributes"))
|
|
37
|
+
for k, v in kwargs.items():
|
|
38
|
+
attrs[f"sentry.message.parameter.{k}"] = v
|
|
39
|
+
if kwargs:
|
|
40
|
+
# only attach template if there are parameters
|
|
41
|
+
attrs["sentry.message.template"] = template
|
|
42
|
+
|
|
43
|
+
with capture_internal_exceptions():
|
|
44
|
+
body = template.format_map(_dict_default_key(kwargs))
|
|
45
|
+
|
|
46
|
+
attrs = {
|
|
47
|
+
k: (
|
|
48
|
+
v
|
|
49
|
+
if (
|
|
50
|
+
isinstance(v, str)
|
|
51
|
+
or isinstance(v, int)
|
|
52
|
+
or isinstance(v, bool)
|
|
53
|
+
or isinstance(v, float)
|
|
54
|
+
)
|
|
55
|
+
else safe_repr(v)
|
|
56
|
+
)
|
|
57
|
+
for (k, v) in attrs.items()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# noinspection PyProtectedMember
|
|
61
|
+
client._capture_log(
|
|
62
|
+
{
|
|
63
|
+
"severity_text": severity_text,
|
|
64
|
+
"severity_number": severity_number,
|
|
65
|
+
"attributes": attrs,
|
|
66
|
+
"body": body,
|
|
67
|
+
"time_unix_nano": time.time_ns(),
|
|
68
|
+
"trace_id": None,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
trace = functools.partial(_capture_log, "trace", 1)
|
|
74
|
+
debug = functools.partial(_capture_log, "debug", 5)
|
|
75
|
+
info = functools.partial(_capture_log, "info", 9)
|
|
76
|
+
warning = functools.partial(_capture_log, "warn", 13)
|
|
77
|
+
error = functools.partial(_capture_log, "error", 17)
|
|
78
|
+
fatal = functools.partial(_capture_log, "fatal", 21)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _otel_severity_text(otel_severity_number):
|
|
82
|
+
# type: (int) -> str
|
|
83
|
+
for (lower, upper), severity in OTEL_RANGES:
|
|
84
|
+
if lower <= otel_severity_number <= upper:
|
|
85
|
+
return severity
|
|
86
|
+
|
|
87
|
+
return "default"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _log_level_to_otel(level, mapping):
|
|
91
|
+
# type: (int, dict[Any, int]) -> tuple[int, str]
|
|
92
|
+
for py_level, otel_severity_number in sorted(mapping.items(), reverse=True):
|
|
93
|
+
if level >= py_level:
|
|
94
|
+
return otel_severity_number, _otel_severity_text(otel_severity_number)
|
|
95
|
+
|
|
96
|
+
return 0, "default"
|
sentry_sdk/metrics.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NOTE: This file contains experimental code that may be changed or removed at any
|
|
3
|
+
time without prior notice.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Optional, TYPE_CHECKING, Union
|
|
8
|
+
|
|
9
|
+
import sentry_sdk
|
|
10
|
+
from sentry_sdk.utils import safe_repr
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sentry_sdk._types import Metric, MetricType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _capture_metric(
|
|
17
|
+
name, # type: str
|
|
18
|
+
metric_type, # type: MetricType
|
|
19
|
+
value, # type: float
|
|
20
|
+
unit=None, # type: Optional[str]
|
|
21
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
22
|
+
):
|
|
23
|
+
# type: (...) -> None
|
|
24
|
+
client = sentry_sdk.get_client()
|
|
25
|
+
|
|
26
|
+
attrs = {} # type: dict[str, Union[str, bool, float, int]]
|
|
27
|
+
if attributes:
|
|
28
|
+
for k, v in attributes.items():
|
|
29
|
+
attrs[k] = (
|
|
30
|
+
v
|
|
31
|
+
if (
|
|
32
|
+
isinstance(v, str)
|
|
33
|
+
or isinstance(v, int)
|
|
34
|
+
or isinstance(v, bool)
|
|
35
|
+
or isinstance(v, float)
|
|
36
|
+
)
|
|
37
|
+
else safe_repr(v)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
metric = {
|
|
41
|
+
"timestamp": time.time(),
|
|
42
|
+
"trace_id": None,
|
|
43
|
+
"span_id": None,
|
|
44
|
+
"name": name,
|
|
45
|
+
"type": metric_type,
|
|
46
|
+
"value": float(value),
|
|
47
|
+
"unit": unit,
|
|
48
|
+
"attributes": attrs,
|
|
49
|
+
} # type: Metric
|
|
50
|
+
|
|
51
|
+
client._capture_metric(metric)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def count(
|
|
55
|
+
name, # type: str
|
|
56
|
+
value, # type: float
|
|
57
|
+
unit=None, # type: Optional[str]
|
|
58
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
59
|
+
):
|
|
60
|
+
# type: (...) -> None
|
|
61
|
+
_capture_metric(name, "counter", value, unit, attributes)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def gauge(
|
|
65
|
+
name, # type: str
|
|
66
|
+
value, # type: float
|
|
67
|
+
unit=None, # type: Optional[str]
|
|
68
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
69
|
+
):
|
|
70
|
+
# type: (...) -> None
|
|
71
|
+
_capture_metric(name, "gauge", value, unit, attributes)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def distribution(
|
|
75
|
+
name, # type: str
|
|
76
|
+
value, # type: float
|
|
77
|
+
unit=None, # type: Optional[str]
|
|
78
|
+
attributes=None, # type: Optional[dict[str, Any]]
|
|
79
|
+
):
|
|
80
|
+
# type: (...) -> None
|
|
81
|
+
_capture_metric(name, "distribution", value, unit, attributes)
|
sentry_sdk/monitor.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from threading import Thread, Lock
|
|
4
|
+
|
|
5
|
+
import sentry_sdk
|
|
6
|
+
from sentry_sdk.utils import logger
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_DOWNSAMPLE_FACTOR = 10
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Monitor:
|
|
18
|
+
"""
|
|
19
|
+
Performs health checks in a separate thread once every interval seconds
|
|
20
|
+
and updates the internal state. Other parts of the SDK only read this state
|
|
21
|
+
and act accordingly.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
name = "sentry.monitor"
|
|
25
|
+
|
|
26
|
+
def __init__(self, transport, interval=10):
|
|
27
|
+
# type: (sentry_sdk.transport.Transport, float) -> None
|
|
28
|
+
self.transport = transport # type: sentry_sdk.transport.Transport
|
|
29
|
+
self.interval = interval # type: float
|
|
30
|
+
|
|
31
|
+
self._healthy = True
|
|
32
|
+
self._downsample_factor = 0 # type: int
|
|
33
|
+
|
|
34
|
+
self._thread = None # type: Optional[Thread]
|
|
35
|
+
self._thread_lock = Lock()
|
|
36
|
+
self._thread_for_pid = None # type: Optional[int]
|
|
37
|
+
self._running = True
|
|
38
|
+
|
|
39
|
+
def _ensure_running(self):
|
|
40
|
+
# type: () -> None
|
|
41
|
+
"""
|
|
42
|
+
Check that the monitor has an active thread to run in, or create one if not.
|
|
43
|
+
|
|
44
|
+
Note that this might fail (e.g. in Python 3.12 it's not possible to
|
|
45
|
+
spawn new threads at interpreter shutdown). In that case self._running
|
|
46
|
+
will be False after running this function.
|
|
47
|
+
"""
|
|
48
|
+
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
with self._thread_lock:
|
|
52
|
+
if self._thread_for_pid == os.getpid() and self._thread is not None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def _thread():
|
|
56
|
+
# type: (...) -> None
|
|
57
|
+
while self._running:
|
|
58
|
+
time.sleep(self.interval)
|
|
59
|
+
if self._running:
|
|
60
|
+
self.run()
|
|
61
|
+
|
|
62
|
+
thread = Thread(name=self.name, target=_thread)
|
|
63
|
+
thread.daemon = True
|
|
64
|
+
try:
|
|
65
|
+
thread.start()
|
|
66
|
+
except RuntimeError:
|
|
67
|
+
# Unfortunately at this point the interpreter is in a state that no
|
|
68
|
+
# longer allows us to spawn a thread and we have to bail.
|
|
69
|
+
self._running = False
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
self._thread = thread
|
|
73
|
+
self._thread_for_pid = os.getpid()
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def run(self):
|
|
78
|
+
# type: () -> None
|
|
79
|
+
self.check_health()
|
|
80
|
+
self.set_downsample_factor()
|
|
81
|
+
|
|
82
|
+
def set_downsample_factor(self):
|
|
83
|
+
# type: () -> None
|
|
84
|
+
if self._healthy:
|
|
85
|
+
if self._downsample_factor > 0:
|
|
86
|
+
logger.debug(
|
|
87
|
+
"[Monitor] health check positive, reverting to normal sampling"
|
|
88
|
+
)
|
|
89
|
+
self._downsample_factor = 0
|
|
90
|
+
else:
|
|
91
|
+
if self.downsample_factor < MAX_DOWNSAMPLE_FACTOR:
|
|
92
|
+
self._downsample_factor += 1
|
|
93
|
+
logger.debug(
|
|
94
|
+
"[Monitor] health check negative, downsampling with a factor of %d",
|
|
95
|
+
self._downsample_factor,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def check_health(self):
|
|
99
|
+
# type: () -> None
|
|
100
|
+
"""
|
|
101
|
+
Perform the actual health checks,
|
|
102
|
+
currently only checks if the transport is rate-limited.
|
|
103
|
+
TODO: augment in the future with more checks.
|
|
104
|
+
"""
|
|
105
|
+
self._healthy = self.transport.is_healthy()
|
|
106
|
+
|
|
107
|
+
def is_healthy(self):
|
|
108
|
+
# type: () -> bool
|
|
109
|
+
self._ensure_running()
|
|
110
|
+
return self._healthy
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def downsample_factor(self):
|
|
114
|
+
# type: () -> int
|
|
115
|
+
self._ensure_running()
|
|
116
|
+
return self._downsample_factor
|
|
117
|
+
|
|
118
|
+
def kill(self):
|
|
119
|
+
# type: () -> None
|
|
120
|
+
self._running = False
|