From 4cee7d106a391314942aab242a111cf01d983605 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 8 May 2025 14:05:58 -0400 Subject: [PATCH 1/3] feat(logs): Forward extra from logger as attributes --- sentry_sdk/integrations/logging.py | 12 ++++++------ tests/test_logs.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 46628bb04b..8c6ed02213 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -348,17 +348,17 @@ def emit(self, record): if not client.options["_experiments"].get("enable_logs", False): return - SentryLogsHandler._capture_log_from_record(client, record) + self._capture_log_from_record(client, record) - @staticmethod - def _capture_log_from_record(client, record): + def _capture_log_from_record(self, 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] + attrs = self._extra_from_record( + record + ) # type: dict[str, str | bool | float | int] + attrs["sentry.origin"] = "auto.logger.log" if isinstance(record.msg, str): attrs["sentry.message.template"] = record.msg if record.args is not None: diff --git a/tests/test_logs.py b/tests/test_logs.py index 49ffd31ec7..eabb7df8c2 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -393,6 +393,27 @@ def test_log_strips_project_root(sentry_init, capture_envelopes): assert attrs["code.file.path"] == "blah/path.py" +def test_extra_data(sentry_init, capture_envelopes): + """ + The python logger should be able to log extra data + """ + sentry_init(_experiments={"enable_logs": True}) + envelopes = capture_envelopes() + + python_logger = logging.Logger("test-logger") + python_logger.warning( + "log #%d", + 1, + extra={"foo": "bar", "numeric": 42, "more_complex": {"nested": "data"}}, + ) + get_client().flush() + + logs = envelopes_to_logs(envelopes) + assert logs[0]["attributes"]["foo"] == "bar" + assert logs[0]["attributes"]["numeric"] == 42 + assert logs[0]["attributes"]["more_complex"] == '{"nested": "data"}' + + def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): """ If you log >100 logs, it should automatically trigger a flush. From 1b73d4ca43c76b82897e1ae68c1c2873e95b9414 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 8 May 2025 19:39:58 -0400 Subject: [PATCH 2/3] explicitly assert on every attribute --- tests/test_logs.py | 63 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index eabb7df8c2..1f6b07e762 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -30,7 +30,7 @@ def _convert_attr(attr): return attr["value"] if attr["value"].startswith("{"): try: - return json.loads(attr["stringValue"]) + return json.loads(attr["value"]) except ValueError: pass return str(attr["value"]) @@ -393,9 +393,9 @@ def test_log_strips_project_root(sentry_init, capture_envelopes): assert attrs["code.file.path"] == "blah/path.py" -def test_extra_data(sentry_init, capture_envelopes): +def test_logger_with_all_attributes(sentry_init, capture_envelopes): """ - The python logger should be able to log extra data + The python logger should be able to log all attributes, including extra data. """ sentry_init(_experiments={"enable_logs": True}) envelopes = capture_envelopes() @@ -409,9 +409,60 @@ def test_extra_data(sentry_init, capture_envelopes): get_client().flush() logs = envelopes_to_logs(envelopes) - assert logs[0]["attributes"]["foo"] == "bar" - assert logs[0]["attributes"]["numeric"] == 42 - assert logs[0]["attributes"]["more_complex"] == '{"nested": "data"}' + + attributes = logs[0]["attributes"] + + assert "process.pid" in attributes + assert isinstance(attributes["process.pid"], int) + del attributes["process.pid"] + + assert "sentry.release" in attributes + assert isinstance(attributes["sentry.release"], str) + del attributes["sentry.release"] + + assert "server.address" in attributes + assert isinstance(attributes["server.address"], str) + del attributes["server.address"] + + assert "thread.id" in attributes + assert isinstance(attributes["thread.id"], int) + del attributes["thread.id"] + + assert "code.file.path" in attributes + assert isinstance(attributes["code.file.path"], str) + del attributes["code.file.path"] + + assert "code.function.name" in attributes + assert isinstance(attributes["code.function.name"], str) + del attributes["code.function.name"] + + assert "code.line.number" in attributes + assert isinstance(attributes["code.line.number"], int) + del attributes["code.line.number"] + + assert "process.executable.name" in attributes + assert isinstance(attributes["process.executable.name"], str) + del attributes["process.executable.name"] + + assert "thread.name" in attributes + assert isinstance(attributes["thread.name"], str) + del attributes["thread.name"] + + # Assert on the remaining non-dynamic attributes. + assert attributes == { + "foo": "bar", + "numeric": 42, + "more_complex": "{'nested': 'data'}", + "logger.name": "test-logger", + "sentry.origin": "auto.logger.log", + "sentry.message.template": "log #%d", + "sentry.message.parameters.0": 1, + "sentry.environment": "production", + "sentry.sdk.name": "sentry.python", + "sentry.sdk.version": VERSION, + "sentry.severity_number": 13, + "sentry.severity_text": "warn", + } def test_auto_flush_logs_after_100(sentry_init, capture_envelopes): From 6c08d7718c47e058ae10e77f78bc2d80ce73d071 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 8 May 2025 20:47:54 -0400 Subject: [PATCH 3/3] cast to any to make typechecking happy --- sentry_sdk/integrations/logging.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 8c6ed02213..74baf3d33a 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -355,9 +355,7 @@ def _capture_log_from_record(self, client, record): 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 = self._extra_from_record( - record - ) # type: dict[str, str | bool | float | int] + attrs = self._extra_from_record(record) # type: Any attrs["sentry.origin"] = "auto.logger.log" if isinstance(record.msg, str): attrs["sentry.message.template"] = record.msg