1
0
forked from erp-dev/erp
Files
erpnew/flower/logging_formatters.py
2026-06-26 17:03:54 +08:00

150 lines
4.5 KiB
Python

import json
import logging
import os
import socket
import traceback
from datetime import datetime, timezone
from typing import Any
from urllib.parse import urlsplit
_STANDARD_RECORD_ATTRS = {
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"message",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"taskName",
"thread",
"threadName",
}
def _json_default(value: Any) -> str:
return repr(value)
def _utc_timestamp(created: float) -> str:
return datetime.fromtimestamp(created, tz=timezone.utc).isoformat(timespec="milliseconds")
def _split_client_addr(client_addr: Any) -> tuple[str | None, int | None]:
if not isinstance(client_addr, str):
return None, None
host, sep, port = client_addr.rpartition(":")
if not sep:
return client_addr, None
try:
return host, int(port)
except ValueError:
return client_addr, None
class JsonFormatter(logging.Formatter):
"""
Emit one JSON object per log line for container log collectors.
Uvicorn access logs carry structured fields in record.args; keep using
uvicorn's own access logger and only unpack those fields at format time.
"""
def __init__(self, *args, service_name: str | None = None, environment: str | None = None, **kwargs):
super().__init__(*args, **kwargs)
self.service_name = service_name or os.getenv("SERVICE_NAME", "flower")
self.environment = environment or os.getenv("ENVIRONMENT") or os.getenv("DJANGO_ENV") or ""
self.hostname = socket.gethostname()
def format(self, record: logging.LogRecord) -> str:
event = {
"timestamp": _utc_timestamp(record.created),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"service": self.service_name,
"environment": self.environment,
"hostname": self.hostname,
"module": record.module,
"function": record.funcName,
"line": record.lineno,
"process": record.process,
"process_name": record.processName,
"thread": record.threadName,
}
if record.name == "uvicorn.access":
event.update(self._format_uvicorn_access(record))
if record.exc_info:
exc_type, exc_value, _tb = record.exc_info
event["exception"] = {
"type": exc_type.__name__ if exc_type else None,
"message": str(exc_value) if exc_value else None,
"stacktrace": "".join(traceback.format_exception(*record.exc_info)),
}
elif record.exc_text:
event["exception"] = {"stacktrace": record.exc_text}
if record.stack_info:
event["stack_info"] = record.stack_info
extra = self._collect_extra(record)
if extra:
event["extra"] = extra
return json.dumps(event, ensure_ascii=False, default=_json_default, separators=(",", ":"))
def _format_uvicorn_access(self, record: logging.LogRecord) -> dict[str, Any]:
if not isinstance(record.args, tuple) or len(record.args) != 5:
return {}
client_addr, method, full_path, http_version, status_code = record.args
split_result = urlsplit(str(full_path))
client_host, client_port = _split_client_addr(client_addr)
return {
"client_addr": client_addr,
"client_host": client_host,
"client_port": client_port,
"method": method,
"full_path": full_path,
"path": split_result.path or str(full_path),
"query_string": split_result.query,
"http_version": http_version,
"status_code": int(status_code),
}
def _collect_extra(self, record: logging.LogRecord) -> dict[str, Any]:
extra = {}
for key, value in record.__dict__.items():
if key.startswith("_") or key in _STANDARD_RECORD_ATTRS:
continue
extra[key] = value
return extra
class MaxLevelFilter(logging.Filter):
"""Allow records below the configured level."""
def __init__(self, max_level: str | int):
super().__init__()
self.max_level = logging._checkLevel(max_level)
def filter(self, record: logging.LogRecord) -> bool:
return record.levelno < self.max_level