Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: getsentry/sentry-python
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master@{1day}
Choose a base ref
...
head repository: getsentry/sentry-python
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 3 commits
  • 5 files changed
  • 2 contributors

Commits on Apr 29, 2025

  1. chore(ourlogs): Use new transport (#4317)

    We've added a more efficient transport for logs handling, use that.
    
    Solves LOGS-60
    colin-sentry authored Apr 29, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7f01372 View commit details
  2. feat(ourlogs): canonicalize paths from the logger integration (#4336)

    We'd like to allow linking to the 'source code' line in the logs page -
    this canonicalizes the path relative to the project root (if one is
    defined)
    
    ![Screenshot 2025-04-28 at 12 03
    45 PM](https://github.com/user-attachments/assets/89dde691-d9c3-45b2-b289-c42996496bf3)
    
    Solves LOGS-58
    colin-sentry authored Apr 29, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2f54dbd View commit details

Commits on Apr 30, 2025

  1. tests: bump test timeout for recursion stacktrace extract to 2s (#4351)

    In some loaded environments, the test may take slightly longer than 1s
    to extract the stacktrace. This was noticed in nixpkgs build system
    where the load is generally high due to high build parallelism and
    resource constraints. I was sometimes getting failures because the time
    it took was e.g. ~1.2s (less than current timeout of 1s).
    
    Disclosure: we'll probably end up disabling the test in nixpkgs anyway
    because we try to avoid time sensitive tests. Regardless, this bump may
    help someone else in a similar situation or environment.
    
    Signed-off-by: Ihar Hrachyshka <[email protected]>
    booxter authored Apr 30, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    18a1104 View commit details
Showing with 109 additions and 61 deletions.
  1. +47 −28 sentry_sdk/_log_batcher.py
  2. +1 −7 sentry_sdk/envelope.py
  3. +5 −1 sentry_sdk/integrations/logging.py
  4. +1 −1 tests/test_basics.py
  5. +55 −24 tests/test_logs.py
75 changes: 47 additions & 28 deletions sentry_sdk/_log_batcher.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
from typing import Optional, List, Callable, TYPE_CHECKING, Any

from sentry_sdk.utils import format_timestamp, safe_repr
from sentry_sdk.envelope import Envelope
from sentry_sdk.envelope import Envelope, Item, PayloadRef

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

@staticmethod
def _log_to_otel(log):
def _log_to_transport_format(log):
# type: (Log) -> Any
def format_attribute(key, val):
# type: (str, int | float | str | bool) -> Any
def format_attribute(val):
# type: (int | float | str | bool) -> Any
if isinstance(val, bool):
return {"key": key, "value": {"boolValue": val}}
return {"value": val, "type": "boolean"}
if isinstance(val, int):
return {"key": key, "value": {"intValue": str(val)}}
return {"value": val, "type": "integer"}
if isinstance(val, float):
return {"key": key, "value": {"doubleValue": val}}
return {"value": val, "type": "double"}
if isinstance(val, str):
return {"key": key, "value": {"stringValue": val}}
return {"key": key, "value": {"stringValue": safe_repr(val)}}

otel_log = {
"severityText": log["severity_text"],
"severityNumber": log["severity_number"],
"body": {"stringValue": log["body"]},
"timeUnixNano": str(log["time_unix_nano"]),
"attributes": [
format_attribute(k, v) for (k, v) in log["attributes"].items()
],
return {"value": val, "type": "string"}
return {"value": safe_repr(val), "type": "string"}

if "sentry.severity_number" not in log["attributes"]:
log["attributes"]["sentry.severity_number"] = log["severity_number"]
if "sentry.severity_text" not in log["attributes"]:
log["attributes"]["sentry.severity_text"] = log["severity_text"]

res = {
"timestamp": int(log["time_unix_nano"]) / 1.0e9,
"trace_id": log.get("trace_id", "00000000-0000-0000-0000-000000000000"),
"level": str(log["severity_text"]),
"body": str(log["body"]),
"attributes": {
k: format_attribute(v) for (k, v) in log["attributes"].items()
},
}

if "trace_id" in log:
otel_log["traceId"] = log["trace_id"]

return otel_log
return res

def _flush(self):
# type: (...) -> Optional[Envelope]
@@ -133,10 +135,27 @@ def _flush(self):
headers={"sent_at": format_timestamp(datetime.now(timezone.utc))}
)
with self._lock:
for log in self._log_buffer:
envelope.add_log(self._log_to_otel(log))
if len(self._log_buffer) == 0:
return None

envelope.add_item(
Item(
type="log",
content_type="application/vnd.sentry.items.log+json",
headers={
"item_count": len(self._log_buffer),
},
payload=PayloadRef(
json={
"items": [
self._log_to_transport_format(log)
for log in self._log_buffer
]
}
),
)
)
self._log_buffer.clear()
if envelope.items:
self._capture_func(envelope)
return envelope
return None

self._capture_func(envelope)
return envelope
8 changes: 1 addition & 7 deletions sentry_sdk/envelope.py
Original file line number Diff line number Diff line change
@@ -106,12 +106,6 @@ def add_sessions(
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))

def add_log(
self, log # type: Any
):
# type: (...) -> None
self.add_item(Item(payload=PayloadRef(json=log), type="otel_log"))

def add_item(
self, item # type: Item
):
@@ -278,7 +272,7 @@ def data_category(self):
return "transaction"
elif ty == "event":
return "error"
elif ty == "otel_log":
elif ty == "log":
return "log"
elif ty == "client_report":
return "internal"
6 changes: 5 additions & 1 deletion sentry_sdk/integrations/logging.py
Original file line number Diff line number Diff line change
@@ -355,6 +355,7 @@ def _capture_log_from_record(client, record):
# type: (BaseClient, LogRecord) -> None
scope = sentry_sdk.get_current_scope()
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
project_root = client.options["project_root"]
attrs = {
"sentry.origin": "auto.logger.log",
} # type: dict[str, str | bool | float | int]
@@ -374,7 +375,10 @@ def _capture_log_from_record(client, record):
if record.lineno:
attrs["code.line.number"] = record.lineno
if record.pathname:
attrs["code.file.path"] = record.pathname
if project_root is not None and record.pathname.startswith(project_root):
attrs["code.file.path"] = record.pathname[len(project_root) + 1 :]
else:
attrs["code.file.path"] = record.pathname
if record.funcName:
attrs["code.function.name"] = record.funcName

2 changes: 1 addition & 1 deletion tests/test_basics.py
Original file line number Diff line number Diff line change
@@ -1158,5 +1158,5 @@ def recurse():
# On my machine, it takes about 100-200ms to capture the exception,
# so this limit should be generous enough.
assert (
capture_end_time - capture_start_time < 10**9
capture_end_time - capture_start_time < 10**9 * 2
), "stacktrace capture took too long, check that frame limit is set correctly"
79 changes: 55 additions & 24 deletions tests/test_logs.py
Original file line number Diff line number Diff line change
@@ -19,42 +19,44 @@


def otel_attributes_to_dict(otel_attrs):
# type: (List[Mapping[str, Any]]) -> Mapping[str, Any]
# type: (Mapping[str, Any]) -> Mapping[str, Any]
def _convert_attr(attr):
# type: (Mapping[str, Union[str, float, bool]]) -> Any
if "boolValue" in attr:
return bool(attr["boolValue"])
if "doubleValue" in attr:
return float(attr["doubleValue"])
if "intValue" in attr:
return int(attr["intValue"])
if attr["stringValue"].startswith("{"):
if attr["type"] == "boolean":
return attr["value"]
if attr["type"] == "double":
return attr["value"]
if attr["type"] == "integer":
return attr["value"]
if attr["value"].startswith("{"):
try:
return json.loads(attr["stringValue"])
except ValueError:
pass
return str(attr["stringValue"])
return str(attr["value"])

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


def envelopes_to_logs(envelopes: List[Envelope]) -> List[Log]:
res = [] # type: List[Log]
for envelope in envelopes:
for item in envelope.items:
if item.type == "otel_log":
log_json = item.payload.json
log = {
"severity_text": log_json["severityText"],
"severity_number": log_json["severityNumber"],
"body": log_json["body"]["stringValue"],
"attributes": otel_attributes_to_dict(log_json["attributes"]),
"time_unix_nano": int(log_json["timeUnixNano"]),
"trace_id": None,
} # type: Log
if "traceId" in log_json:
log["trace_id"] = log_json["traceId"]
res.append(log)
if item.type == "log":
for log_json in item.payload.json["items"]:
log = {
"severity_text": log_json["attributes"]["sentry.severity_text"][
"value"
],
"severity_number": int(
log_json["attributes"]["sentry.severity_number"]["value"]
),
"body": log_json["body"],
"attributes": otel_attributes_to_dict(log_json["attributes"]),
"time_unix_nano": int(float(log_json["timestamp"]) * 1e9),
"trace_id": log_json["trace_id"],
} # type: Log
res.append(log)
return res


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

print(envelopes)
logs = envelopes_to_logs(envelopes)
assert logs[0]["severity_text"] == "error"
assert "sentry.message.template" not in logs[0]["attributes"]
@@ -362,6 +363,36 @@ def test_logging_errors(sentry_init, capture_envelopes):
assert len(logs) == 2


def test_log_strips_project_root(sentry_init, capture_envelopes):
"""
The python logger should strip project roots from the log record path
"""
sentry_init(
_experiments={"enable_logs": True},
project_root="/custom/test",
)
envelopes = capture_envelopes()

python_logger = logging.Logger("test-logger")
python_logger.handle(
logging.LogRecord(
name="test-logger",
level=logging.WARN,
pathname="/custom/test/blah/path.py",
lineno=123,
msg="This is a test log with a custom pathname",
args=(),
exc_info=None,
)
)
get_client().flush()

logs = envelopes_to_logs(envelopes)
assert len(logs) == 1
attrs = logs[0]["attributes"]
assert attrs["code.file.path"] == "blah/path.py"


def test_auto_flush_logs_after_100(sentry_init, capture_envelopes):
"""
If you log >100 logs, it should automatically trigger a flush.