- Reset master to upstream/main (16,697 commits) - Overlay 2,271 local-only files (skills, tools, workspace, configs, apps) - Restore IDENTITY.md and USER.md templates - Build verified, gateway running, Discord working Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""Shared logging for James components.
|
|
|
|
Usage:
|
|
from james.lib.log import get_logger
|
|
log = get_logger("watcher")
|
|
log.info("started", interval=30)
|
|
log.error("redis connect failed", exc_info=True)
|
|
|
|
Output is JSON-lines to stderr (captured by systemd journal).
|
|
Set JAMES_LOG_LEVEL to DEBUG/INFO/WARNING/ERROR (default: INFO).
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from typing import Any
|
|
|
|
|
|
class JsonFormatter(logging.Formatter):
|
|
"""Formats log records as single-line JSON for easy parsing."""
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
entry: dict[str, Any] = {
|
|
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(record.created)),
|
|
"level": record.levelname.lower(),
|
|
"component": record.name,
|
|
"msg": record.getMessage(),
|
|
}
|
|
# Merge extra fields passed via log.info("msg", extra={"k": "v"})
|
|
# or via the convenience wrapper below.
|
|
extras = getattr(record, "_extra_fields", None)
|
|
if extras:
|
|
entry.update(extras)
|
|
if record.exc_info and record.exc_info[1] is not None:
|
|
entry["error"] = str(record.exc_info[1])
|
|
entry["error_type"] = type(record.exc_info[1]).__name__
|
|
return json.dumps(entry, default=str)
|
|
|
|
|
|
class JamesLogger(logging.Logger):
|
|
"""Logger subclass that accepts keyword args as structured fields."""
|
|
|
|
def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1, **kwargs):
|
|
if extra is None:
|
|
extra = {}
|
|
extra["_extra_fields"] = kwargs
|
|
super()._log(level, msg, args, exc_info=exc_info, extra=extra, stack_info=stack_info, stacklevel=stacklevel + 1)
|
|
|
|
|
|
def get_logger(component: str) -> JamesLogger:
|
|
"""Return a named JSON logger for a James component."""
|
|
logging.setLoggerClass(JamesLogger)
|
|
logger = logging.getLogger(f"james.{component}")
|
|
if not logger.handlers:
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
handler.setFormatter(JsonFormatter())
|
|
logger.addHandler(handler)
|
|
level = os.environ.get("JAMES_LOG_LEVEL", "INFO").upper()
|
|
logger.setLevel(getattr(logging, level, logging.INFO))
|
|
return logger # type: ignore[return-value]
|