Skip to content

Commit 7fdd054

Browse files
author
Izaak Gough
committed
fix: serialize sys.exc_info exception types in structured logs
1 parent e1b19e9 commit 7fdd054

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

src/firebase_functions/logger.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ def _exception_from_args(
9797
return details
9898

9999

100+
def _exception_type_from_args(
101+
exception_type: type[BaseException],
102+
) -> dict[str, _typing.Any]:
103+
"""
104+
Creates a JSON-safe representation of an exception class.
105+
106+
If the class matches the active exception from `sys.exc_info()`, include
107+
the current exception message and stack trace as well.
108+
"""
109+
110+
details: dict[str, _typing.Any] = {
111+
"type": exception_type.__name__,
112+
"message": exception_type.__name__,
113+
}
114+
exc_type, exc_value, exc_traceback = _sys.exc_info()
115+
if exc_type is exception_type and exc_value is not None:
116+
details["message"] = _safe_exception_string(exc_value)
117+
if exc_traceback is not None:
118+
details["stack_trace"] = "".join(
119+
_traceback.format_exception(exc_type, exc_value, exc_traceback)
120+
)
121+
return details
122+
123+
100124
def _safe_exception_string(exception: BaseException) -> str:
101125
"""
102126
Returns a string representation of an exception without propagating repr/str errors.
@@ -128,6 +152,8 @@ def _remove_circular(obj: _typing.Any, refs: set[int] | None = None):
128152
result: _typing.Any
129153
if isinstance(obj, BaseException):
130154
result = _exception_from_args(obj, refs)
155+
elif isinstance(obj, type) and issubclass(obj, BaseException):
156+
result = _exception_type_from_args(obj)
131157
elif isinstance(obj, dict):
132158
result = {key: _remove_circular(value, refs) for key, value in obj.items()}
133159
elif isinstance(obj, list):

tests/test_logger.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import json
7+
import sys
78

89
import pytest
910

@@ -75,6 +76,22 @@ def test_error_should_accept_exception(self, capsys: pytest.CaptureFixture[str])
7576
assert "stack_trace" in log_output["error"]
7677
assert "ValueError: boom" in log_output["error"]["stack_trace"]
7778

79+
def test_error_should_accept_exception_type(self, capsys: pytest.CaptureFixture[str]):
80+
try:
81+
raise TypeError("boom")
82+
except TypeError:
83+
logger.error("failed", error=sys.exc_info()[0])
84+
85+
raw_log_output = capsys.readouterr().err
86+
log_output = json.loads(raw_log_output)
87+
88+
assert log_output["severity"] == "ERROR"
89+
assert log_output["message"] == "failed"
90+
assert log_output["error"]["type"] == "TypeError"
91+
assert log_output["error"]["message"] == "boom"
92+
assert "stack_trace" in log_output["error"]
93+
assert "TypeError: boom" in log_output["error"]["stack_trace"]
94+
7895
def test_error_should_accept_self_referential_exception(
7996
self, capsys: pytest.CaptureFixture[str]
8097
):

0 commit comments

Comments
 (0)