init commit

This commit is contained in:
2025-11-02 01:14:41 -05:00
commit 7bf81109b3
31 changed files with 2387 additions and 0 deletions

149
app/utils/logging.py Normal file
View 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)