Skip to content

Commit c744ded

Browse files
committed
Initial implementation of logger
1 parent f7d9864 commit c744ded

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

src/firebase_functions/logger.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import enum as _enum
2+
import json as _json
3+
import sys as _sys
4+
import typing as _typing
5+
import typing_extensions as _typing_extensions
6+
7+
8+
class LogSeverity(str, _enum.Enum):
9+
"""
10+
`LogSeverity` indicates the detailed severity of the log entry. See [LogSeverity](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity).
11+
"""
12+
13+
DEBUG = "DEBUG"
14+
INFO = "INFO"
15+
NOTICE = "NOTICE"
16+
WARNING = "WARNING"
17+
ERROR = "ERROR"
18+
CRITICAL = "CRITICAL"
19+
ALERT = "ALERT"
20+
EMERGENCY = "EMERGENCY"
21+
22+
23+
class LogEntry(_typing.TypedDict):
24+
"""
25+
`LogEntry` represents a log entry.
26+
See [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry).
27+
"""
28+
29+
severity: _typing_extensions.Required[LogSeverity]
30+
message: _typing_extensions.NotRequired[str]
31+
32+
33+
def _entry_from_args(severity: LogSeverity, **kwargs) -> LogEntry:
34+
"""
35+
Creates a `LogEntry` from the given arguments.
36+
"""
37+
38+
message: str = " ".join(
39+
# do we need to replace_circular here?
40+
[
41+
value if isinstance(value, str) else _json.dumps(
42+
_remove_circular(value)) for value in kwargs.values()
43+
])
44+
45+
return {"severity": severity, "message": message}
46+
47+
48+
def _remove_circular(obj: _typing.Any, refs: _typing.Set[_typing.Any] = set()):
49+
"""
50+
Removes circular references from the given object and replaces them with "[CIRCULAR]".
51+
"""
52+
53+
if (id(obj) in refs):
54+
return "[CIRCULAR]"
55+
56+
if not isinstance(obj, (str, int, float, bool, type(None))):
57+
refs.add(id(obj))
58+
59+
if isinstance(obj, dict):
60+
return {key: _remove_circular(value, refs) for key, value in obj.items()}
61+
elif isinstance(obj, list):
62+
return [_remove_circular(value, refs) for _, value in enumerate(obj)]
63+
elif isinstance(obj, tuple):
64+
return tuple(_remove_circular(value, refs) for _, value in enumerate(obj))
65+
else:
66+
return obj
67+
68+
69+
def _get_write_file(severity: LogSeverity) -> _typing.TextIO:
70+
if severity == LogSeverity.ERROR:
71+
return _sys.stderr
72+
return _sys.stdout
73+
74+
75+
def write(entry: LogEntry) -> None:
76+
write_file = _get_write_file(entry['severity'])
77+
print(_json.dumps(_remove_circular(entry)), file=write_file)
78+
79+
80+
def debug(**kwargs) -> None:
81+
"""
82+
Logs a debug message.
83+
"""
84+
write(_entry_from_args(LogSeverity.DEBUG, **kwargs))
85+
86+
87+
def log(**kwargs) -> None:
88+
"""
89+
Logs a log message.
90+
"""
91+
write(_entry_from_args(LogSeverity.NOTICE, **kwargs))
92+
93+
94+
def info(**kwargs) -> None:
95+
"""
96+
Logs an info message.
97+
"""
98+
write(_entry_from_args(LogSeverity.INFO, **kwargs))
99+
100+
101+
def warn(**kwargs) -> None:
102+
"""
103+
Logs a warning message.
104+
"""
105+
write(_entry_from_args(LogSeverity.WARNING, **kwargs))
106+
107+
108+
def error(**kwargs) -> None:
109+
"""
110+
Logs an error message.
111+
"""
112+
write(_entry_from_args(LogSeverity.ERROR, **kwargs))

tests/test_logger.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
import json
3+
from firebase_functions import logger
4+
5+
6+
class TestLogger:
7+
def test_format_should_be_valid_json(self, capsys: pytest.CaptureFixture[str]):
8+
logger.log(foo="bar")
9+
raw_log_output = capsys.readouterr().out
10+
try:
11+
json.loads(raw_log_output)
12+
except json.JSONDecodeError:
13+
pytest.fail("Log output was not valid JSON.")
14+
15+
def test_log_should_have_severity(self, capsys: pytest.CaptureFixture[str]):
16+
logger.log(foo="bar")
17+
raw_log_output = capsys.readouterr().out
18+
log_output = json.loads(raw_log_output)
19+
assert "severity" in log_output
20+
21+
def test_log_should_have_message(self, capsys: pytest.CaptureFixture[str]):
22+
logger.log(foo="bar")
23+
raw_log_output = capsys.readouterr().out
24+
log_output = json.loads(raw_log_output)
25+
assert "message" in log_output

0 commit comments

Comments
 (0)