Skip to content

Commit 962ae48

Browse files
add struct logging
1 parent bc6cb86 commit 962ae48

3 files changed

Lines changed: 48 additions & 39 deletions

File tree

components/clp-py-utils/clp_py_utils/clp_logging.py

Lines changed: 33 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
import pathlib
44
from collections.abc import Iterator
55
from contextlib import contextmanager
6-
from contextvars import ContextVar
76
from typing import get_args, Literal
87

8+
import structlog
9+
from structlog.contextvars import (
10+
bind_contextvars,
11+
get_contextvars,
12+
merge_contextvars,
13+
reset_contextvars,
14+
)
15+
916
LoggingLevel = Literal[
1017
"INFO",
1118
"DEBUG",
@@ -15,21 +22,8 @@
1522
"CRITICAL",
1623
]
1724

18-
19-
_LOG_CONTEXT: ContextVar[dict[str, object] | None] = ContextVar("clp_log_context", default=None)
20-
21-
_LOG_CONTEXT_FIELD_ORDER = (
22-
"job_id",
23-
"query_job_type",
24-
"task_id",
25-
"archive_id",
26-
"dataset",
27-
"partition_id",
28-
"celery_task_id",
29-
"clp_subprocess_pid",
30-
"query",
31-
"query_hash",
32-
)
25+
_TIMESTAMP_PROCESSOR = structlog.processors.TimeStamper(fmt="iso", utc=True, key="timestamp")
26+
_JSON_RENDERER = structlog.processors.JSONRenderer()
3327

3428

3529
@contextmanager
@@ -40,43 +34,43 @@ def bind_log_context(**fields: object) -> Iterator[None]:
4034
Fields with ``None`` values are ignored. Nested contexts merge with outer contexts and restore
4135
the previous context on exit.
4236
"""
43-
current_context = _LOG_CONTEXT.get() or {}
44-
new_context = current_context.copy()
45-
new_context.update({key: value for key, value in fields.items() if value is not None})
46-
token = _LOG_CONTEXT.set(new_context)
37+
tokens = bind_contextvars(**{key: value for key, value in fields.items() if value is not None})
4738
try:
4839
yield
4940
finally:
50-
_LOG_CONTEXT.reset(token)
41+
reset_contextvars(**tokens)
5142

5243

5344
def get_log_context() -> dict[str, object]:
5445
"""Returns a copy of the active CLP log correlation context."""
55-
return (_LOG_CONTEXT.get() or {}).copy()
46+
return get_contextvars()
5647

5748

58-
class ClpContextFormatter(logging.Formatter):
59-
"""Appends active CLP correlation fields to log records."""
49+
class _ClpJsonFormatter(logging.Formatter):
50+
"""Formats log records as JSON and includes active CLP correlation fields."""
6051

6152
def format(self, record: logging.LogRecord) -> str:
62-
message = super().format(record)
63-
context = get_log_context()
64-
if not context:
65-
return message
66-
67-
fields = [f"{key}={context[key]}" for key in _LOG_CONTEXT_FIELD_ORDER if key in context]
68-
fields.extend(
69-
f"{key}={value}"
70-
for key, value in sorted(context.items())
71-
if key not in _LOG_CONTEXT_FIELD_ORDER
72-
)
73-
if len(fields) > 0:
74-
message = f"{message} [{' '.join(fields)}]"
75-
return message
53+
method_name = record.levelname.lower()
54+
event_dict: dict[str, object] = {
55+
"event": record.getMessage(),
56+
"logger": record.name,
57+
"level": method_name,
58+
}
59+
if record.exc_info:
60+
event_dict["exception"] = self.formatException(record.exc_info)
61+
if record.stack_info:
62+
event_dict["stack"] = self.formatStack(record.stack_info)
63+
64+
merge_contextvars(None, method_name, event_dict)
65+
_TIMESTAMP_PROCESSOR(None, method_name, event_dict)
66+
rendered_log = _JSON_RENDERER(None, method_name, event_dict)
67+
if isinstance(rendered_log, bytes):
68+
return rendered_log.decode("utf-8")
69+
return rendered_log
7670

7771

7872
def get_logging_formatter() -> logging.Formatter:
79-
return ClpContextFormatter("%(asctime)s %(name)s [%(levelname)s] %(message)s")
73+
return _ClpJsonFormatter()
8074

8175

8276
def set_formatter_on_handlers(logger: logging.Logger) -> None:

components/clp-py-utils/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"result>=0.17.0",
1616
"sqlalchemy>=2.0.46",
1717
"StrEnum>=0.4.15",
18+
"structlog>=25.5.0",
1819
]
1920

2021
[build-system]

components/clp-py-utils/uv.lock

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

0 commit comments

Comments
 (0)