Skip to content

Commit d33bf7e

Browse files
committed
Merge branch 'master' into potel-base
2 parents 0a2d878 + 2f4b028 commit d33bf7e

File tree

13 files changed

+282
-133
lines changed

13 files changed

+282
-133
lines changed

.github/workflows/test-integrations-web-2.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
strategy:
3030
fail-fast: false
3131
matrix:
32-
python-version: ["3.8","3.9","3.11","3.12","3.13"]
32+
python-version: ["3.8","3.9","3.12","3.13"]
3333
os: [ubuntu-22.04]
3434
steps:
3535
- uses: actions/checkout@v4.2.2

scripts/populate_tox/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@
6969
"launchdarkly": {
7070
"package": "launchdarkly-server-sdk",
7171
},
72+
"litestar": {
73+
"package": "litestar",
74+
"deps": {
75+
"*": ["pytest-asyncio", "python-multipart", "requests", "cryptography"],
76+
"<2.7": ["httpx<0.28"],
77+
},
78+
},
7279
"loguru": {
7380
"package": "loguru",
7481
},

scripts/populate_tox/populate_tox.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,26 @@
4949
# suites over to this script. Some entries will probably stay forever
5050
# as they don't fit the mold (e.g. common, asgi, which don't have a 3rd party
5151
# pypi package to install in different versions).
52+
#
53+
# Test suites that will have to remain hardcoded since they don't fit the
54+
# toxgen usecase
55+
"asgi",
56+
"aws_lambda",
57+
"cloud_resource_context",
5258
"common",
5359
"gevent",
5460
"opentelemetry",
5561
"potel",
62+
# Integrations that can be migrated -- we should eventually remove all
63+
# of these from the IGNORE list
5664
"aiohttp",
5765
"anthropic",
5866
"arq",
59-
"asgi",
6067
"asyncpg",
61-
"aws_lambda",
6268
"beam",
6369
"boto3",
6470
"chalice",
6571
"cohere",
66-
"cloud_resource_context",
67-
"cohere",
6872
"django",
6973
"fastapi",
7074
"gcp",
@@ -73,7 +77,6 @@
7377
"huggingface_hub",
7478
"langchain",
7579
"langchain_notiktoken",
76-
"litestar",
7780
"openai",
7881
"openai_notiktoken",
7982
"pure_eval",

scripts/populate_tox/tox.jinja

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,6 @@ envlist =
115115
{py3.9,py3.11,py3.12}-langchain-latest
116116
{py3.9,py3.11,py3.12}-langchain-notiktoken
117117

118-
# Litestar
119-
{py3.8,py3.11}-litestar-v{2.0}
120-
{py3.8,py3.11,py3.12}-litestar-v{2.6}
121-
{py3.8,py3.11,py3.12}-litestar-v{2.12}
122-
{py3.8,py3.11,py3.12}-litestar-latest
123-
124118
# OpenAI
125119
{py3.9,py3.11,py3.12}-openai-v1.0
126120
{py3.9,py3.11,py3.12}-openai-v1.22
@@ -346,17 +340,6 @@ deps =
346340
langchain-{latest,notiktoken}: openai>=1.6.1
347341
langchain-latest: tiktoken~=0.6.0
348342
349-
# Litestar
350-
litestar: pytest-asyncio
351-
litestar: python-multipart
352-
litestar: requests
353-
litestar: cryptography
354-
litestar-v{2.0,2.6}: httpx<0.28
355-
litestar-v2.0: litestar~=2.0.0
356-
litestar-v2.6: litestar~=2.6.0
357-
litestar-v2.12: litestar~=2.12.0
358-
litestar-latest: litestar
359-
360343
# OpenAI
361344
openai: pytest-asyncio
362345
openai-v1.0: openai~=1.0.0

sentry_sdk/_experimental_logger.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# NOTE: this is the logger sentry exposes to users, not some generic logger.
22
import functools
3+
import time
34
from typing import Any
45

56
from sentry_sdk import get_client, get_current_scope
@@ -9,7 +10,27 @@ def _capture_log(severity_text, severity_number, template, **kwargs):
910
# type: (str, int, str, **Any) -> None
1011
client = get_client()
1112
scope = get_current_scope()
12-
client.capture_log(scope, severity_text, severity_number, template, **kwargs)
13+
14+
attrs = {
15+
"sentry.message.template": template,
16+
} # type: dict[str, str | bool | float | int]
17+
if "attributes" in kwargs:
18+
attrs.update(kwargs.pop("attributes"))
19+
for k, v in kwargs.items():
20+
attrs[f"sentry.message.parameters.{k}"] = v
21+
22+
# noinspection PyProtectedMember
23+
client._capture_experimental_log(
24+
scope,
25+
{
26+
"severity_text": severity_text,
27+
"severity_number": severity_number,
28+
"attributes": attrs,
29+
"body": template.format(**kwargs),
30+
"time_unix_nano": time.time_ns(),
31+
"trace_id": None,
32+
},
33+
)
1334

1435

1536
trace = functools.partial(_capture_log, "trace", 1)

sentry_sdk/client.py

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import os
3-
import time
43
import uuid
54
import random
65
import socket
@@ -184,8 +183,8 @@ def capture_event(self, *args, **kwargs):
184183
# type: (*Any, **Any) -> Optional[str]
185184
return None
186185

187-
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
188-
# type: (Scope, str, int, str, **Any) -> None
186+
def _capture_experimental_log(self, scope, log):
187+
# type: (Scope, Log) -> None
189188
pass
190189

191190
def capture_session(self, *args, **kwargs):
@@ -805,47 +804,36 @@ def capture_event(
805804

806805
return return_value
807806

808-
def capture_log(self, scope, severity_text, severity_number, template, **kwargs):
809-
# type: (Scope, str, int, str, **Any) -> None
807+
def _capture_experimental_log(self, current_scope, log):
808+
# type: (Scope, Log) -> None
810809
logs_enabled = self.options["_experiments"].get("enable_sentry_logs", False)
811810
if not logs_enabled:
812811
return
812+
isolation_scope = current_scope.get_isolation_scope()
813813

814814
headers = {
815815
"sent_at": format_timestamp(datetime.now(timezone.utc)),
816816
} # type: dict[str, object]
817817

818-
attrs = {
819-
"sentry.message.template": template,
820-
} # type: dict[str, str | bool | float | int]
821-
822-
kwargs_attributes = kwargs.get("attributes")
823-
if kwargs_attributes is not None:
824-
attrs.update(kwargs_attributes)
825-
826818
environment = self.options.get("environment")
827-
if environment is not None:
828-
attrs["sentry.environment"] = environment
819+
if environment is not None and "sentry.environment" not in log["attributes"]:
820+
log["attributes"]["sentry.environment"] = environment
829821

830822
release = self.options.get("release")
831-
if release is not None:
832-
attrs["sentry.release"] = release
823+
if release is not None and "sentry.release" not in log["attributes"]:
824+
log["attributes"]["sentry.release"] = release
833825

834-
span = scope.span
835-
if span is not None:
836-
attrs["sentry.trace.parent_span_id"] = span.span_id
826+
span = current_scope.span
827+
if span is not None and "sentry.trace.parent_span_id" not in log["attributes"]:
828+
log["attributes"]["sentry.trace.parent_span_id"] = span.span_id
837829

838-
for k, v in kwargs.items():
839-
attrs[f"sentry.message.parameters.{k}"] = v
840-
841-
log = {
842-
"severity_text": severity_text,
843-
"severity_number": severity_number,
844-
"body": template.format(**kwargs),
845-
"attributes": attrs,
846-
"time_unix_nano": time.time_ns(),
847-
"trace_id": None,
848-
} # type: Log
830+
if log.get("trace_id") is None:
831+
transaction = current_scope.transaction
832+
propagation_context = isolation_scope.get_active_propagation_context()
833+
if transaction is not None:
834+
log["trace_id"] = transaction.trace_id
835+
elif propagation_context is not None:
836+
log["trace_id"] = propagation_context.trace_id
849837

850838
# If debug is enabled, log the log to the console
851839
debug = self.options.get("debug", False)
@@ -859,15 +847,10 @@ def capture_log(self, scope, severity_text, severity_number, template, **kwargs)
859847
"fatal": logging.CRITICAL,
860848
}
861849
logger.log(
862-
severity_text_to_logging_level.get(severity_text, logging.DEBUG),
850+
severity_text_to_logging_level.get(log["severity_text"], logging.DEBUG),
863851
f'[Sentry Logs] {log["body"]}',
864852
)
865853

866-
propagation_context = scope.get_active_propagation_context()
867-
if propagation_context is not None:
868-
headers["trace_id"] = propagation_context.trace_id
869-
log["trace_id"] = propagation_context.trace_id
870-
871854
envelope = Envelope(headers=headers)
872855

873856
before_emit_log = self.options["_experiments"].get("before_emit_log")

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class CompressionAlgo(Enum):
7070
"transport_compression_algo": Optional[CompressionAlgo],
7171
"transport_num_pools": Optional[int],
7272
"transport_http2": Optional[bool],
73+
"enable_sentry_logs": Optional[bool],
7374
},
7475
total=False,
7576
)

sentry_sdk/integrations/dramatiq.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def before_process_message(self, broker, message):
9595
message._scope_manager.__enter__()
9696

9797
scope = sentry_sdk.get_current_scope()
98-
scope.transaction = message.actor_name
98+
scope.set_transaction_name(message.actor_name)
9999
scope.set_extra("dramatiq_message_id", message.message_id)
100100
scope.add_event_processor(_make_message_event_processor(message, integration))
101101

sentry_sdk/integrations/logging.py

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import json
12
import logging
23
from datetime import datetime, timezone
34
from fnmatch import fnmatch
45

56
import sentry_sdk
7+
from sentry_sdk.client import BaseClient
68
from sentry_sdk.utils import (
79
to_string,
810
event_from_exception,
@@ -11,7 +13,7 @@
1113
)
1214
from sentry_sdk.integrations import Integration
1315

14-
from typing import TYPE_CHECKING
16+
from typing import TYPE_CHECKING, Tuple
1517

1618
if TYPE_CHECKING:
1719
from collections.abc import MutableMapping
@@ -66,14 +68,23 @@ def ignore_logger(
6668
class LoggingIntegration(Integration):
6769
identifier = "logging"
6870

69-
def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
70-
# type: (Optional[int], Optional[int]) -> None
71+
def __init__(
72+
self,
73+
level=DEFAULT_LEVEL,
74+
event_level=DEFAULT_EVENT_LEVEL,
75+
sentry_logs_level=DEFAULT_LEVEL,
76+
):
77+
# type: (Optional[int], Optional[int], Optional[int]) -> None
7178
self._handler = None
7279
self._breadcrumb_handler = None
80+
self._sentry_logs_handler = None
7381

7482
if level is not None:
7583
self._breadcrumb_handler = BreadcrumbHandler(level=level)
7684

85+
if sentry_logs_level is not None:
86+
self._sentry_logs_handler = SentryLogsHandler(level=sentry_logs_level)
87+
7788
if event_level is not None:
7889
self._handler = EventHandler(level=event_level)
7990

@@ -88,6 +99,12 @@ def _handle_record(self, record):
8899
):
89100
self._breadcrumb_handler.handle(record)
90101

102+
if (
103+
self._sentry_logs_handler is not None
104+
and record.levelno >= self._sentry_logs_handler.level
105+
):
106+
self._sentry_logs_handler.handle(record)
107+
91108
@staticmethod
92109
def setup_once():
93110
# type: () -> None
@@ -301,3 +318,90 @@ def _breadcrumb_from_record(self, record):
301318
"timestamp": datetime.fromtimestamp(record.created, timezone.utc),
302319
"data": self._extra_from_record(record),
303320
}
321+
322+
323+
def _python_level_to_otel(record_level):
324+
# type: (int) -> Tuple[int, str]
325+
for py_level, otel_severity_number, otel_severity_text in [
326+
(50, 21, "fatal"),
327+
(40, 17, "error"),
328+
(30, 13, "warn"),
329+
(20, 9, "info"),
330+
(10, 5, "debug"),
331+
(5, 1, "trace"),
332+
]:
333+
if record_level >= py_level:
334+
return otel_severity_number, otel_severity_text
335+
return 0, "default"
336+
337+
338+
class SentryLogsHandler(_BaseHandler):
339+
"""
340+
A logging handler that records Sentry logs for each Python log record.
341+
342+
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
343+
"""
344+
345+
def emit(self, record):
346+
# type: (LogRecord) -> Any
347+
with capture_internal_exceptions():
348+
self.format(record)
349+
if not self._can_record(record):
350+
return
351+
352+
client = sentry_sdk.get_client()
353+
if not client.is_active():
354+
return
355+
356+
if not client.options["_experiments"].get("enable_sentry_logs", False):
357+
return
358+
359+
SentryLogsHandler._capture_log_from_record(client, record)
360+
361+
@staticmethod
362+
def _capture_log_from_record(client, record):
363+
# type: (BaseClient, LogRecord) -> None
364+
scope = sentry_sdk.get_current_scope()
365+
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
366+
attrs = {
367+
"sentry.message.template": (
368+
record.msg if isinstance(record.msg, str) else json.dumps(record.msg)
369+
),
370+
} # type: dict[str, str | bool | float | int]
371+
if record.args is not None:
372+
if isinstance(record.args, tuple):
373+
for i, arg in enumerate(record.args):
374+
attrs[f"sentry.message.parameters.{i}"] = (
375+
arg if isinstance(arg, str) else json.dumps(arg)
376+
)
377+
if record.lineno:
378+
attrs["code.line.number"] = record.lineno
379+
if record.pathname:
380+
attrs["code.file.path"] = record.pathname
381+
if record.funcName:
382+
attrs["code.function.name"] = record.funcName
383+
384+
if record.thread:
385+
attrs["thread.id"] = record.thread
386+
if record.threadName:
387+
attrs["thread.name"] = record.threadName
388+
389+
if record.process:
390+
attrs["process.pid"] = record.process
391+
if record.processName:
392+
attrs["process.executable.name"] = record.processName
393+
if record.name:
394+
attrs["logger.name"] = record.name
395+
396+
# noinspection PyProtectedMember
397+
client._capture_experimental_log(
398+
scope,
399+
{
400+
"severity_text": otel_severity_text,
401+
"severity_number": otel_severity_number,
402+
"body": record.message,
403+
"attributes": attrs,
404+
"time_unix_nano": int(record.created * 1e9),
405+
"trace_id": None,
406+
},
407+
)

0 commit comments

Comments
 (0)