forked from erp-dev/erp
150 lines
4.5 KiB
Python
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
|