sentry-sdk 0.7.5__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 -30
- sentry_sdk/_compat.py +74 -61
- 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 +289 -0
- sentry_sdk/_types.py +338 -0
- 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 +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- 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 +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- 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 +134 -0
- 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 +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- 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 +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- 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 +141 -0
- 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 +112 -68
- 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 +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- 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 +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- 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 +201 -119
- 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/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.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/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/integrations/wsgi.py
CHANGED
|
@@ -1,165 +1,245 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from sentry_sdk.
|
|
6
|
-
from sentry_sdk.
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
17
|
+
from sentry_sdk.utils import (
|
|
18
|
+
ContextVar,
|
|
19
|
+
capture_internal_exceptions,
|
|
20
|
+
event_from_exception,
|
|
21
|
+
reraise,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
9
27
|
from typing import Callable
|
|
10
28
|
from typing import Dict
|
|
11
|
-
from typing import List
|
|
12
29
|
from typing import Iterator
|
|
13
30
|
from typing import Any
|
|
14
31
|
from typing import Tuple
|
|
15
32
|
from typing import Optional
|
|
33
|
+
from typing import TypeVar
|
|
34
|
+
from typing import Protocol
|
|
16
35
|
|
|
17
36
|
from sentry_sdk.utils import ExcInfo
|
|
37
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
18
38
|
|
|
39
|
+
WsgiResponseIter = TypeVar("WsgiResponseIter")
|
|
40
|
+
WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
|
|
41
|
+
WsgiExcInfo = TypeVar("WsgiExcInfo")
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return s.decode(charset, errors)
|
|
25
|
-
|
|
43
|
+
class StartResponse(Protocol):
|
|
44
|
+
def __call__(self, status, response_headers, exc_info=None): # type: ignore
|
|
45
|
+
# type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
|
|
46
|
+
pass
|
|
26
47
|
|
|
27
|
-
else:
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
# type: (str, str, str) -> str
|
|
31
|
-
return s.encode("latin1").decode(charset, errors)
|
|
49
|
+
_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
|
|
32
50
|
|
|
33
51
|
|
|
34
|
-
def
|
|
35
|
-
# type: (
|
|
36
|
-
""
|
|
37
|
-
if environ.get("HTTP_HOST"):
|
|
38
|
-
rv = environ["HTTP_HOST"]
|
|
39
|
-
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
|
|
40
|
-
rv = rv[:-3]
|
|
41
|
-
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
|
|
42
|
-
rv = rv[:-4]
|
|
43
|
-
elif environ.get("SERVER_NAME"):
|
|
44
|
-
rv = environ["SERVER_NAME"]
|
|
45
|
-
if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
|
|
46
|
-
("https", "443"),
|
|
47
|
-
("http", "80"),
|
|
48
|
-
):
|
|
49
|
-
rv += ":" + environ["SERVER_PORT"]
|
|
50
|
-
else:
|
|
51
|
-
# In spite of the WSGI spec, SERVER_NAME might not be present.
|
|
52
|
-
rv = "unknown"
|
|
53
|
-
|
|
54
|
-
return rv
|
|
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)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def get_request_url(environ):
|
|
58
|
-
# type: (Dict[str, str]) -> str
|
|
57
|
+
def get_request_url(environ, use_x_forwarded_for=False):
|
|
58
|
+
# type: (Dict[str, str], bool) -> str
|
|
59
59
|
"""Return the absolute URL without query string for the given WSGI
|
|
60
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
|
+
|
|
61
65
|
return "%s://%s/%s" % (
|
|
62
66
|
environ.get("wsgi.url_scheme"),
|
|
63
|
-
get_host(environ),
|
|
64
|
-
wsgi_decoding_dance(
|
|
67
|
+
get_host(environ, use_x_forwarded_for),
|
|
68
|
+
wsgi_decoding_dance(path).lstrip("/"),
|
|
65
69
|
)
|
|
66
70
|
|
|
67
71
|
|
|
68
|
-
class SentryWsgiMiddleware
|
|
69
|
-
__slots__ = (
|
|
72
|
+
class SentryWsgiMiddleware:
|
|
73
|
+
__slots__ = (
|
|
74
|
+
"app",
|
|
75
|
+
"use_x_forwarded_for",
|
|
76
|
+
"span_origin",
|
|
77
|
+
"http_methods_to_capture",
|
|
78
|
+
)
|
|
70
79
|
|
|
71
|
-
def __init__(
|
|
72
|
-
|
|
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
|
|
73
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
|
|
74
92
|
|
|
75
93
|
def __call__(self, environ, start_response):
|
|
76
|
-
# type: (Dict[str, str], Callable) -> _ScopedResponse
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
# type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
|
|
95
|
+
if _wsgi_middleware_applied.get(False):
|
|
96
|
+
return self.app(environ, start_response)
|
|
97
|
+
|
|
98
|
+
_wsgi_middleware_applied.set(True)
|
|
99
|
+
try:
|
|
100
|
+
with sentry_sdk.isolation_scope() as scope:
|
|
101
|
+
with track_session(scope, session_mode="request"):
|
|
102
|
+
with capture_internal_exceptions():
|
|
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
|
|
108
|
+
)
|
|
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()
|
|
129
|
+
)
|
|
130
|
+
with transaction_context:
|
|
131
|
+
try:
|
|
132
|
+
response = self.app(
|
|
133
|
+
environ,
|
|
134
|
+
partial(
|
|
135
|
+
_sentry_start_response, start_response, transaction
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
except BaseException:
|
|
139
|
+
reraise(*_capture_exception())
|
|
140
|
+
finally:
|
|
141
|
+
_wsgi_middleware_applied.set(False)
|
|
142
|
+
|
|
143
|
+
return _ScopedResponse(scope, response)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sentry_start_response( # type: ignore
|
|
147
|
+
old_start_response, # type: StartResponse
|
|
148
|
+
transaction, # type: Optional[Transaction]
|
|
149
|
+
status, # type: str
|
|
150
|
+
response_headers, # type: WsgiResponseHeaders
|
|
151
|
+
exc_info=None, # type: Optional[WsgiExcInfo]
|
|
152
|
+
):
|
|
153
|
+
# type: (...) -> WsgiResponseIter
|
|
154
|
+
with capture_internal_exceptions():
|
|
155
|
+
status_int = int(status.split(" ", 1)[0])
|
|
156
|
+
if transaction is not None:
|
|
157
|
+
transaction.set_http_status(status_int)
|
|
158
|
+
|
|
159
|
+
if exc_info is None:
|
|
160
|
+
# The Django Rest Framework WSGI test client, and likely other
|
|
161
|
+
# (incorrect) implementations, cannot deal with the exc_info argument
|
|
162
|
+
# if one is present. Avoid providing a third argument if not necessary.
|
|
163
|
+
return old_start_response(status, response_headers)
|
|
164
|
+
else:
|
|
165
|
+
return old_start_response(status, response_headers, exc_info)
|
|
91
166
|
|
|
92
167
|
|
|
93
168
|
def _get_environ(environ):
|
|
94
169
|
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
|
|
95
170
|
"""
|
|
96
|
-
Returns our
|
|
171
|
+
Returns our explicitly included environment variables we want to
|
|
172
|
+
capture (server name, port and remote addr if pii is enabled).
|
|
97
173
|
"""
|
|
98
|
-
keys =
|
|
99
|
-
if
|
|
100
|
-
|
|
174
|
+
keys = ["SERVER_NAME", "SERVER_PORT"]
|
|
175
|
+
if should_send_default_pii():
|
|
176
|
+
# make debugging of proxy setup easier. Proxy headers are
|
|
177
|
+
# in headers.
|
|
178
|
+
keys += ["REMOTE_ADDR"]
|
|
101
179
|
|
|
102
180
|
for key in keys:
|
|
103
181
|
if key in environ:
|
|
104
182
|
yield key, environ[key]
|
|
105
183
|
|
|
106
184
|
|
|
107
|
-
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
|
|
108
|
-
#
|
|
109
|
-
# We need this function because Django does not give us a "pure" http header
|
|
110
|
-
# dict. So we might as well use it for all WSGI integrations.
|
|
111
|
-
def _get_headers(environ):
|
|
112
|
-
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
|
|
113
|
-
"""
|
|
114
|
-
Returns only proper HTTP headers.
|
|
115
|
-
|
|
116
|
-
"""
|
|
117
|
-
for key, value in environ.items():
|
|
118
|
-
key = str(key)
|
|
119
|
-
if key.startswith("HTTP_") and key not in (
|
|
120
|
-
"HTTP_CONTENT_TYPE",
|
|
121
|
-
"HTTP_CONTENT_LENGTH",
|
|
122
|
-
):
|
|
123
|
-
yield key[5:].replace("_", "-").title(), value
|
|
124
|
-
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
|
|
125
|
-
yield key.replace("_", "-").title(), value
|
|
126
|
-
|
|
127
|
-
|
|
128
185
|
def get_client_ip(environ):
|
|
129
186
|
# type: (Dict[str, str]) -> Optional[Any]
|
|
130
187
|
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Note: Don't use this in security sensitive situations since this
|
|
135
|
-
value may be forged from a client.
|
|
188
|
+
Infer the user IP address from various headers. This cannot be used in
|
|
189
|
+
security sensitive situations since the value may be forged from a client,
|
|
190
|
+
but it's good enough for the event payload.
|
|
136
191
|
"""
|
|
137
192
|
try:
|
|
138
193
|
return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
|
|
139
194
|
except (KeyError, IndexError):
|
|
140
|
-
|
|
195
|
+
pass
|
|
141
196
|
|
|
197
|
+
try:
|
|
198
|
+
return environ["HTTP_X_REAL_IP"]
|
|
199
|
+
except KeyError:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
return environ.get("REMOTE_ADDR")
|
|
142
203
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
204
|
+
|
|
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
|
+
"""
|
|
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:
|
|
148
217
|
event, hint = event_from_exception(
|
|
149
218
|
exc_info,
|
|
150
|
-
client_options=
|
|
219
|
+
client_options=sentry_sdk.get_client().options,
|
|
151
220
|
mechanism={"type": "wsgi", "handled": False},
|
|
152
221
|
)
|
|
153
|
-
|
|
222
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
223
|
+
|
|
154
224
|
return exc_info
|
|
155
225
|
|
|
156
226
|
|
|
157
|
-
class _ScopedResponse
|
|
158
|
-
|
|
227
|
+
class _ScopedResponse:
|
|
228
|
+
"""
|
|
229
|
+
Users a separate scope for each response chunk.
|
|
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
|
+
"""
|
|
237
|
+
|
|
238
|
+
__slots__ = ("_response", "_scope")
|
|
159
239
|
|
|
160
|
-
def __init__(self,
|
|
161
|
-
# type: (
|
|
162
|
-
self.
|
|
240
|
+
def __init__(self, scope, response):
|
|
241
|
+
# type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
|
|
242
|
+
self._scope = scope
|
|
163
243
|
self._response = response
|
|
164
244
|
|
|
165
245
|
def __iter__(self):
|
|
@@ -167,32 +247,33 @@ class _ScopedResponse(object):
|
|
|
167
247
|
iterator = iter(self._response)
|
|
168
248
|
|
|
169
249
|
while True:
|
|
170
|
-
with self.
|
|
250
|
+
with use_isolation_scope(self._scope):
|
|
171
251
|
try:
|
|
172
252
|
chunk = next(iterator)
|
|
173
253
|
except StopIteration:
|
|
174
254
|
break
|
|
175
|
-
except
|
|
176
|
-
reraise(*_capture_exception(
|
|
255
|
+
except BaseException:
|
|
256
|
+
reraise(*_capture_exception())
|
|
177
257
|
|
|
178
258
|
yield chunk
|
|
179
259
|
|
|
180
260
|
def close(self):
|
|
181
|
-
|
|
261
|
+
# type: () -> None
|
|
262
|
+
with use_isolation_scope(self._scope):
|
|
182
263
|
try:
|
|
183
|
-
self._response.close()
|
|
264
|
+
self._response.close() # type: ignore
|
|
184
265
|
except AttributeError:
|
|
185
266
|
pass
|
|
186
|
-
except
|
|
187
|
-
reraise(*_capture_exception(
|
|
267
|
+
except BaseException:
|
|
268
|
+
reraise(*_capture_exception())
|
|
188
269
|
|
|
189
270
|
|
|
190
|
-
def _make_wsgi_event_processor(environ):
|
|
191
|
-
# type: (Dict[str, str]) ->
|
|
271
|
+
def _make_wsgi_event_processor(environ, use_x_forwarded_for):
|
|
272
|
+
# type: (Dict[str, str], bool) -> EventProcessor
|
|
192
273
|
# It's a bit unfortunate that we have to extract and parse the request data
|
|
193
274
|
# from the environ so eagerly, but there are a few good reasons for this.
|
|
194
275
|
#
|
|
195
|
-
# We might be in a situation where the scope
|
|
276
|
+
# We might be in a situation where the scope never gets torn down
|
|
196
277
|
# properly. In that case we will have an unnecessary strong reference to
|
|
197
278
|
# all objects in the environ (some of which may take a lot of memory) when
|
|
198
279
|
# we're really just interested in a few of them.
|
|
@@ -202,21 +283,22 @@ def _make_wsgi_event_processor(environ):
|
|
|
202
283
|
# https://github.com/unbit/uwsgi/issues/1950
|
|
203
284
|
|
|
204
285
|
client_ip = get_client_ip(environ)
|
|
205
|
-
request_url = get_request_url(environ)
|
|
286
|
+
request_url = get_request_url(environ, use_x_forwarded_for)
|
|
206
287
|
query_string = environ.get("QUERY_STRING")
|
|
207
288
|
method = environ.get("REQUEST_METHOD")
|
|
208
289
|
env = dict(_get_environ(environ))
|
|
209
290
|
headers = _filter_headers(dict(_get_headers(environ)))
|
|
210
291
|
|
|
211
292
|
def event_processor(event, hint):
|
|
212
|
-
# type: (
|
|
293
|
+
# type: (Event, Dict[str, Any]) -> Event
|
|
213
294
|
with capture_internal_exceptions():
|
|
214
295
|
# if the code below fails halfway through we at least have some data
|
|
215
296
|
request_info = event.setdefault("request", {})
|
|
216
297
|
|
|
217
|
-
if
|
|
298
|
+
if should_send_default_pii():
|
|
218
299
|
user_info = event.setdefault("user", {})
|
|
219
|
-
|
|
300
|
+
if client_ip:
|
|
301
|
+
user_info.setdefault("ip_address", client_ip)
|
|
220
302
|
|
|
221
303
|
request_info["url"] = request_url
|
|
222
304
|
request_info["query_string"] = query_string
|
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)
|