mirror of
https://github.com/kristoferssolo/bunyan-formatter.git
synced 2026-02-04 06:22:05 +00:00
118 lines
3.1 KiB
Python
118 lines
3.1 KiB
Python
import json
|
|
import logging
|
|
import socket
|
|
import time
|
|
import traceback
|
|
from logging import Formatter, LogRecord
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
DEFAULT_FIELDS = {
|
|
"name",
|
|
"msg",
|
|
"args",
|
|
"levelname",
|
|
"levelno",
|
|
"pathname",
|
|
"filename",
|
|
"module",
|
|
"exc_info",
|
|
"exc_text",
|
|
"stack_info",
|
|
"lineno",
|
|
"funcName",
|
|
"created",
|
|
"msecs",
|
|
"relativeCreated",
|
|
"thread",
|
|
"threadName",
|
|
"processName",
|
|
"process",
|
|
"taskName",
|
|
}
|
|
|
|
LEVEL_MAP = {
|
|
logging.NOTSET: 10,
|
|
logging.DEBUG: 20,
|
|
logging.INFO: 30,
|
|
logging.WARNING: 40,
|
|
logging.ERROR: 50,
|
|
logging.CRITICAL: 60,
|
|
}
|
|
|
|
|
|
class BunyanFormatter(Formatter):
|
|
def __init__(self, project_name: str, project_root: Path) -> None:
|
|
super().__init__()
|
|
self.project_name = project_name
|
|
self.project_root = project_root
|
|
self.hostname = socket.gethostname()
|
|
|
|
def format(self, record: LogRecord) -> str:
|
|
file_path = Path(record.pathname)
|
|
try:
|
|
relative_path = file_path.relative_to(self.project_root)
|
|
except ValueError:
|
|
relative_path = file_path
|
|
|
|
log_entry = {
|
|
"v": 0,
|
|
"name": self.project_name,
|
|
"msg": record.getMessage(),
|
|
"level": LEVEL_MAP.get(record.levelno, record.levelno),
|
|
"levelname": record.levelname,
|
|
"hostname": self.hostname,
|
|
"pid": record.process,
|
|
"time": self.formatTime(record),
|
|
"target": record.name,
|
|
"line": record.lineno,
|
|
"file": str(relative_path),
|
|
}
|
|
|
|
# Handle extra fields
|
|
extra_fields = {k: v for k, v in record.__dict__.items() if k not in DEFAULT_FIELDS}
|
|
if extra_fields:
|
|
log_entry["extra"] = extra_fields
|
|
|
|
# Handle exception information
|
|
if record.exc_info and all(record.exc_info):
|
|
log_entry["err"] = self._format_exception(record)
|
|
|
|
return json.dumps(log_entry)
|
|
|
|
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: # noqa: N802
|
|
ct = self.converter(record.created)
|
|
if datefmt:
|
|
s = time.strftime(datefmt, ct)
|
|
else:
|
|
t = time.strftime("%Y-%m-%dT%H:%M:%S", ct)
|
|
s = f"{t}.{int(record.msecs):03d}Z"
|
|
return s
|
|
|
|
def _format_exception(self, record: LogRecord) -> dict[str, Any]:
|
|
exc_info = record.exc_info
|
|
|
|
if exc_info is None or len(exc_info) != 3:
|
|
return {}
|
|
|
|
exc_type, exc_value, exc_traceback = exc_info
|
|
|
|
if exc_type is None or exc_value is None or exc_traceback is None:
|
|
return {}
|
|
|
|
stack = traceback.extract_tb(exc_traceback)
|
|
|
|
return {
|
|
"message": str(exc_value),
|
|
"name": getattr(exc_type, "__name__", "UnknownException"),
|
|
"stack": [
|
|
{
|
|
"file": frame.filename,
|
|
"line": frame.lineno,
|
|
"function": frame.name,
|
|
"text": frame.line.strip() if frame.line is not None else "",
|
|
}
|
|
for frame in stack
|
|
],
|
|
}
|