-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathlogger.py
More file actions
204 lines (158 loc) · 5.7 KB
/
logger.py
File metadata and controls
204 lines (158 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""
Logger module for Firebase Functions.
"""
import enum as _enum
import json as _json
import sys as _sys
import typing as _typing
import traceback as _traceback
import typing_extensions as _typing_extensions
# If encoding is not 'utf-8', change it to 'utf-8'.
if _sys.stdout.encoding != "utf-8":
_sys.stdout.reconfigure(encoding="utf-8") # type: ignore
if _sys.stderr.encoding != "utf-8":
_sys.stderr.reconfigure(encoding="utf-8") # type: ignore
class LogSeverity(str, _enum.Enum):
"""
`LogSeverity` indicates the detailed severity of the log entry. See
`LogSeverity <https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity>`.
"""
DEBUG = "DEBUG"
INFO = "INFO"
NOTICE = "NOTICE"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
ALERT = "ALERT"
EMERGENCY = "EMERGENCY"
def __str__(self) -> str:
return self.value
class LogEntry(_typing.TypedDict):
"""
`LogEntry` represents a log entry.
See `LogEntry <https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry>`_.
"""
severity: _typing_extensions.Required[LogSeverity]
message: _typing_extensions.NotRequired[str]
def _entry_from_args(severity: LogSeverity, *args, **kwargs) -> LogEntry:
"""
Creates a `LogEntry` from the given arguments.
"""
message: str = " ".join(
[
value
if isinstance(value, str)
else _json.dumps(_remove_circular(value), ensure_ascii=False)
for value in args
]
)
other: dict[str, _typing.Any] = {
key: value if isinstance(value, str) else _remove_circular(value)
for key, value in kwargs.items()
}
entry: dict[str, _typing.Any] = {"severity": severity, **other}
if message:
entry["message"] = message
return _typing.cast(LogEntry, entry)
def _exception_from_args(
exception: BaseException, refs: set[_typing.Any] | None = None
) -> dict[str, _typing.Any]:
"""
Creates a JSON-safe representation of an exception.
"""
details: dict[str, _typing.Any] = {
"type": exception.__class__.__name__,
"message": _safe_exception_string(exception),
}
if exception.args:
details["args"] = _remove_circular(exception.args, refs)
if exception.__traceback__ is not None:
try:
details["stack_trace"] = "".join(
_traceback.format_exception(
exception.__class__, exception, exception.__traceback__
)
)
except Exception:
details["stack_trace"] = "".join(_traceback.format_tb(exception.__traceback__))
details["stack_trace"] += f"{exception.__class__.__name__}: {details['message']}\n"
return details
def _safe_exception_string(exception: BaseException) -> str:
"""
Returns a string representation of an exception without propagating repr/str errors.
"""
try:
return str(exception)
except Exception:
return exception.__class__.__name__
def _remove_circular(obj: _typing.Any, refs: set[_typing.Any] | None = None):
"""
Removes circular references from the given object and replaces them with "[CIRCULAR]".
"""
if refs is None:
refs = set()
# Check if the object is already in the current recursion stack
if id(obj) in refs:
return "[CIRCULAR]"
# For non-primitive objects, add the current object's id to the recursion stack
if not isinstance(obj, str | int | float | bool | type(None)):
refs.add(id(obj))
# Recursively process the object based on its type
result: _typing.Any
if isinstance(obj, BaseException):
result = _exception_from_args(obj, refs)
elif isinstance(obj, dict):
result = {key: _remove_circular(value, refs) for key, value in obj.items()}
elif isinstance(obj, list):
result = [_remove_circular(item, refs) for item in obj]
elif isinstance(obj, tuple):
result = tuple(_remove_circular(item, refs) for item in obj)
else:
result = obj
# Remove the object's id from the recursion stack after processing
if not isinstance(obj, str | int | float | bool | type(None)):
refs.remove(id(obj))
return result
def _get_write_file(severity: LogSeverity) -> _typing.TextIO:
if severity == LogSeverity.ERROR:
return _sys.stderr
return _sys.stdout
def write(entry: LogEntry) -> None:
write_file = _get_write_file(entry["severity"])
print(_json.dumps(_remove_circular(entry), ensure_ascii=False), file=write_file)
def debug(*args, **kwargs) -> None:
"""
Logs a debug message.
"""
write(_entry_from_args(LogSeverity.DEBUG, *args, **kwargs))
def log(*args, **kwargs) -> None:
"""
Logs a log message.
"""
write(_entry_from_args(LogSeverity.NOTICE, *args, **kwargs))
def info(*args, **kwargs) -> None:
"""
Logs an info message.
"""
write(_entry_from_args(LogSeverity.INFO, *args, **kwargs))
def warn(*args, **kwargs) -> None:
"""
Logs a warning message.
"""
write(_entry_from_args(LogSeverity.WARNING, *args, **kwargs))
def error(*args, **kwargs) -> None:
"""
Logs an error message.
"""
write(_entry_from_args(LogSeverity.ERROR, *args, **kwargs))
def exception(*args, **kwargs) -> None:
"""
Logs an error message and includes the active stack trace.
"""
entry = _entry_from_args(LogSeverity.ERROR, *args, **kwargs)
exc_type, exc_value, exc_traceback = _sys.exc_info()
if exc_type is not None and exc_value is not None and exc_traceback is not None:
entry["stack_trace"] = "".join(
_traceback.format_exception(exc_type, exc_value, exc_traceback)
)
write(entry)