48 lines
1.7 KiB
Python
48 lines
1.7 KiB
Python
"""Stdlib logging setup — one stream handler on stdout, human or JSON.
|
|
|
|
Workers used to print() everything; that gives no levels, no timestamps, and nothing
|
|
Loki can parse. Default is a compact human format for local runs; set LOG_JSON=1 in the
|
|
container so Grafana Alloy -> Loki gets structured fields (ts, level, logger, msg) plus
|
|
any `extra=` keys a call site attaches.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
|
|
# logging.LogRecord built-ins we don't want to echo into a JSON line as "extra" fields.
|
|
_RESERVED = set(
|
|
logging.makeLogRecord({}).__dict__
|
|
) | {"message", "asctime", "taskName"}
|
|
|
|
|
|
class _JsonFormatter(logging.Formatter):
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
payload = {
|
|
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
|
|
"level": record.levelname,
|
|
"logger": record.name,
|
|
"msg": record.getMessage(),
|
|
}
|
|
for key, value in record.__dict__.items():
|
|
if key not in _RESERVED and not key.startswith("_"):
|
|
payload[key] = value
|
|
if record.exc_info:
|
|
payload["exc"] = self.formatException(record.exc_info)
|
|
return json.dumps(payload, default=str)
|
|
|
|
|
|
def configure(level: str = "INFO", json_logs: bool = False) -> None:
|
|
"""Install a single stdout handler on the root logger (idempotent)."""
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
if json_logs:
|
|
handler.setFormatter(_JsonFormatter())
|
|
else:
|
|
handler.setFormatter(
|
|
logging.Formatter("%(asctime)s %(levelname)-5s %(name)s | %(message)s", "%H:%M:%S")
|
|
)
|
|
root = logging.getLogger()
|
|
root.handlers.clear()
|
|
root.addHandler(handler)
|
|
root.setLevel(level)
|