feat(common): complete Phase 1 - project config, structured logging, and ClickHouse DDL

This commit is contained in:
2026-09-19 13:12:04 +03:30
parent 5f75a1a2be
commit 43df200eeb
9 changed files with 364 additions and 3 deletions

87
app/common/logging.py Normal file
View File

@@ -0,0 +1,87 @@
import json
import logging
import sys
from datetime import datetime, timezone
from typing import Any, Dict, Optional
class StructuredJsonFormatter(logging.Formatter):
"""
JSON log formatter emitting structured log records with standard telecom pipeline fields:
- timestamp (ISO 8601 UTC)
- level (INFO, WARN, ERROR, DEBUG)
- service (service name)
- raw_id (record identifier or None)
- event (message string)
"""
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def format(self, record: logging.LogRecord) -> str:
log_entry: Dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"level": record.levelname,
"service": getattr(record, "service", self.service_name),
"raw_id": getattr(record, "raw_id", None),
"event": record.getMessage(),
}
# Include exception info if present
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
# Include additional extra context fields if present
for key, value in record.__dict__.items():
if key not in {
"name",
"msg",
"args",
"levelname",
"levelno",
"pathname",
"filename",
"module",
"exc_info",
"exc_text",
"stack_info",
"lineno",
"funcName",
"created",
"msecs",
"relativeCreated",
"thread",
"threadName",
"processName",
"process",
"raw_id",
"service",
}:
log_entry[key] = value
return json.dumps(log_entry, default=str)
def setup_logger(
service_name: str, level: Optional[str] = None
) -> logging.Logger:
"""
Configures and returns a logger configured with StructuredJsonFormatter on stdout.
"""
if level is None:
from app.common.config import config
level = config.log_level
logger = logging.getLogger(service_name)
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
logger.propagate = False
# Avoid duplicate handlers if setup is called multiple times
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(StructuredJsonFormatter(service_name=service_name))
logger.addHandler(handler)
return logger