-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogging_config.py
More file actions
108 lines (88 loc) · 3.38 KB
/
logging_config.py
File metadata and controls
108 lines (88 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""
Structured Logging Adapter
============================
Provides structured, JSON-capable logging with context binding.
Uses `structlog` when available, falls back to stdlib `logging`.
"""
import json
import logging
import os
import sys
from datetime import datetime, timezone
from typing import Any
_HAS_STRUCTLOG = False
try:
import structlog
_HAS_STRUCTLOG = True
except ImportError:
pass
def get_logger(name: str = "matchmaker", **initial_context: Any):
"""Get a structured logger.
If structlog is installed it returns a BoundLogger; otherwise a
stdlib Logger wrapped with a thin JSON formatter.
"""
if _HAS_STRUCTLOG:
return _get_structlog_logger(name, **initial_context)
return _get_stdlib_logger(name, **initial_context)
def _get_structlog_logger(name: str, **context: Any):
"""Configure and return a structlog BoundLogger."""
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
(
structlog.dev.ConsoleRenderer()
if os.environ.get("LOG_FORMAT", "json") != "json"
else structlog.processors.JSONRenderer()
),
],
wrapper_class=structlog.make_filtering_bound_logger(
logging.getLevelName(os.environ.get("LOG_LEVEL", "INFO").upper())
),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
return structlog.get_logger(name, **context)
class _JSONFormatter(logging.Formatter):
"""Custom JSON formatter for stdlib logging."""
def format(self, record: logging.LogRecord) -> str:
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if hasattr(record, "context"):
entry["context"] = record.context
if record.exc_info and record.exc_info[1]:
entry["exception"] = str(record.exc_info[1])
return json.dumps(entry)
class _ContextAdapter(logging.LoggerAdapter):
"""Logger adapter that carries a bound context dict."""
def process(self, msg, kwargs):
extra = kwargs.setdefault("extra", {})
extra["context"] = self.extra
return msg, kwargs
def bind(self, **new_context: Any) -> "_ContextAdapter":
"""Return a new adapter with additional context."""
merged = {**self.extra, **new_context}
return _ContextAdapter(self.logger, merged)
def _get_stdlib_logger(name: str, **context: Any) -> _ContextAdapter:
"""Set up a stdlib logger with JSON formatting."""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler(sys.stderr)
log_format = os.environ.get("LOG_FORMAT", "json")
if log_format == "json":
handler.setFormatter(_JSONFormatter())
else:
handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
)
logger.addHandler(handler)
level_name = os.environ.get("LOG_LEVEL", "INFO").upper()
logger.setLevel(logging.getLevelName(level_name))
return _ContextAdapter(logger, context)