Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def _scrub_structlog_extra(extra: dict[str, typing.Any]) -> dict[str, typing.Any


def enrich_sentry_event_from_structlog_log(
event: "sentry_types.Event", _: "sentry_types.Hint"
event: "sentry_types.Event", hint: "sentry_types.Hint"
) -> typing.Optional["sentry_types.Event"]:
logentry = event.get("logentry") or {}
if not (
Expand All @@ -181,7 +181,11 @@ def enrich_sentry_event_from_structlog_log(
):
return event

payload = StructuredLogPayload.parse(formatted_message)
# Sentry truncates logentry before before_send; the original record still has complete JSON.
record = hint.get("log_record")
payload = StructuredLogPayload.parse(record.getMessage()) if isinstance(record, logging.LogRecord) else None
if payload is None:
payload = StructuredLogPayload.parse(formatted_message)
if payload is None:
return event
if payload.skip_sentry:
Expand Down
94 changes: 90 additions & 4 deletions tests/instruments/test_sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,12 +559,16 @@ def before_send(event: "sentry_types.Event", _: "sentry_types.Hint") -> "sentry_
sentry.teardown()


@pytest.mark.parametrize("attach_stacktrace", [False, True])
def test_structlog_stored_exception_reaches_sentry_and_stdout(
minimal_sentry_config: SentryConfig,
sentry_mock: SentryTestTransport,
capsys: pytest.CaptureFixture[str],
*,
attach_stacktrace: bool,
) -> None:
sentry = SentryInstrument(bootstrap_config=minimal_sentry_config)
config = dataclasses.replace(minimal_sentry_config, sentry_attach_stacktrace=attach_stacktrace)
sentry = SentryInstrument(bootstrap_config=config)
logs = LoggingInstrument(bootstrap_config=LoggingConfig())
sentry.bootstrap()
logs.bootstrap()
Expand All @@ -574,20 +578,102 @@ def test_structlog_stored_exception_reaches_sentry_and_stdout(
except ValueError as exc:
stored = exc

logger.error("stored exception", exc_info=stored, component="worker")
marker = uuid.uuid4().hex
padding = "x" * config.sentry_max_value_length
logger.error("stored exception", exc_info=stored, component="worker", password=marker, padding=padding)

line = json.loads(capsys.readouterr().out.splitlines()[-1])
assert line["password"] == marker
assert line["padding"] == padding
assert "Traceback (most recent call last)" in line["exception"]
assert "ValueError: invalid literal for int() with base 10: 'original stored error'" in line["exception"]
event = sentry_mock.mock_envelopes[0].get_event()
envelope = sentry_mock.mock_envelopes[0]
assert marker.encode() not in envelope.serialize()
event = envelope.get_event()
assert event is not None
(exception,) = event["exception"]["values"]
assert exception["type"] == "ValueError"
assert exception["value"] == "invalid literal for int() with base 10: 'original stored error'"
assert exception["stacktrace"]["frames"][-1]["function"] == (
"test_structlog_stored_exception_reaches_sentry_and_stdout"
)
assert event["contexts"]["structlog"] == {"component": "worker"}
assert event["contexts"]["structlog"]["component"] == "worker"
assert event["contexts"]["structlog"]["password"] == "[Filtered]" # noqa: S105
finally:
logs.teardown()
sentry.teardown()


@pytest.mark.parametrize("attach_stacktrace", [False, True])
@pytest.mark.parametrize("oversized", [False, True])
def test_structlog_envelope_privacy_across_string_limit(
minimal_sentry_config: SentryConfig,
sentry_mock: SentryTestTransport,
capsys: pytest.CaptureFixture[str],
*,
attach_stacktrace: bool,
oversized: bool,
) -> None:
limit = 1024
marker = uuid.uuid4().hex
padding = "x" * (limit + 1 if oversized else limit - 256)
callback_events: list[sentry_types.Event] = []

def before_send(event: "sentry_types.Event", _: "sentry_types.Hint") -> "sentry_types.Event":
callback_events.append(copy.deepcopy(event))
return event

config = dataclasses.replace(
minimal_sentry_config,
sentry_max_value_length=limit,
sentry_attach_stacktrace=attach_stacktrace,
sentry_before_send=before_send,
)
sentry = SentryInstrument(bootstrap_config=config)
logs = LoggingInstrument(bootstrap_config=LoggingConfig())
sentry.bootstrap()
logs.bootstrap()
try:
logger.error("failure", password=marker, padding=padding)
logger.error("suppressed", password=marker, padding=padding, skip_sentry=True)

lines = capsys.readouterr().out.splitlines()
expected_lines = 2
assert len(lines) == expected_lines
for line in lines:
assert (len(line) > limit) == oversized
rendered = json.loads(line)
assert rendered["password"] == marker
assert rendered["padding"] == padding
assert json.loads(lines[1])["skip_sentry"] is True
(envelope,) = sentry_mock.mock_envelopes
assert marker.encode() not in envelope.serialize()
(event,) = callback_events
assert event["logentry"] == {"message": "failure", "formatted": "failure", "params": []}
assert event["contexts"]["structlog"]["password"] == "[Filtered]" # noqa: S105
assert ("threads" in event) == attach_stacktrace
finally:
logs.teardown()
sentry.teardown()


@pytest.mark.parametrize("template", ["plain %s", "{invalid %s", '{"unrelated":"%s"}'])
def test_unstructured_log_keeps_sdk_truncation(
minimal_sentry_config: SentryConfig, sentry_mock: SentryTestTransport, template: str
) -> None:
limit = 256
config = dataclasses.replace(minimal_sentry_config, sentry_max_value_length=limit)
sentry = SentryInstrument(bootstrap_config=config)
sentry.bootstrap()
try:
std_logger.error(template, "x" * (limit * 2))
(envelope,) = sentry_mock.mock_envelopes
event = envelope.get_event()
assert event is not None
formatted = event["logentry"]["formatted"]
assert isinstance(formatted, str)
assert len(formatted) <= limit
assert event["logentry"]["message"] == template
assert "structlog" not in event["contexts"]
finally:
sentry.teardown()
Loading