Files
openclaw/workspace/james/lib/log.py
Clawd Bot ca9b510922 chore: align with upstream openclaw/openclaw and overlay local additions
- 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>
2026-03-03 07:40:46 +01:00

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]