Skip to content

Commit 23eb439

Browse files
committed
Merge branch 'master' into potel-base
2 parents 35fab6b + 18a1104 commit 23eb439

File tree

9 files changed

+142
-99
lines changed

9 files changed

+142
-99
lines changed

scripts/populate_tox/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
},
1515
"python": ">=3.7",
1616
},
17+
"anthropic": {
18+
"package": "anthropic",
19+
"deps": {
20+
"*": ["pytest-asyncio"],
21+
"<0.50": ["httpx<0.28.0"],
22+
},
23+
"python": ">=3.8",
24+
},
1725
"ariadne": {
1826
"package": "ariadne",
1927
"deps": {

scripts/populate_tox/populate_tox.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"potel",
6767
# Integrations that can be migrated -- we should eventually remove all
6868
# of these from the IGNORE list
69-
"anthropic",
7069
"arq",
7170
"asyncpg",
7271
"beam",

scripts/populate_tox/tox.jinja

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ envlist =
3333
# At a minimum, we should test against at least the lowest
3434
# and the latest supported version of a framework.
3535

36-
# Anthropic
37-
{py3.8,py3.11,py3.12}-anthropic-v{0.16,0.28,0.40}
38-
{py3.7,py3.11,py3.12}-anthropic-latest
39-
4036
# Arq
4137
{py3.7,py3.11}-arq-v{0.23}
4238
{py3.7,py3.12,py3.13}-arq-latest
@@ -160,14 +156,6 @@ deps =
160156
161157
# === Integrations ===
162158
163-
# Anthropic
164-
anthropic: pytest-asyncio
165-
anthropic-v{0.16,0.28}: httpx<0.28.0
166-
anthropic-v0.16: anthropic~=0.16.0
167-
anthropic-v0.28: anthropic~=0.28.0
168-
anthropic-v0.40: anthropic~=0.40.0
169-
anthropic-latest: anthropic
170-
171159
# Arq
172160
arq-v0.23: arq~=0.23.0
173161
arq-v0.23: pydantic<2

sentry_sdk/_log_batcher.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Optional, List, Callable, TYPE_CHECKING, Any
66

77
from sentry_sdk.utils import format_timestamp, safe_repr
8-
from sentry_sdk.envelope import Envelope
8+
from sentry_sdk.envelope import Envelope, Item, PayloadRef
99

1010
if TYPE_CHECKING:
1111
from sentry_sdk._types import Log
@@ -97,34 +97,36 @@ def flush(self):
9797
self._flush()
9898

9999
@staticmethod
100-
def _log_to_otel(log):
100+
def _log_to_transport_format(log):
101101
# type: (Log) -> Any
102-
def format_attribute(key, val):
103-
# type: (str, int | float | str | bool) -> Any
102+
def format_attribute(val):
103+
# type: (int | float | str | bool) -> Any
104104
if isinstance(val, bool):
105-
return {"key": key, "value": {"boolValue": val}}
105+
return {"value": val, "type": "boolean"}
106106
if isinstance(val, int):
107-
return {"key": key, "value": {"intValue": str(val)}}
107+
return {"value": val, "type": "integer"}
108108
if isinstance(val, float):
109-
return {"key": key, "value": {"doubleValue": val}}
109+
return {"value": val, "type": "double"}
110110
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-
],
111+
return {"value": val, "type": "string"}
112+
return {"value": safe_repr(val), "type": "string"}
113+
114+
if "sentry.severity_number" not in log["attributes"]:
115+
log["attributes"]["sentry.severity_number"] = log["severity_number"]
116+
if "sentry.severity_text" not in log["attributes"]:
117+
log["attributes"]["sentry.severity_text"] = log["severity_text"]
118+
119+
res = {
120+
"timestamp": int(log["time_unix_nano"]) / 1.0e9,
121+
"trace_id": log.get("trace_id", "00000000-0000-0000-0000-000000000000"),
122+
"level": str(log["severity_text"]),
123+
"body": str(log["body"]),
124+
"attributes": {
125+
k: format_attribute(v) for (k, v) in log["attributes"].items()
126+
},
122127
}
123128

124-
if "trace_id" in log:
125-
otel_log["traceId"] = log["trace_id"]
126-
127-
return otel_log
129+
return res
128130

129131
def _flush(self):
130132
# type: (...) -> Optional[Envelope]
@@ -133,10 +135,27 @@ def _flush(self):
133135
headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
134136
)
135137
with self._lock:
136-
for log in self._log_buffer:
137-
envelope.add_log(self._log_to_otel(log))
138+
if len(self._log_buffer) == 0:
139+
return None
140+
141+
envelope.add_item(
142+
Item(
143+
type="log",
144+
content_type="application/vnd.sentry.items.log+json",
145+
headers={
146+
"item_count": len(self._log_buffer),
147+
},
148+
payload=PayloadRef(
149+
json={
150+
"items": [
151+
self._log_to_transport_format(log)
152+
for log in self._log_buffer
153+
]
154+
}
155+
),
156+
)
157+
)
138158
self._log_buffer.clear()
139-
if envelope.items:
140-
self._capture_func(envelope)
141-
return envelope
142-
return None
159+
160+
self._capture_func(envelope)
161+
return envelope

sentry_sdk/envelope.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,6 @@ def add_sessions(
106106
# type: (...) -> None
107107
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))
108108

109-
def add_log(
110-
self, log # type: Any
111-
):
112-
# type: (...) -> None
113-
self.add_item(Item(payload=PayloadRef(json=log), type="otel_log"))
114-
115109
def add_item(
116110
self, item # type: Item
117111
):
@@ -278,7 +272,7 @@ def data_category(self):
278272
return "transaction"
279273
elif ty == "event":
280274
return "error"
281-
elif ty == "otel_log":
275+
elif ty == "log":
282276
return "log"
283277
elif ty == "client_report":
284278
return "internal"

sentry_sdk/integrations/logging.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ def _capture_log_from_record(client, record):
360360
# type: (BaseClient, LogRecord) -> None
361361
scope = sentry_sdk.get_current_scope()
362362
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
363+
project_root = client.options["project_root"]
363364
attrs = {
364365
"sentry.origin": "auto.logger.log",
365366
} # type: dict[str, str | bool | float | int]
@@ -379,7 +380,10 @@ def _capture_log_from_record(client, record):
379380
if record.lineno:
380381
attrs["code.line.number"] = record.lineno
381382
if record.pathname:
382-
attrs["code.file.path"] = record.pathname
383+
if project_root is not None and record.pathname.startswith(project_root):
384+
attrs["code.file.path"] = record.pathname[len(project_root) + 1 :]
385+
else:
386+
attrs["code.file.path"] = record.pathname
383387
if record.funcName:
384388
attrs["code.function.name"] = record.funcName
385389

tests/test_basics.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -967,14 +967,12 @@ def recurse():
967967
(event,) = events
968968

969969
assert event["exception"]["values"][0]["stacktrace"] is None
970-
assert event["_meta"] == {
971-
"exception": {
972-
"values": {"0": {"stacktrace": {"": {"rem": [["!config", "x"]]}}}}
973-
}
970+
assert event["_meta"]["exception"] == {
971+
"values": {"0": {"stacktrace": {"": {"rem": [["!config", "x"]]}}}}
974972
}
975973

976974
# On my machine, it takes about 100-200ms to capture the exception,
977975
# so this limit should be generous enough.
978976
assert (
979-
capture_end_time - capture_start_time < 10**9
977+
capture_end_time - capture_start_time < 10**9 * 2
980978
), "stacktrace capture took too long, check that frame limit is set correctly"

tests/test_logs.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,44 @@
1919

2020

2121
def otel_attributes_to_dict(otel_attrs):
22-
# type: (List[Mapping[str, Any]]) -> Mapping[str, Any]
22+
# type: (Mapping[str, Any]) -> Mapping[str, Any]
2323
def _convert_attr(attr):
2424
# type: (Mapping[str, Union[str, float, bool]]) -> Any
25-
if "boolValue" in attr:
26-
return bool(attr["boolValue"])
27-
if "doubleValue" in attr:
28-
return float(attr["doubleValue"])
29-
if "intValue" in attr:
30-
return int(attr["intValue"])
31-
if attr["stringValue"].startswith("{"):
25+
if attr["type"] == "boolean":
26+
return attr["value"]
27+
if attr["type"] == "double":
28+
return attr["value"]
29+
if attr["type"] == "integer":
30+
return attr["value"]
31+
if attr["value"].startswith("{"):
3232
try:
3333
return json.loads(attr["stringValue"])
3434
except ValueError:
3535
pass
36-
return str(attr["stringValue"])
36+
return str(attr["value"])
3737

38-
return {item["key"]: _convert_attr(item["value"]) for item in otel_attrs}
38+
return {k: _convert_attr(v) for (k, v) in otel_attrs.items()}
3939

4040

4141
def envelopes_to_logs(envelopes: List[Envelope]) -> List[Log]:
4242
res = [] # type: List[Log]
4343
for envelope in envelopes:
4444
for item in envelope.items:
45-
if item.type == "otel_log":
46-
log_json = item.payload.json
47-
log = {
48-
"severity_text": log_json["severityText"],
49-
"severity_number": log_json["severityNumber"],
50-
"body": log_json["body"]["stringValue"],
51-
"attributes": otel_attributes_to_dict(log_json["attributes"]),
52-
"time_unix_nano": int(log_json["timeUnixNano"]),
53-
"trace_id": None,
54-
} # type: Log
55-
if "traceId" in log_json:
56-
log["trace_id"] = log_json["traceId"]
57-
res.append(log)
45+
if item.type == "log":
46+
for log_json in item.payload.json["items"]:
47+
log = {
48+
"severity_text": log_json["attributes"]["sentry.severity_text"][
49+
"value"
50+
],
51+
"severity_number": int(
52+
log_json["attributes"]["sentry.severity_number"]["value"]
53+
),
54+
"body": log_json["body"],
55+
"attributes": otel_attributes_to_dict(log_json["attributes"]),
56+
"time_unix_nano": int(float(log_json["timestamp"]) * 1e9),
57+
"trace_id": log_json["trace_id"],
58+
} # type: Log
59+
res.append(log)
5860
return res
5961

6062

@@ -347,7 +349,6 @@ def test_logging_errors(sentry_init, capture_envelopes):
347349
error_event_2 = envelopes[1].items[0].payload.json
348350
assert error_event_2["level"] == "error"
349351

350-
print(envelopes)
351352
logs = envelopes_to_logs(envelopes)
352353
assert logs[0]["severity_text"] == "error"
353354
assert "sentry.message.template" not in logs[0]["attributes"]
@@ -365,6 +366,36 @@ def test_logging_errors(sentry_init, capture_envelopes):
365366
assert len(logs) == 2
366367

367368

369+
def test_log_strips_project_root(sentry_init, capture_envelopes):
370+
"""
371+
The python logger should strip project roots from the log record path
372+
"""
373+
sentry_init(
374+
_experiments={"enable_logs": True},
375+
project_root="/custom/test",
376+
)
377+
envelopes = capture_envelopes()
378+
379+
python_logger = logging.Logger("test-logger")
380+
python_logger.handle(
381+
logging.LogRecord(
382+
name="test-logger",
383+
level=logging.WARN,
384+
pathname="/custom/test/blah/path.py",
385+
lineno=123,
386+
msg="This is a test log with a custom pathname",
387+
args=(),
388+
exc_info=None,
389+
)
390+
)
391+
get_client().flush()
392+
393+
logs = envelopes_to_logs(envelopes)
394+
assert len(logs) == 1
395+
attrs = logs[0]["attributes"]
396+
assert attrs["code.file.path"] == "blah/path.py"
397+
398+
368399
def test_auto_flush_logs_after_100(sentry_init, capture_envelopes):
369400
"""
370401
If you log >100 logs, it should automatically trigger a flush.

0 commit comments

Comments
 (0)