Skip to content

Commit a7e4796

Browse files
committed
fix(logger): no longer replaces object duplicates with "CIRCULAR"
1 parent e1110eb commit a7e4796

File tree

2 files changed

+107
-5
lines changed

2 files changed

+107
-5
lines changed

src/firebase_functions/logger.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,29 @@ def _remove_circular(obj: _typing.Any,
7575
if refs is None:
7676
refs = set()
7777

78+
# Check if the object is already in the current recursion stack
7879
if id(obj) in refs:
7980
return "[CIRCULAR]"
8081

82+
# For non-primitive objects, add the current object's id to the recursion stack
8183
if not isinstance(obj, (str, int, float, bool, type(None))):
8284
refs.add(id(obj))
8385

86+
# Recursively process the object based on its type
8487
if isinstance(obj, dict):
85-
return {key: _remove_circular(value, refs) for key, value in obj.items()}
88+
result = {key: _remove_circular(value, refs) for key, value in obj.items()}
8689
elif isinstance(obj, list):
87-
return [_remove_circular(value, refs) for _, value in enumerate(obj)]
90+
result = [_remove_circular(item, refs) for item in obj]
8891
elif isinstance(obj, tuple):
89-
return tuple(
90-
_remove_circular(value, refs) for _, value in enumerate(obj))
92+
result = tuple(_remove_circular(item, refs) for item in obj)
9193
else:
92-
return obj
94+
result = obj
95+
96+
# Remove the object's id from the recursion stack after processing
97+
if not isinstance(obj, (str, int, float, bool, type(None))):
98+
refs.remove(id(obj))
99+
100+
return result
93101

94102

95103
def _get_write_file(severity: LogSeverity) -> _typing.TextIO:

tests/test_logger.py

+94
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,97 @@ def test_message_should_be_space_separated(
7979
raw_log_output = capsys.readouterr().out
8080
log_output = json.loads(raw_log_output)
8181
assert log_output["message"] == expected_message
82+
83+
def test_remove_circular_references(self, capsys: pytest.CaptureFixture[str]):
84+
# Create an object with a circular reference.
85+
circ = {"b": "foo"}
86+
circ["circ"] = circ
87+
88+
entry = {
89+
"severity": "ERROR",
90+
"message": "testing circular",
91+
"circ": circ,
92+
}
93+
logger.write(entry)
94+
raw_log_output = capsys.readouterr().err
95+
log_output = json.loads(raw_log_output)
96+
97+
expected = {
98+
"severity": "ERROR",
99+
"message": "testing circular",
100+
"circ": {"b": "foo", "circ": "[CIRCULAR]"},
101+
}
102+
assert log_output == expected
103+
104+
def test_remove_circular_references_in_arrays(self, capsys: pytest.CaptureFixture[str]):
105+
# Create an object with a circular reference inside an array.
106+
circ = {"b": "foo"}
107+
circ["circ"] = [circ]
108+
109+
entry = {
110+
"severity": "ERROR",
111+
"message": "testing circular",
112+
"circ": circ,
113+
}
114+
logger.write(entry)
115+
raw_log_output = capsys.readouterr().err
116+
log_output = json.loads(raw_log_output)
117+
118+
expected = {
119+
"severity": "ERROR",
120+
"message": "testing circular",
121+
"circ": {"b": "foo", "circ": ["[CIRCULAR]"]},
122+
}
123+
assert log_output == expected
124+
125+
def test_no_false_circular_for_duplicates(self, capsys: pytest.CaptureFixture[str]):
126+
# Ensure that duplicate objects (used in multiple keys) are not marked as circular.
127+
obj = {"a": "foo"}
128+
entry = {
129+
"severity": "ERROR",
130+
"message": "testing circular",
131+
"a": obj,
132+
"b": obj,
133+
}
134+
logger.write(entry)
135+
raw_log_output = capsys.readouterr().err
136+
log_output = json.loads(raw_log_output)
137+
138+
expected = {
139+
"severity": "ERROR",
140+
"message": "testing circular",
141+
"a": {"a": "foo"},
142+
"b": {"a": "foo"},
143+
}
144+
assert log_output == expected
145+
146+
def test_no_false_circular_in_array_duplicates(self, capsys: pytest.CaptureFixture[str]):
147+
# Ensure that duplicate objects in arrays are not falsely detected as circular.
148+
obj = {"a": "foo"}
149+
arr = [
150+
{"a": obj, "b": obj},
151+
{"a": obj, "b": obj},
152+
]
153+
entry = {
154+
"severity": "ERROR",
155+
"message": "testing circular",
156+
"a": arr,
157+
"b": arr,
158+
}
159+
logger.write(entry)
160+
raw_log_output = capsys.readouterr().err
161+
log_output = json.loads(raw_log_output)
162+
163+
expected = {
164+
"severity": "ERROR",
165+
"message": "testing circular",
166+
"a": [
167+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
168+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
169+
],
170+
"b": [
171+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
172+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
173+
],
174+
}
175+
assert log_output == expected

0 commit comments

Comments
 (0)