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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/__init__.py CHANGED
@@ -1,21 +1,60 @@
1
- from sentry_sdk.hub import Hub, init
1
+ from sentry_sdk import profiler
2
+ from sentry_sdk import metrics
2
3
  from sentry_sdk.scope import Scope
3
4
  from sentry_sdk.transport import Transport, HttpTransport
4
5
  from sentry_sdk.client import Client
5
6
 
6
7
  from sentry_sdk.api import * # noqa
7
- from sentry_sdk.api import __all__ as api_all
8
+ from sentry_sdk.consts import VERSION
8
9
 
9
- from sentry_sdk.consts import VERSION # noqa
10
-
11
- __all__ = api_all + [ # noqa
10
+ __all__ = [ # noqa
12
11
  "Hub",
13
12
  "Scope",
14
13
  "Client",
15
14
  "Transport",
16
15
  "HttpTransport",
17
- "init",
16
+ "VERSION",
18
17
  "integrations",
18
+ # From sentry_sdk.api
19
+ "init",
20
+ "add_attachment",
21
+ "add_breadcrumb",
22
+ "capture_event",
23
+ "capture_exception",
24
+ "capture_message",
25
+ "configure_scope",
26
+ "continue_trace",
27
+ "flush",
28
+ "get_baggage",
29
+ "get_client",
30
+ "get_global_scope",
31
+ "get_isolation_scope",
32
+ "get_current_scope",
33
+ "get_current_span",
34
+ "get_traceparent",
35
+ "is_initialized",
36
+ "isolation_scope",
37
+ "last_event_id",
38
+ "new_scope",
39
+ "push_scope",
40
+ "set_context",
41
+ "set_extra",
42
+ "set_level",
43
+ "set_measurement",
44
+ "set_tag",
45
+ "set_tags",
46
+ "set_user",
47
+ "start_span",
48
+ "start_transaction",
49
+ "trace",
50
+ "monitor",
51
+ "logger",
52
+ "metrics",
53
+ "profiler",
54
+ "start_session",
55
+ "end_session",
56
+ "set_transaction_name",
57
+ "update_current_span",
19
58
  ]
20
59
 
21
60
  # Initialize the debug support after everything is loaded
@@ -23,3 +62,6 @@ from sentry_sdk.debug import init_debug_support
23
62
 
24
63
  init_debug_support()
25
64
  del init_debug_support
65
+
66
+ # circular imports
67
+ from sentry_sdk.hub import Hub
sentry_sdk/_compat.py CHANGED
@@ -1,58 +1,18 @@
1
1
  import sys
2
2
 
3
- from sentry_sdk._types import MYPY
3
+ from typing import TYPE_CHECKING
4
4
 
5
- if MYPY:
6
- from typing import Optional
7
- from typing import Tuple
5
+ if TYPE_CHECKING:
8
6
  from typing import Any
9
- from typing import Type
10
-
11
7
  from typing import TypeVar
12
8
 
13
9
  T = TypeVar("T")
14
10
 
15
11
 
16
- PY2 = sys.version_info[0] == 2
17
-
18
- if PY2:
19
- import urlparse # noqa
20
-
21
- text_type = unicode # noqa
22
-
23
- string_types = (str, text_type)
24
- number_types = (int, long, float) # noqa
25
- int_types = (int, long) # noqa
26
- iteritems = lambda x: x.iteritems() # noqa: B301
27
-
28
- def implements_str(cls):
29
- # type: (T) -> T
30
- cls.__unicode__ = cls.__str__
31
- cls.__str__ = lambda x: unicode(x).encode("utf-8") # noqa
32
- return cls
33
-
34
- exec("def reraise(tp, value, tb=None):\n raise tp, value, tb")
35
-
36
-
37
- else:
38
- import urllib.parse as urlparse # noqa
39
-
40
- text_type = str
41
- string_types = (text_type,) # type: Tuple[type]
42
- number_types = (int, float) # type: Tuple[type, type]
43
- int_types = (int,) # noqa
44
- iteritems = lambda x: x.items()
45
-
46
- def implements_str(x):
47
- # type: (T) -> T
48
- return x
49
-
50
- def reraise(tp, value, tb=None):
51
- # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> None
52
- assert value is not None
53
- if value.__traceback__ is not tb:
54
- raise value.with_traceback(tb)
55
- raise value
12
+ PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7
13
+ PY38 = sys.version_info[0] == 3 and sys.version_info[1] >= 8
14
+ PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10
15
+ PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11
56
16
 
57
17
 
58
18
  def with_metaclass(meta, *bases):
@@ -65,26 +25,74 @@ def with_metaclass(meta, *bases):
65
25
  return type.__new__(MetaClass, "temporary_class", (), {})
66
26
 
67
27
 
68
- def check_thread_support():
69
- # type: () -> None
28
+ def check_uwsgi_thread_support():
29
+ # type: () -> bool
30
+ # We check two things here:
31
+ #
32
+ # 1. uWSGI doesn't run in threaded mode by default -- issue a warning if
33
+ # that's the case.
34
+ #
35
+ # 2. Additionally, if uWSGI is running in preforking mode (default), it needs
36
+ # the --py-call-uwsgi-fork-hooks option for the SDK to work properly. This
37
+ # is because any background threads spawned before the main process is
38
+ # forked are NOT CLEANED UP IN THE CHILDREN BY DEFAULT even if
39
+ # --enable-threads is on. One has to explicitly provide
40
+ # --py-call-uwsgi-fork-hooks to force uWSGI to run regular cpython
41
+ # after-fork hooks that take care of cleaning up stale thread data.
70
42
  try:
71
43
  from uwsgi import opt # type: ignore
72
44
  except ImportError:
73
- return
45
+ return True
46
+
47
+ from sentry_sdk.consts import FALSE_VALUES
48
+
49
+ def enabled(option):
50
+ # type: (str) -> bool
51
+ value = opt.get(option, False)
52
+ if isinstance(value, bool):
53
+ return value
54
+
55
+ if isinstance(value, bytes):
56
+ try:
57
+ value = value.decode()
58
+ except Exception:
59
+ pass
60
+
61
+ return value and str(value).lower() not in FALSE_VALUES
74
62
 
75
63
  # When `threads` is passed in as a uwsgi option,
76
64
  # `enable-threads` is implied on.
77
- if "threads" in opt:
78
- return
65
+ threads_enabled = "threads" in opt or enabled("enable-threads")
66
+ fork_hooks_on = enabled("py-call-uwsgi-fork-hooks")
67
+ lazy_mode = enabled("lazy-apps") or enabled("lazy")
68
+
69
+ if lazy_mode and not threads_enabled:
70
+ from warnings import warn
71
+
72
+ warn(
73
+ Warning(
74
+ "IMPORTANT: "
75
+ "We detected the use of uWSGI without thread support. "
76
+ "This might lead to unexpected issues. "
77
+ 'Please run uWSGI with "--enable-threads" for full support.'
78
+ )
79
+ )
79
80
 
80
- if str(opt.get("enable-threads", "0")).lower() in ("false", "off", "no", "0"):
81
+ return False
82
+
83
+ elif not lazy_mode and (not threads_enabled or not fork_hooks_on):
81
84
  from warnings import warn
82
85
 
83
86
  warn(
84
87
  Warning(
85
- "We detected the use of uwsgi with disabled threads. "
86
- "This will cause issues with the transport you are "
87
- "trying to use. Please enable threading for uwsgi. "
88
- '(Add the "enable-threads" flag).'
88
+ "IMPORTANT: "
89
+ "We detected the use of uWSGI in preforking mode without "
90
+ "thread support. This might lead to crashing workers. "
91
+ 'Please run uWSGI with both "--enable-threads" and '
92
+ '"--py-call-uwsgi-fork-hooks" for full support.'
89
93
  )
90
94
  )
95
+
96
+ return False
97
+
98
+ return True
@@ -0,0 +1,84 @@
1
+ import warnings
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import sentry_sdk
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Any, ContextManager, Optional
9
+
10
+ import sentry_sdk.consts
11
+
12
+
13
+ class _InitGuard:
14
+ _CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE = (
15
+ "Using the return value of sentry_sdk.init as a context manager "
16
+ "and manually calling the __enter__ and __exit__ methods on the "
17
+ "return value are deprecated. We are no longer maintaining this "
18
+ "functionality, and we will remove it in the next major release."
19
+ )
20
+
21
+ def __init__(self, client):
22
+ # type: (sentry_sdk.Client) -> None
23
+ self._client = client
24
+
25
+ def __enter__(self):
26
+ # type: () -> _InitGuard
27
+ warnings.warn(
28
+ self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE,
29
+ stacklevel=2,
30
+ category=DeprecationWarning,
31
+ )
32
+
33
+ return self
34
+
35
+ def __exit__(self, exc_type, exc_value, tb):
36
+ # type: (Any, Any, Any) -> None
37
+ warnings.warn(
38
+ self._CONTEXT_MANAGER_DEPRECATION_WARNING_MESSAGE,
39
+ stacklevel=2,
40
+ category=DeprecationWarning,
41
+ )
42
+
43
+ c = self._client
44
+ if c is not None:
45
+ c.close()
46
+
47
+
48
+ def _check_python_deprecations():
49
+ # type: () -> None
50
+ # Since we're likely to deprecate Python versions in the future, I'm keeping
51
+ # this handy function around. Use this to detect the Python version used and
52
+ # to output logger.warning()s if it's deprecated.
53
+ pass
54
+
55
+
56
+ def _init(*args, **kwargs):
57
+ # type: (*Optional[str], **Any) -> ContextManager[Any]
58
+ """Initializes the SDK and optionally integrations.
59
+
60
+ This takes the same arguments as the client constructor.
61
+ """
62
+ client = sentry_sdk.Client(*args, **kwargs)
63
+ sentry_sdk.get_global_scope().set_client(client)
64
+ _check_python_deprecations()
65
+ rv = _InitGuard(client)
66
+ return rv
67
+
68
+
69
+ if TYPE_CHECKING:
70
+ # Make mypy, PyCharm and other static analyzers think `init` is a type to
71
+ # have nicer autocompletion for params.
72
+ #
73
+ # Use `ClientConstructor` to define the argument types of `init` and
74
+ # `ContextManager[Any]` to tell static analyzers about the return type.
75
+
76
+ class init(sentry_sdk.consts.ClientConstructor, _InitGuard): # noqa: N801
77
+ pass
78
+
79
+ else:
80
+ # Alias `init` for actual usage. Go through the lambda indirection to throw
81
+ # PyCharm off of the weakly typed signature (it would otherwise discover
82
+ # both the weakly typed signature of `_init` and our faked `init` type).
83
+
84
+ init = (lambda: _init)()
@@ -0,0 +1,172 @@
1
+ import os
2
+ import random
3
+ import threading
4
+ from datetime import datetime, timezone
5
+ from typing import Optional, List, Callable, TYPE_CHECKING, Any
6
+
7
+ from sentry_sdk.utils import format_timestamp, safe_repr
8
+ from sentry_sdk.envelope import Envelope, Item, PayloadRef
9
+
10
+ if TYPE_CHECKING:
11
+ from sentry_sdk._types import Log
12
+
13
+
14
+ class LogBatcher:
15
+ MAX_LOGS_BEFORE_FLUSH = 100
16
+ MAX_LOGS_BEFORE_DROP = 1_000
17
+ FLUSH_WAIT_TIME = 5.0
18
+
19
+ def __init__(
20
+ self,
21
+ capture_func, # type: Callable[[Envelope], None]
22
+ record_lost_func, # type: Callable[..., None]
23
+ ):
24
+ # type: (...) -> None
25
+ self._log_buffer = [] # type: List[Log]
26
+ self._capture_func = capture_func
27
+ self._record_lost_func = record_lost_func
28
+ self._running = True
29
+ self._lock = threading.Lock()
30
+
31
+ self._flush_event = threading.Event() # type: threading.Event
32
+
33
+ self._flusher = None # type: Optional[threading.Thread]
34
+ self._flusher_pid = None # type: Optional[int]
35
+
36
+ def _ensure_thread(self):
37
+ # type: (...) -> bool
38
+ """For forking processes we might need to restart this thread.
39
+ This ensures that our process actually has that thread running.
40
+ """
41
+ if not self._running:
42
+ return False
43
+
44
+ pid = os.getpid()
45
+ if self._flusher_pid == pid:
46
+ return True
47
+
48
+ with self._lock:
49
+ # Recheck to make sure another thread didn't get here and start the
50
+ # the flusher in the meantime
51
+ if self._flusher_pid == pid:
52
+ return True
53
+
54
+ self._flusher_pid = pid
55
+
56
+ self._flusher = threading.Thread(target=self._flush_loop)
57
+ self._flusher.daemon = True
58
+
59
+ try:
60
+ self._flusher.start()
61
+ except RuntimeError:
62
+ # Unfortunately at this point the interpreter is in a state that no
63
+ # longer allows us to spawn a thread and we have to bail.
64
+ self._running = False
65
+ return False
66
+
67
+ return True
68
+
69
+ def _flush_loop(self):
70
+ # type: (...) -> None
71
+ while self._running:
72
+ self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random())
73
+ self._flush_event.clear()
74
+ self._flush()
75
+
76
+ def add(
77
+ self,
78
+ log, # type: Log
79
+ ):
80
+ # type: (...) -> None
81
+ if not self._ensure_thread() or self._flusher is None:
82
+ return None
83
+
84
+ with self._lock:
85
+ if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_DROP:
86
+ self._record_lost_func(
87
+ reason="queue_overflow",
88
+ data_category="log_item",
89
+ quantity=1,
90
+ )
91
+ return None
92
+
93
+ self._log_buffer.append(log)
94
+ if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_FLUSH:
95
+ self._flush_event.set()
96
+
97
+ def kill(self):
98
+ # type: (...) -> None
99
+ if self._flusher is None:
100
+ return
101
+
102
+ self._running = False
103
+ self._flush_event.set()
104
+ self._flusher = None
105
+
106
+ def flush(self):
107
+ # type: (...) -> None
108
+ self._flush()
109
+
110
+ @staticmethod
111
+ def _log_to_transport_format(log):
112
+ # type: (Log) -> Any
113
+ def format_attribute(val):
114
+ # type: (int | float | str | bool) -> Any
115
+ if isinstance(val, bool):
116
+ return {"value": val, "type": "boolean"}
117
+ if isinstance(val, int):
118
+ return {"value": val, "type": "integer"}
119
+ if isinstance(val, float):
120
+ return {"value": val, "type": "double"}
121
+ if isinstance(val, str):
122
+ return {"value": val, "type": "string"}
123
+ return {"value": safe_repr(val), "type": "string"}
124
+
125
+ if "sentry.severity_number" not in log["attributes"]:
126
+ log["attributes"]["sentry.severity_number"] = log["severity_number"]
127
+ if "sentry.severity_text" not in log["attributes"]:
128
+ log["attributes"]["sentry.severity_text"] = log["severity_text"]
129
+
130
+ res = {
131
+ "timestamp": int(log["time_unix_nano"]) / 1.0e9,
132
+ "trace_id": log.get("trace_id", "00000000-0000-0000-0000-000000000000"),
133
+ "level": str(log["severity_text"]),
134
+ "body": str(log["body"]),
135
+ "attributes": {
136
+ k: format_attribute(v) for (k, v) in log["attributes"].items()
137
+ },
138
+ }
139
+
140
+ return res
141
+
142
+ def _flush(self):
143
+ # type: (...) -> Optional[Envelope]
144
+
145
+ envelope = Envelope(
146
+ headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
147
+ )
148
+ with self._lock:
149
+ if len(self._log_buffer) == 0:
150
+ return None
151
+
152
+ envelope.add_item(
153
+ Item(
154
+ type="log",
155
+ content_type="application/vnd.sentry.items.log+json",
156
+ headers={
157
+ "item_count": len(self._log_buffer),
158
+ },
159
+ payload=PayloadRef(
160
+ json={
161
+ "items": [
162
+ self._log_to_transport_format(log)
163
+ for log in self._log_buffer
164
+ ]
165
+ }
166
+ ),
167
+ )
168
+ )
169
+ self._log_buffer.clear()
170
+
171
+ self._capture_func(envelope)
172
+ return envelope
@@ -0,0 +1,47 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ if TYPE_CHECKING:
4
+ from typing import Any
5
+
6
+
7
+ _SENTINEL = object()
8
+
9
+
10
+ class LRUCache:
11
+ def __init__(self, max_size):
12
+ # type: (int) -> None
13
+ if max_size <= 0:
14
+ raise AssertionError(f"invalid max_size: {max_size}")
15
+ self.max_size = max_size
16
+ self._data = {} # type: dict[Any, Any]
17
+ self.hits = self.misses = 0
18
+ self.full = False
19
+
20
+ def set(self, key, value):
21
+ # type: (Any, Any) -> None
22
+ current = self._data.pop(key, _SENTINEL)
23
+ if current is not _SENTINEL:
24
+ self._data[key] = value
25
+ elif self.full:
26
+ self._data.pop(next(iter(self._data)))
27
+ self._data[key] = value
28
+ else:
29
+ self._data[key] = value
30
+ self.full = len(self._data) >= self.max_size
31
+
32
+ def get(self, key, default=None):
33
+ # type: (Any, Any) -> Any
34
+ try:
35
+ ret = self._data.pop(key)
36
+ except KeyError:
37
+ self.misses += 1
38
+ ret = default
39
+ else:
40
+ self.hits += 1
41
+ self._data[key] = ret
42
+
43
+ return ret
44
+
45
+ def get_all(self):
46
+ # type: () -> list[tuple[Any, Any]]
47
+ return list(self._data.items())
@@ -0,0 +1,167 @@
1
+ import os
2
+ import random
3
+ import threading
4
+ from datetime import datetime, timezone
5
+ from typing import Optional, List, Callable, TYPE_CHECKING, Any, Union
6
+
7
+ from sentry_sdk.utils import format_timestamp, safe_repr
8
+ from sentry_sdk.envelope import Envelope, Item, PayloadRef
9
+
10
+ if TYPE_CHECKING:
11
+ from sentry_sdk._types import Metric
12
+
13
+
14
+ class MetricsBatcher:
15
+ MAX_METRICS_BEFORE_FLUSH = 1000
16
+ MAX_METRICS_BEFORE_DROP = 10_000
17
+ FLUSH_WAIT_TIME = 5.0
18
+
19
+ def __init__(
20
+ self,
21
+ capture_func, # type: Callable[[Envelope], None]
22
+ record_lost_func, # type: Callable[..., None]
23
+ ):
24
+ # type: (...) -> None
25
+ self._metric_buffer = [] # type: List[Metric]
26
+ self._capture_func = capture_func
27
+ self._record_lost_func = record_lost_func
28
+ self._running = True
29
+ self._lock = threading.Lock()
30
+
31
+ self._flush_event = threading.Event() # type: threading.Event
32
+
33
+ self._flusher = None # type: Optional[threading.Thread]
34
+ self._flusher_pid = None # type: Optional[int]
35
+
36
+ def _ensure_thread(self):
37
+ # type: (...) -> bool
38
+ if not self._running:
39
+ return False
40
+
41
+ pid = os.getpid()
42
+ if self._flusher_pid == pid:
43
+ return True
44
+
45
+ with self._lock:
46
+ if self._flusher_pid == pid:
47
+ return True
48
+
49
+ self._flusher_pid = pid
50
+
51
+ self._flusher = threading.Thread(target=self._flush_loop)
52
+ self._flusher.daemon = True
53
+
54
+ try:
55
+ self._flusher.start()
56
+ except RuntimeError:
57
+ self._running = False
58
+ return False
59
+
60
+ return True
61
+
62
+ def _flush_loop(self):
63
+ # type: (...) -> None
64
+ while self._running:
65
+ self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random())
66
+ self._flush_event.clear()
67
+ self._flush()
68
+
69
+ def add(
70
+ self,
71
+ metric, # type: Metric
72
+ ):
73
+ # type: (...) -> None
74
+ if not self._ensure_thread() or self._flusher is None:
75
+ return None
76
+
77
+ with self._lock:
78
+ if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_DROP:
79
+ self._record_lost_func(
80
+ reason="queue_overflow",
81
+ data_category="trace_metric",
82
+ quantity=1,
83
+ )
84
+ return None
85
+
86
+ self._metric_buffer.append(metric)
87
+ if len(self._metric_buffer) >= self.MAX_METRICS_BEFORE_FLUSH:
88
+ self._flush_event.set()
89
+
90
+ def kill(self):
91
+ # type: (...) -> None
92
+ if self._flusher is None:
93
+ return
94
+
95
+ self._running = False
96
+ self._flush_event.set()
97
+ self._flusher = None
98
+
99
+ def flush(self):
100
+ # type: (...) -> None
101
+ self._flush()
102
+
103
+ @staticmethod
104
+ def _metric_to_transport_format(metric):
105
+ # type: (Metric) -> Any
106
+ def format_attribute(val):
107
+ # type: (Union[int, float, str, bool]) -> Any
108
+ if isinstance(val, bool):
109
+ return {"value": val, "type": "boolean"}
110
+ if isinstance(val, int):
111
+ return {"value": val, "type": "integer"}
112
+ if isinstance(val, float):
113
+ return {"value": val, "type": "double"}
114
+ if isinstance(val, str):
115
+ return {"value": val, "type": "string"}
116
+ return {"value": safe_repr(val), "type": "string"}
117
+
118
+ res = {
119
+ "timestamp": metric["timestamp"],
120
+ "trace_id": metric["trace_id"],
121
+ "name": metric["name"],
122
+ "type": metric["type"],
123
+ "value": metric["value"],
124
+ "attributes": {
125
+ k: format_attribute(v) for (k, v) in metric["attributes"].items()
126
+ },
127
+ }
128
+
129
+ if metric.get("span_id") is not None:
130
+ res["span_id"] = metric["span_id"]
131
+
132
+ if metric.get("unit") is not None:
133
+ res["unit"] = metric["unit"]
134
+
135
+ return res
136
+
137
+ def _flush(self):
138
+ # type: (...) -> Optional[Envelope]
139
+
140
+ envelope = Envelope(
141
+ headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
142
+ )
143
+ with self._lock:
144
+ if len(self._metric_buffer) == 0:
145
+ return None
146
+
147
+ envelope.add_item(
148
+ Item(
149
+ type="trace_metric",
150
+ content_type="application/vnd.sentry.items.trace-metric+json",
151
+ headers={
152
+ "item_count": len(self._metric_buffer),
153
+ },
154
+ payload=PayloadRef(
155
+ json={
156
+ "items": [
157
+ self._metric_to_transport_format(metric)
158
+ for metric in self._metric_buffer
159
+ ]
160
+ }
161
+ ),
162
+ )
163
+ )
164
+ self._metric_buffer.clear()
165
+
166
+ self._capture_func(envelope)
167
+ return envelope