Skip to content

Commit 01a2386

Browse files
authored
Rename and clean up logging and starlette (#48)
* Rename and clean up logging and starlette * Verify log_level
1 parent 0696975 commit 01a2386

File tree

7 files changed

+94
-158
lines changed

7 files changed

+94
-158
lines changed

dnstapir/logging.py

Lines changed: 83 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,83 @@
1-
import logging.config
2-
from datetime import datetime
3-
from typing import Any
4-
5-
from jsonformatter import JsonFormatter as _JsonFormatter
6-
7-
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
8-
9-
LOGGING_RECORD_CUSTOM_FORMAT = {
10-
"time": "asctime",
11-
# "Created": "created",
12-
# "RelativeCreated": "relativeCreated",
13-
"name": "name",
14-
# "Levelno": "levelno",
15-
"levelname": "levelname",
16-
"process": "process",
17-
"thread": "thread",
18-
# "threadName": "threadName",
19-
# "Pathname": "pathname",
20-
# "Filename": "filename",
21-
# "Module": "module",
22-
# "Lineno": "lineno",
23-
# "FuncName": "funcName",
24-
"message": "message",
25-
}
26-
27-
LOGGING_CONFIG_JSON: dict[str, Any] = {
28-
"version": 1,
29-
"disable_existing_loggers": False,
30-
"formatters": {
31-
"json": {
32-
"class": "dnstapir.logging.JsonFormatter",
33-
"format": LOGGING_RECORD_CUSTOM_FORMAT,
34-
},
35-
},
36-
"handlers": {
37-
"json": {"class": "logging.StreamHandler", "formatter": "json"},
38-
},
39-
"root": {"handlers": ["json"], "level": "DEBUG"},
40-
}
41-
42-
43-
class JsonFormatter(_JsonFormatter):
44-
def formatTime(self, record, datefmt=None) -> str:
45-
dt = datetime.fromtimestamp(record.created).astimezone()
46-
return dt.strftime(TIMESTAMP_FORMAT)
47-
48-
49-
def configure_json_logging() -> dict[str, Any]:
50-
"""Configure JSON logging and return configuration dictionary"""
51-
logging.config.dictConfig(LOGGING_CONFIG_JSON)
52-
return LOGGING_CONFIG_JSON
1+
"""
2+
structlog helpers based on code from https://wazaari.dev/blog/fastapi-structlog-integration
3+
"""
4+
5+
import logging
6+
7+
import structlog
8+
from structlog.types import EventDict, Processor
9+
10+
VALID_LOG_LEVELS = set(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"])
11+
12+
13+
def drop_color_message_key(_, __, event_dict: EventDict) -> EventDict:
14+
"""
15+
Uvicorn logs the message a second time in the extra `color_message`, but we don't
16+
need it. This processor drops the key from the event dict if it exists.
17+
"""
18+
event_dict.pop("color_message", None)
19+
return event_dict
20+
21+
22+
def setup_logging(json_logs: bool = False, log_level: str = "INFO") -> None:
23+
"""Set up logging"""
24+
25+
if log_level.upper() not in VALID_LOG_LEVELS:
26+
raise ValueError(f"Invalid log level: {log_level}")
27+
28+
timestamper = structlog.processors.TimeStamper(fmt="iso")
29+
30+
shared_processors: list[Processor] = [
31+
structlog.contextvars.merge_contextvars,
32+
structlog.stdlib.add_logger_name,
33+
structlog.stdlib.add_log_level,
34+
structlog.stdlib.PositionalArgumentsFormatter(),
35+
structlog.stdlib.ExtraAdder(),
36+
drop_color_message_key,
37+
timestamper,
38+
structlog.processors.StackInfoRenderer(),
39+
]
40+
41+
if json_logs:
42+
# Format the exception only for JSON logs, as we want to pretty-print them when
43+
# using the ConsoleRenderer
44+
shared_processors.append(structlog.processors.format_exc_info)
45+
46+
structlog.configure(
47+
processors=shared_processors
48+
+ [
49+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
50+
],
51+
logger_factory=structlog.stdlib.LoggerFactory(),
52+
cache_logger_on_first_use=True,
53+
)
54+
55+
log_renderer = structlog.processors.JSONRenderer() if json_logs else structlog.dev.ConsoleRenderer()
56+
57+
formatter = structlog.stdlib.ProcessorFormatter(
58+
# These run ONLY on `logging` entries that do NOT originate within
59+
# structlog.
60+
foreign_pre_chain=shared_processors,
61+
# These run on ALL entries after the pre_chain is done.
62+
processors=[
63+
# Remove _record & _from_structlog.
64+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
65+
log_renderer,
66+
],
67+
)
68+
69+
# Reconfigure the root logger to use our structlog formatter, effectively emitting the logs via structlog
70+
handler = logging.StreamHandler()
71+
handler.setFormatter(formatter)
72+
root_logger = logging.getLogger()
73+
root_logger.addHandler(handler)
74+
root_logger.setLevel(log_level.upper())
75+
76+
for _log in ["uvicorn", "uvicorn.error"]:
77+
# Make sure the logs are handled by the root logger
78+
logging.getLogger(_log).handlers.clear()
79+
logging.getLogger(_log).propagate = True
80+
81+
# Uvicorn logs are re-emitted with more context. We effectively silence them here
82+
logging.getLogger("uvicorn.access").handlers.clear()
83+
logging.getLogger("uvicorn.access").propagate = False
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Starlette middleware and other utilities"""
2+
13
import time
24
import uuid
35
from collections.abc import Awaitable, Callable

dnstapir/structlog.py

Lines changed: 0 additions & 76 deletions
This file was deleted.

poetry.lock

Lines changed: 4 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ readme = "README.md"
77

88
[tool.poetry.dependencies]
99
python = "^3.11"
10-
jsonformatter = "^0.3.2"
1110
opentelemetry-api = { version = "^1.28.1", optional = true }
1211
opentelemetry-exporter-otlp = { version = "^1.28.1", optional = true }
1312
opentelemetry-instrumentation-botocore = { version = ">=0.48b0", optional = true }

tests/test_logging.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import logging
1+
import structlog
22

3-
from dnstapir.logging import configure_json_logging
3+
from dnstapir.logging import setup_logging
44

55

66
def test_logging():
7-
configure_json_logging()
8-
logger = logging.getLogger(__name__)
9-
logger.warning("Hello world")
7+
setup_logging(json_logs=False, log_level="INFO")
8+
logger = structlog.getLogger()
9+
logger.warning("Hello %s", "world", foo="bar")

tests/test_structlog.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)