forked from erp-dev/erp
feat: log formatted
This commit is contained in:
149
flower/logging_formatters.py
Normal file
149
flower/logging_formatters.py
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
Reference in New Issue
Block a user