free-claude-code/config/logging_config.py
Alishahryar1 f3a7528d49
Some checks are pending
CI / checks (push) Waiting to run
Major refactor: API, providers, messaging, and Anthropic protocol
Consolidates the incremental refactor work into a single change set: modular web tools (api/web_tools), native Anthropic request building and SSE block policy, OpenAI conversion and error handling, provider transports and rate limiting, messaging handler and tree queue, safe logging, smoke tests, and broad test coverage.
2026-04-26 03:01:14 -07:00

125 lines
3.7 KiB
Python

"""Loguru-based structured logging configuration.
All logs are written to server.log as JSON lines for full traceability.
Stdlib logging is intercepted and funneled to loguru.
Context vars (request_id, node_id, chat_id) from contextualize() are
included at top level for easy grep/filter.
"""
import json
import logging
import re
from pathlib import Path
from loguru import logger
_configured = False
# Context keys we promote to top-level JSON for traceability
_CONTEXT_KEYS = ("request_id", "node_id", "chat_id")
_TELEGRAM_BOT_RE = re.compile(
r"(https?://api\.telegram\.org/)bot([0-9]+:[A-Za-z0-9_-]+)(/?)",
re.IGNORECASE,
)
# Authorization: Bearer <token> (HTTP client / proxy debug lines)
_AUTH_BEARER_RE = re.compile(
r"(\bAuthorization\s*:\s*Bearer\s+)([^\s'\"]+)",
re.IGNORECASE,
)
def _redact_sensitive_substrings(message: str) -> str:
"""Remove obvious API tokens and secrets before JSON log line emission."""
text = _TELEGRAM_BOT_RE.sub(r"\1bot<redacted>\3", message)
return _AUTH_BEARER_RE.sub(r"\1<redacted>", text)
def _serialize_with_context(record) -> str:
"""Format record as JSON with context vars at top level.
Returns a format template; we inject _json into record for output.
"""
extra = record.get("extra", {})
out = {
"time": str(record["time"]),
"level": record["level"].name,
"message": _redact_sensitive_substrings(str(record["message"])),
"module": record["name"],
"function": record["function"],
"line": record["line"],
}
for key in _CONTEXT_KEYS:
if key in extra and extra[key] is not None:
out[key] = extra[key]
record["_json"] = json.dumps(out, default=str)
return "{_json}\n"
class InterceptHandler(logging.Handler):
"""Redirect stdlib logging to loguru."""
def emit(self, record: logging.LogRecord) -> None:
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = logging.currentframe(), 2
while frame is not None and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
def configure_logging(
log_file: str, *, force: bool = False, verbose_third_party: bool = False
) -> None:
"""Configure loguru with JSON output to log_file and intercept stdlib logging.
Idempotent: skips if already configured (e.g. hot reload).
Use force=True to reconfigure (e.g. in tests with a different log path).
When ``verbose_third_party`` is false, noisy HTTP and Telegram loggers are capped
at WARNING unless explicitly configured otherwise.
"""
global _configured
if _configured and not force:
return
_configured = True
# Remove default loguru handler (writes to stderr)
logger.remove()
# Truncate log file on fresh start for clean debugging
Path(log_file).write_text("")
# Add file sink: JSON lines, DEBUG level, context vars at top level
logger.add(
log_file,
level="DEBUG",
format=_serialize_with_context,
encoding="utf-8",
mode="a",
rotation="50 MB",
)
# Intercept stdlib logging: route all root logger output to loguru
intercept = InterceptHandler()
logging.root.handlers = [intercept]
logging.root.setLevel(logging.DEBUG)
third_party = (
"httpx",
"httpcore",
"httpcore.http11",
"httpcore.connection",
"telegram",
"telegram.ext",
)
for name in third_party:
logging.getLogger(name).setLevel(
logging.WARNING if not verbose_third_party else logging.NOTSET
)