Skip to content

Commit 51ef299

Browse files
authored
Merge pull request #591 from mdekstrand/feature/json-logging
Add support for 12Factor-style JSON logging
2 parents f3e2274 + bcccbad commit 51ef299

File tree

3 files changed

+58
-32
lines changed

3 files changed

+58
-32
lines changed

lenskit/lenskit/logging/__init__.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22
Logging, progress, and resource records.
33
"""
44

5-
import os
6-
from typing import Any
7-
8-
import structlog
9-
105
from ._proxy import get_logger
116
from .config import LoggingConfig, basic_logging
127
from .progress import Progress, item_progress, set_progress_impl
138
from .tasks import Task
9+
from .tracing import trace
1410

1511
__all__ = [
1612
"LoggingConfig",
@@ -22,24 +18,3 @@
2218
"get_logger",
2319
"trace",
2420
]
25-
26-
_trace_debug = os.environ.get("LK_TRACE", "no").lower() == "debug"
27-
28-
29-
def trace(logger: structlog.stdlib.BoundLogger, *args: Any, **kwargs: Any):
30-
"""
31-
Emit a trace-level message, if LensKit tracing is enabled. Trace-level
32-
messages are more fine-grained than debug-level messages, and you usually
33-
don't want them.
34-
35-
This function does not work on the lazy proxies returned by
36-
:func:`get_logger` and similar — it only works on bound loggers.
37-
38-
Stability:
39-
Caller
40-
"""
41-
meth = getattr(logger, "trace", None)
42-
if meth is not None:
43-
meth(*args, **kwargs)
44-
elif _trace_debug:
45-
logger.debug(*args, **kwargs)

lenskit/lenskit/logging/config.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import logging
88
import os
99
import re
10+
import sys
1011
import warnings
1112
from pathlib import Path
13+
from typing import Literal, TypeAlias
1214

1315
import structlog
1416

@@ -24,6 +26,7 @@
2426
structlog.stdlib.PositionalArgumentsFormatter(),
2527
structlog.processors.MaybeTimeStamper(),
2628
]
29+
LogFormat: TypeAlias = Literal["json", "logfmt", "text"]
2730

2831
_active_config: LoggingConfig | None = None
2932

@@ -64,8 +67,10 @@ class LoggingConfig: # pragma: nocover
6467
"""
6568

6669
level: int = logging.INFO
70+
stream_json: bool = False
6771
file: Path | None = None
6872
file_level: int | None = None
73+
file_format: LogFormat = "json"
6974

7075
def __init__(self):
7176
# initialize configuration from environment variables
@@ -85,6 +90,12 @@ def effective_level(self) -> int:
8590
else:
8691
return self.level
8792

93+
def set_term_json(self, flag: bool = True):
94+
"""
95+
Configure logging to stream JSON lines to the stderr (useful for web services).
96+
"""
97+
self.stream_json = flag
98+
8899
def set_verbose(self, verbose: bool | int = True):
89100
"""
90101
Enable verbose logging.
@@ -108,12 +119,15 @@ def set_verbose(self, verbose: bool | int = True):
108119
else:
109120
self.level = logging.INFO
110121

111-
def log_file(self, path: os.PathLike[str], level: int | None = None):
122+
def set_log_file(
123+
self, path: os.PathLike[str], level: int | None = None, format: LogFormat = "json"
124+
):
112125
"""
113126
Configure a log file.
114127
"""
115128
self.file = Path(path)
116129
self.file_level = level
130+
self.file_format = format
117131

118132
def apply(self):
119133
"""
@@ -123,8 +137,15 @@ def apply(self):
123137

124138
setup_console()
125139
root = logging.getLogger()
126-
term = ConsoleHandler()
127-
term.setLevel(self.level)
140+
141+
if self.stream_json:
142+
term = logging.StreamHandler(sys.stderr)
143+
term.setLevel(self.level)
144+
proc_fmt = structlog.processors.JSONRenderer()
145+
else:
146+
term = ConsoleHandler()
147+
term.setLevel(self.level)
148+
proc_fmt = structlog.dev.ConsoleRenderer(colors=term.supports_color)
128149

129150
eff_lvl = self.effective_level
130151
structlog.configure(
@@ -136,7 +157,7 @@ def apply(self):
136157
processors=[
137158
remove_internal,
138159
format_timestamp,
139-
structlog.dev.ConsoleRenderer(colors=term.supports_color),
160+
proc_fmt,
140161
],
141162
foreign_pre_chain=CORE_PROCESSORS,
142163
)
@@ -147,11 +168,19 @@ def apply(self):
147168
if self.file:
148169
file_level = self.file_level if self.file_level is not None else self.level
149170
file = logging.FileHandler(self.file, mode="w")
171+
172+
if self.file_format == "json":
173+
proc_fmt = structlog.processors.JSONRenderer()
174+
elif self.file_format == "logfmt":
175+
proc_fmt = structlog.processors.LogfmtRenderer(key_order=["event", "timestamp"])
176+
else:
177+
proc_fmt = structlog.processors.KeyValueRenderer(key_order=["event", "timestamp"])
178+
150179
ffmt = structlog.stdlib.ProcessorFormatter(
151180
processors=[
152181
remove_internal,
153182
structlog.processors.ExceptionPrettyPrinter(),
154-
structlog.processors.JSONRenderer(),
183+
proc_fmt,
155184
],
156185
foreign_pre_chain=CORE_PROCESSORS,
157186
)

lenskit/lenskit/logging/tracing.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,32 @@
55
from __future__ import annotations
66

77
import logging
8+
import os
89
from typing import Any
910

1011
import structlog
1112

13+
_trace_debug = os.environ.get("LK_TRACE", "no").lower() == "debug"
14+
15+
16+
def trace(logger: structlog.stdlib.BoundLogger, *args: Any, **kwargs: Any):
17+
"""
18+
Emit a trace-level message, if LensKit tracing is enabled. Trace-level
19+
messages are more fine-grained than debug-level messages, and you usually
20+
don't want them.
21+
22+
This function does not work on the lazy proxies returned by
23+
:func:`get_logger` and similar — it only works on bound loggers.
24+
25+
Stability:
26+
Caller
27+
"""
28+
meth = getattr(logger, "trace", None)
29+
if meth is not None:
30+
meth(*args, **kwargs)
31+
elif _trace_debug:
32+
logger.debug(*args, **kwargs)
33+
1234

1335
class TracingLogger(structlog.stdlib.BoundLogger):
1436
"""
@@ -25,7 +47,7 @@ def trace(self, event: str | None, *args: Any, **kw: Any):
2547
if args:
2648
kw["positional_args"] = args
2749
try:
28-
args, kwargs = self._process_event("trace", event, kw)
50+
args, kwargs = self._process_event("trace", event, kw) # type: ignore
2951
except structlog.DropEvent:
3052
return None
3153
self._logger.debug(*args, **kwargs)

0 commit comments

Comments
 (0)