init commit
This commit is contained in:
149
app/utils/logging.py
Normal file
149
app/utils/logging.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Structured logging setup for Code of Conquest.
|
||||
|
||||
Environment-aware behavior:
|
||||
- dev → colorful console output (human-friendly)
|
||||
- test/prod → JSON logs (machine-friendly)
|
||||
|
||||
Features:
|
||||
- Includes logger name, level, filename, and line number
|
||||
- Unified stdlib + structlog integration
|
||||
- Respects LOG_LEVEL from environment
|
||||
- Works with both ChatGPT/Ollama components and Web/TUI layers
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from structlog.dev import ConsoleRenderer
|
||||
from structlog.processors import JSONRenderer, TimeStamper, CallsiteParameterAdder, CallsiteParameter
|
||||
from structlog.stdlib import ProcessorFormatter
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ConfiguredState:
|
||||
configured: bool = False
|
||||
level_name: str = "INFO"
|
||||
|
||||
|
||||
_state = _ConfiguredState()
|
||||
|
||||
|
||||
def _level_from_name(name: str) -> int:
|
||||
mapping = {
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
"NOTSET": logging.NOTSET,
|
||||
}
|
||||
return mapping.get((name or "INFO").upper(), logging.INFO)
|
||||
|
||||
|
||||
def _shared_processors():
|
||||
"""
|
||||
Processors common to both console and JSON pipelines.
|
||||
Adds level, logger name, and callsite metadata.
|
||||
"""
|
||||
return [
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
CallsiteParameterAdder(
|
||||
parameters=[
|
||||
CallsiteParameter.FILENAME,
|
||||
CallsiteParameter.LINENO,
|
||||
CallsiteParameter.FUNC_NAME,
|
||||
]
|
||||
),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
]
|
||||
|
||||
|
||||
|
||||
def _console_renderer():
|
||||
"""Pretty colored logs for development."""
|
||||
return ConsoleRenderer()
|
||||
|
||||
|
||||
def _json_renderer():
|
||||
"""Machine-friendly JSON logs for test/prod."""
|
||||
return JSONRenderer(sort_keys=True)
|
||||
|
||||
|
||||
def configure_logging(settings=None) -> None:
|
||||
"""
|
||||
Configure structlog + stdlib logging once for the process.
|
||||
"""
|
||||
if _state.configured:
|
||||
return
|
||||
|
||||
if settings is None:
|
||||
from app.utils.settings import get_settings # lazy import
|
||||
settings = get_settings()
|
||||
|
||||
env = settings.env.value
|
||||
level_name = settings.log_level or "INFO"
|
||||
level = _level_from_name(level_name)
|
||||
|
||||
# Choose renderers
|
||||
if env == "dev":
|
||||
renderer = _console_renderer()
|
||||
foreign_pre_chain = _shared_processors()
|
||||
else:
|
||||
renderer = _json_renderer()
|
||||
foreign_pre_chain = _shared_processors() + [
|
||||
TimeStamper(fmt="iso", utc=True, key="ts")
|
||||
]
|
||||
|
||||
# stdlib -> structlog bridge
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(
|
||||
ProcessorFormatter(
|
||||
processor=renderer,
|
||||
foreign_pre_chain=foreign_pre_chain,
|
||||
)
|
||||
)
|
||||
|
||||
root = logging.getLogger()
|
||||
root.handlers.clear()
|
||||
root.setLevel(level)
|
||||
root.addHandler(handler)
|
||||
|
||||
# Quiet noisy libs
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
||||
|
||||
# structlog pipeline
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.filter_by_level,
|
||||
*foreign_pre_chain,
|
||||
ProcessorFormatter.wrap_for_formatter, # hand off to renderer
|
||||
],
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
_state.configured = True
|
||||
_state.level_name = level_name
|
||||
|
||||
|
||||
def get_logger(name: Optional[str] = None) -> structlog.stdlib.BoundLogger:
|
||||
"""
|
||||
Retrieve a structlog logger.
|
||||
"""
|
||||
if name is None:
|
||||
return structlog.get_logger()
|
||||
return structlog.get_logger(name)
|
||||
Reference in New Issue
Block a user