Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ omit =
src/_monitors_load/*
src/tmp/*
src/main.py
src/utils/log.py
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
Expand Down
2 changes: 1 addition & 1 deletion configs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ logging:
timestamp: created
level: levelname
file_path: pathname
logger_name: name
function_name: funcName
line_number: lineno
logger_name: name
message: message

# Application database pool settings
Expand Down
2 changes: 2 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Still missing tests for main.py, so it's been ignored in the .coveragerc file

import asyncio
import logging
import sys
Expand Down
4 changes: 2 additions & 2 deletions src/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ def format(self, record: logging.LogRecord) -> str:
key: getattr(record, record_field) for key, record_field in self.fields.items()
}

if record.exc_info:
if record.exc_info: # pragma: no cover
message_dict["exception"] = self.formatException(record.exc_info)

if record.stack_info:
if record.stack_info: # pragma: no cover
message_dict["stack_info"] = self.formatStack(record.stack_info)

return json.dumps(message_dict, default=str)
Expand Down
123 changes: 123 additions & 0 deletions tests/utils/test_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import json
import logging
import re
import time

import pytest

import utils.log as log
from configs import FriendlyLogConfig, JsonLogConfig, configs


def set_friendly_formatter(monkeypatch, log_format: str | None) -> None:
"""Set the friendly formatter for the logging configuration"""
monkeypatch.setattr(logging.root, "handlers", [])
monkeypatch.setattr(configs, "logging", FriendlyLogConfig(mode="friendly", format=log_format))
log.setup()


def set_json_formatter(monkeypatch, fields: dict[str, str] | None) -> None:
"""Set the JSON formatter for the logging configuration"""
monkeypatch.setattr(logging.root, "handlers", [])
monkeypatch.setattr(configs, "logging", JsonLogConfig(mode="json", fields=fields))
log.setup()


@pytest.mark.parametrize(
"log_format",
[
"%(asctime)s %(levelname)s: %(message)s",
"%(asctime)s %(levelname)s %(message)s",
"%(levelname)s %(message)s",
],
)
@pytest.mark.parametrize("level", ["info", "warning", "error"])
def test_friendly_formatter(capsys, monkeypatch, log_format, level):
"""'friendly' formatter should format the log message in a friendly way using the provided log
format"""
set_friendly_formatter(monkeypatch, log_format)

logger = logging.getLogger("test_friendly_formatter")
message = f"message for '{level}' level"
getattr(logger, level)(message)

captured_message = capsys.readouterr().err.strip()

expected_content = log_format.replace("%(asctime)s", r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d*")
expected_content = expected_content.replace("%(levelname)s", level.upper())
expected_content = expected_content.replace("%(message)s", message)
expected_pattern = "\x1b" + r".*m" + expected_content + "\x1b" + r"\[0m"

match = re.match(expected_pattern, captured_message)
assert match is not None


def test_friendly_formatter_no_log_format(capsys, monkeypatch):
"""'friendly' formatter should format the log message in a friendly way using a default log
format if none was provided"""
set_friendly_formatter(monkeypatch, None)

logger = logging.getLogger("test_friendly_formatter_no_log_format")
message = "message for 'info' level"
logger.info(message)

captured_message = capsys.readouterr().err.strip()

log_format = "%(asctime)s \\[%(levelname)s\\]: %(message)s"
expected_content = log_format.replace("%(asctime)s", r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d*")
expected_content = expected_content.replace("%(levelname)s", "INFO")
expected_content = expected_content.replace("%(message)s", message)
expected_pattern = "\x1b" + r".*m" + expected_content + "\x1b" + r"\[0m"

match = re.match(expected_pattern, captured_message)
assert match is not None


@pytest.mark.parametrize(
"fields",
[
{"message": "message"},
{"some_message": "message"},
{"message": "message", "level": "levelname"},
{"some_message": "message", "log_level": "levelname"},
{"message": "message", "level": "levelname", "time": "created"},
{"some_message": "message", "log_level": "levelname", "timestamp": "created"},
],
)
@pytest.mark.parametrize("level", ["info", "warning", "error"])
def test_json_formatter(capsys, monkeypatch, fields, level):
"""'json' formatter should format the log message as a JSON object using the provided fields"""
set_json_formatter(monkeypatch, fields)

logger = logging.getLogger("test_json_formatter")
message = f"message for '{level}' level"
getattr(logger, level)(message)

captured_message = capsys.readouterr().err.strip()
message_dict = json.loads(captured_message)

for key, value in message_dict.items():
if key in ("message", "some_message"):
assert value == message
elif key in ("level", "log_level"):
assert value == level.upper()
elif key in ("time", "timestamp"):
assert isinstance(value, float)
assert value > time.time() - 0.001
else:
assert False, "Unexpected field in log"


def test_json_formatter_no_fields(capsys, monkeypatch):
"""'json' formatter should format the log message as a JSON object only with the 'message' field
if no fields were provided"""
set_json_formatter(monkeypatch, None)

logger = logging.getLogger("test_json_formatter_no_fields")
message = "message for 'info' level"
logger.info(message)

captured_message = capsys.readouterr().err.strip()
message_dict = json.loads(captured_message)

assert message_dict == {"message": "message for 'info' level"}
Loading