-
Notifications
You must be signed in to change notification settings - Fork 26
feat: implement logger #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
c744ded
Initial implementation of logger
exaby73 aaec1a6
format code
exaby73 ef3a765
fix: lints
exaby73 89d7a4f
fix: format code
exaby73 b416ba3
add: additional tests
exaby73 a02b001
Merge branch 'main' into feat/logger
taeold 2315e4d
Merge branch 'main' into feat/logger
exaby73 43f3bf5
feat: add support for json data
exaby73 78fc22d
fix: tests and lints
exaby73 dbf8fc2
fix: lints
exaby73 225ccd1
fix: format
exaby73 3662d07
Merge branch 'main' into feat/logger
taeold File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
""" | ||
Logger module for Firebase Functions. | ||
""" | ||
|
||
import enum as _enum | ||
import json as _json | ||
import sys as _sys | ||
import typing as _typing | ||
import typing_extensions as _typing_extensions | ||
|
||
|
||
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" | ||
|
||
|
||
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)) | ||
for value in args | ||
]) | ||
|
||
other: _typing.Dict[str, _typing.Any] = { | ||
key: value if isinstance(value, str) else _remove_circular(value) | ||
for key, value in kwargs.items() | ||
} | ||
|
||
entry: _typing.Dict[str, _typing.Any] = {"severity": severity, **other} | ||
if message: | ||
entry["message"] = message | ||
|
||
return _typing.cast(LogEntry, entry) | ||
|
||
|
||
def _remove_circular(obj: _typing.Any, | ||
refs: _typing.Set[_typing.Any] | None = None): | ||
""" | ||
Removes circular references from the given object and replaces them with "[CIRCULAR]". | ||
""" | ||
|
||
if refs is None: | ||
refs = set() | ||
|
||
if id(obj) in refs: | ||
return "[CIRCULAR]" | ||
|
||
if not isinstance(obj, (str, int, float, bool, type(None))): | ||
refs.add(id(obj)) | ||
|
||
if isinstance(obj, dict): | ||
return {key: _remove_circular(value, refs) for key, value in obj.items()} | ||
elif isinstance(obj, list): | ||
return [_remove_circular(value, refs) for _, value in enumerate(obj)] | ||
elif isinstance(obj, tuple): | ||
return tuple( | ||
_remove_circular(value, refs) for _, value in enumerate(obj)) | ||
else: | ||
return obj | ||
|
||
|
||
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)), 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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -387,6 +387,8 @@ def timestamp_conversion(time: str) -> _dt.datetime: | |
elif precision_timestamp == PrecisionTimestamp.SECONDS: | ||
return second_timestamp_conversion(time) | ||
|
||
raise ValueError("Invalid timestamp") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likewise - related to log impl? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again a lint failure here because the function wasn't ending in a return and the if, elif is not exhaustive |
||
|
||
|
||
def microsecond_timestamp_conversion(time: str) -> _dt.datetime: | ||
"""Converts a microsecond timestamp and returns a datetime object of the current time in UTC""" | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
""" | ||
Logger module tests. | ||
""" | ||
|
||
import pytest | ||
import json | ||
from firebase_functions import logger | ||
|
||
|
||
class TestLogger: | ||
""" | ||
Tests for the logger module. | ||
""" | ||
|
||
def test_format_should_be_valid_json(self, | ||
capsys: pytest.CaptureFixture[str]): | ||
logger.log(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
try: | ||
json.loads(raw_log_output) | ||
except json.JSONDecodeError: | ||
pytest.fail("Log output was not valid JSON.") | ||
|
||
def test_log_should_have_severity(self, capsys: pytest.CaptureFixture[str]): | ||
logger.log(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert "severity" in log_output | ||
|
||
def test_severity_should_be_debug(self, capsys: pytest.CaptureFixture[str]): | ||
logger.debug(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["severity"] == "DEBUG" | ||
|
||
def test_severity_should_be_notice(self, | ||
capsys: pytest.CaptureFixture[str]): | ||
logger.log(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["severity"] == "NOTICE" | ||
|
||
def test_severity_should_be_info(self, capsys: pytest.CaptureFixture[str]): | ||
logger.info(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["severity"] == "INFO" | ||
|
||
def test_severity_should_be_warning(self, | ||
capsys: pytest.CaptureFixture[str]): | ||
logger.warn(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["severity"] == "WARNING" | ||
|
||
def test_severity_should_be_error(self, capsys: pytest.CaptureFixture[str]): | ||
logger.error(foo="bar") | ||
raw_log_output = capsys.readouterr().err | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["severity"] == "ERROR" | ||
|
||
def test_log_should_have_message(self, capsys: pytest.CaptureFixture[str]): | ||
logger.log("bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert "message" in log_output | ||
|
||
def test_log_should_have_other_keys(self, | ||
capsys: pytest.CaptureFixture[str]): | ||
logger.log(foo="bar") | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert "foo" in log_output | ||
|
||
def test_message_should_be_space_separated( | ||
self, capsys: pytest.CaptureFixture[str]): | ||
logger.log("bar", "qux") | ||
expected_message = "bar qux" | ||
raw_log_output = capsys.readouterr().out | ||
log_output = json.loads(raw_log_output) | ||
assert log_output["message"] == expected_message |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm this feels unrelated? should we make a separate PR for this or is this necessary for log impl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mypy was updated and was causing CI to fail because the types didn't match. I'm open to suggestions. This is mainly a linting fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe clearest to make the mypy related linting fixes in a separate PR, and have only the logging things here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without these, the CI doesn't pass. Possibly can make a separate PR for these that can merged into
main
, before we merge this in so that we can have those changes rebased into this branch. I'll look into doing this next week :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh let's save ourselves some time and push it in this PR.