Skip to content

Commit 445ebc9

Browse files
committed
Merge branch 'master' into potel-base
2 parents bff8fdd + adcfa0f commit 445ebc9

28 files changed

+824
-490
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
steps:
2121
- name: Get auth token
2222
id: token
23-
uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7
23+
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
2424
with:
2525
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
2626
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}

.github/workflows/test-integrations-web-1.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.10","3.12","3.13"]
32+
python-version: ["3.8","3.12","3.13"]
3333
os: [ubuntu-22.04]
3434
services:
3535
postgres:

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 2.25.1
4+
5+
### Various fixes & improvements
6+
7+
- fix(logs): Add a class which batches groups of logs together. (#4229) by @colin-sentry
8+
- fix(logs): Use repr instead of json for message and arguments (#4227) by @colin-sentry
9+
- fix(logs): Debug output from Sentry logs should always be `debug` level. (#4224) by @antonpirker
10+
- fix(ai): Do not consume anthropic streaming stop (#4232) by @colin-sentry
11+
- fix(spotlight): Do not spam sentry_sdk.warnings logger w/ Spotlight (#4219) by @BYK
12+
- fix(docs): fixed code snippet (#4218) by @antonpirker
13+
- build(deps): bump actions/create-github-app-token from 1.11.7 to 1.12.0 (#4214) by @dependabot
14+
315
## 2.25.0
416

517
### Various fixes & improvements
@@ -13,6 +25,8 @@
1325
This is how you can use it (Sentry Logs is in beta right now so the API can still change):
1426

1527
```python
28+
import logging
29+
1630
import sentry_sdk
1731
from sentry_sdk.integrations.logging import LoggingIntegration
1832

@@ -23,12 +37,11 @@
2337
"enable_sentry_logs": True
2438
}
2539
integrations=[
26-
LoggingIntegration(sentry_logs_level="error"),
40+
LoggingIntegration(sentry_logs_level=logging.ERROR),
2741
]
2842
)
2943

3044
# Your existing logging setup
31-
import logging
3245
some_logger = logging.Logger("some-logger")
3346

3447
some_logger.info('In this example info events will not be sent to Sentry logs. my_value=%s', my_value)

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year)
3434
author = "Sentry Team and Contributors"
3535

36-
release = "2.25.0"
36+
release = "2.25.1"
3737
version = ".".join(release.split(".")[:2]) # The short X.Y version.
3838

3939

scripts/populate_tox/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@
2929
"clickhouse_driver": {
3030
"package": "clickhouse-driver",
3131
},
32+
"django": {
33+
"package": "django",
34+
"deps": {
35+
"*": [
36+
"psycopg2-binary",
37+
"djangorestframework",
38+
"pytest-django",
39+
"Werkzeug",
40+
],
41+
">=3.0": ["pytest-asyncio"],
42+
">=2.2,<3.1": ["six"],
43+
"<3.3": [
44+
"djangorestframework>=3.0,<4.0",
45+
"Werkzeug<2.1.0",
46+
],
47+
"<3.1": ["pytest-django<4.0"],
48+
">=2.0": ["channels[daphne]"],
49+
},
50+
},
3251
"dramatiq": {
3352
"package": "dramatiq",
3453
},

scripts/populate_tox/populate_tox.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"boto3",
7070
"chalice",
7171
"cohere",
72-
"django",
7372
"fastapi",
7473
"gcp",
7574
"httpx",

scripts/populate_tox/tox.jinja

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,6 @@ envlist =
8080
{py3.9,py3.11,py3.12}-cohere-v5
8181
{py3.9,py3.11,py3.12}-cohere-latest
8282

83-
# Django
84-
# - Django 1.x
85-
{py3.7}-django-v{1.11}
86-
# - Django 2.x
87-
{py3.7}-django-v{2.0}
88-
{py3.7,py3.9}-django-v{2.2}
89-
# - Django 3.x
90-
{py3.7,py3.9}-django-v{3.0}
91-
{py3.7,py3.9,py3.11}-django-v{3.2}
92-
# - Django 4.x
93-
{py3.8,py3.11,py3.12}-django-v{4.0,4.1,4.2}
94-
# - Django 5.x
95-
{py3.10,py3.11,py3.12}-django-v{5.0,5.1}
96-
{py3.10,py3.12,py3.13}-django-latest
97-
9883
# FastAPI
9984
{py3.7,py3.10}-fastapi-v{0.79}
10085
{py3.8,py3.12,py3.13}-fastapi-latest
@@ -266,35 +251,6 @@ deps =
266251
cohere-v5: cohere~=5.3.3
267252
cohere-latest: cohere
268253
269-
# Django
270-
django: psycopg2-binary
271-
django-v{1.11,2.0,2.1,2.2,3.0,3.1,3.2}: djangorestframework>=3.0.0,<4.0.0
272-
django-v{2.0,2.2,3.0,3.2,4.0,4.1,4.2,5.0,5.1}: channels[daphne]
273-
django-v{2.2,3.0}: six
274-
django-v{1.11,2.0,2.2,3.0,3.2}: Werkzeug<2.1.0
275-
django-v{1.11,2.0,2.2,3.0}: pytest-django<4.0
276-
django-v{3.2,4.0,4.1,4.2,5.0,5.1}: pytest-django
277-
django-v{4.0,4.1,4.2,5.0,5.1}: djangorestframework
278-
django-v{4.0,4.1,4.2,5.0,5.1}: pytest-asyncio
279-
django-v{4.0,4.1,4.2,5.0,5.1}: Werkzeug
280-
django-latest: djangorestframework
281-
django-latest: pytest-asyncio
282-
django-latest: pytest-django
283-
django-latest: Werkzeug
284-
django-latest: channels[daphne]
285-
286-
django-v1.11: Django~=1.11.0
287-
django-v2.0: Django~=2.0.0
288-
django-v2.2: Django~=2.2.0
289-
django-v3.0: Django~=3.0.0
290-
django-v3.2: Django~=3.2.0
291-
django-v4.0: Django~=4.0.0
292-
django-v4.1: Django~=4.1.0
293-
django-v4.2: Django~=4.2.0
294-
django-v5.0: Django~=5.0.0
295-
django-v5.1: Django==5.1rc1
296-
django-latest: Django
297-
298254
# FastAPI
299255
fastapi: httpx
300256
# (this is a dependency of httpx)

sentry_sdk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"start_transaction",
4343
"trace",
4444
"monitor",
45-
"_experimental_logger",
45+
"logger",
4646
]
4747

4848
# Initialize the debug support after everything is loaded

sentry_sdk/_log_batcher.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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
9+
10+
if TYPE_CHECKING:
11+
from sentry_sdk._types import Log
12+
13+
14+
class LogBatcher:
15+
MAX_LOGS_BEFORE_FLUSH = 100
16+
FLUSH_WAIT_TIME = 5.0
17+
18+
def __init__(
19+
self,
20+
capture_func, # type: Callable[[Envelope], None]
21+
):
22+
# type: (...) -> None
23+
self._log_buffer = [] # type: List[Log]
24+
self._capture_func = capture_func
25+
self._running = True
26+
self._lock = threading.Lock()
27+
28+
self._flush_event = threading.Event() # type: threading.Event
29+
30+
self._flusher = None # type: Optional[threading.Thread]
31+
self._flusher_pid = None # type: Optional[int]
32+
33+
def _ensure_thread(self):
34+
# type: (...) -> bool
35+
"""For forking processes we might need to restart this thread.
36+
This ensures that our process actually has that thread running.
37+
"""
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+
# Recheck to make sure another thread didn't get here and start the
47+
# the flusher in the meantime
48+
if self._flusher_pid == pid:
49+
return True
50+
51+
self._flusher_pid = pid
52+
53+
self._flusher = threading.Thread(target=self._flush_loop)
54+
self._flusher.daemon = True
55+
56+
try:
57+
self._flusher.start()
58+
except RuntimeError:
59+
# Unfortunately at this point the interpreter is in a state that no
60+
# longer allows us to spawn a thread and we have to bail.
61+
self._running = False
62+
return False
63+
64+
return True
65+
66+
def _flush_loop(self):
67+
# type: (...) -> None
68+
while self._running:
69+
self._flush_event.wait(self.FLUSH_WAIT_TIME + random.random())
70+
self._flush_event.clear()
71+
self._flush()
72+
73+
def add(
74+
self,
75+
log, # type: Log
76+
):
77+
# type: (...) -> None
78+
if not self._ensure_thread() or self._flusher is None:
79+
return None
80+
81+
with self._lock:
82+
self._log_buffer.append(log)
83+
if len(self._log_buffer) >= self.MAX_LOGS_BEFORE_FLUSH:
84+
self._flush_event.set()
85+
86+
def kill(self):
87+
# type: (...) -> None
88+
if self._flusher is None:
89+
return
90+
91+
self._running = False
92+
self._flush_event.set()
93+
self._flusher = None
94+
95+
def flush(self):
96+
# type: (...) -> None
97+
self._flush()
98+
99+
@staticmethod
100+
def _log_to_otel(log):
101+
# type: (Log) -> Any
102+
def format_attribute(key, val):
103+
# type: (str, int | float | str | bool) -> Any
104+
if isinstance(val, bool):
105+
return {"key": key, "value": {"boolValue": val}}
106+
if isinstance(val, int):
107+
return {"key": key, "value": {"intValue": str(val)}}
108+
if isinstance(val, float):
109+
return {"key": key, "value": {"doubleValue": val}}
110+
if isinstance(val, str):
111+
return {"key": key, "value": {"stringValue": val}}
112+
return {"key": key, "value": {"stringValue": safe_repr(val)}}
113+
114+
otel_log = {
115+
"severityText": log["severity_text"],
116+
"severityNumber": log["severity_number"],
117+
"body": {"stringValue": log["body"]},
118+
"timeUnixNano": str(log["time_unix_nano"]),
119+
"attributes": [
120+
format_attribute(k, v) for (k, v) in log["attributes"].items()
121+
],
122+
}
123+
124+
if "trace_id" in log:
125+
otel_log["traceId"] = log["trace_id"]
126+
127+
return otel_log
128+
129+
def _flush(self):
130+
# type: (...) -> Optional[Envelope]
131+
132+
envelope = Envelope(
133+
headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
134+
)
135+
with self._lock:
136+
for log in self._log_buffer:
137+
envelope.add_log(self._log_to_otel(log))
138+
self._log_buffer.clear()
139+
if envelope.items:
140+
self._capture_func(envelope)
141+
return envelope
142+
return None

sentry_sdk/_types.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ def __eq__(self, other):
3030

3131
return self.value == other.value and self.metadata == other.metadata
3232

33+
def __str__(self):
34+
# type: (AnnotatedValue) -> str
35+
return str({"value": str(self.value), "metadata": str(self.metadata)})
36+
37+
def __len__(self):
38+
# type: (AnnotatedValue) -> int
39+
if self.value is not None:
40+
return len(self.value)
41+
else:
42+
return 0
43+
3344
@classmethod
3445
def removed_because_raw_data(cls):
3546
# type: () -> AnnotatedValue
@@ -151,8 +162,8 @@ class SDKInfo(TypedDict):
151162
Event = TypedDict(
152163
"Event",
153164
{
154-
"breadcrumbs": dict[
155-
Literal["values"], list[dict[str, Any]]
165+
"breadcrumbs": Annotated[
166+
dict[Literal["values"], list[dict[str, Any]]]
156167
], # TODO: We can expand on this type
157168
"check_in_id": str,
158169
"contexts": dict[str, dict[str, object]],

0 commit comments

Comments
 (0)