Files
headquarter/apps/api/src/logging_config.py
T
alex 37ccaa4fdc refactor: organize API routers and services into subpackages
Service organization (19 files moved into 6 subpackages):
- services/instance/ — event_bus, health_monitor, lifecycle_hooks
- services/config/ — config_profile_resolver
- services/git/ — clone, git_operations, git_service
- services/build/ — docker_build, manifest_compiler
- services/terminal/ — terminal_manager, terminal_session
- services/shared/ — correlation, file_service, notification_service,
  permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager

API router organization (16 files moved into 6 subpackages):
- api/tool/ — tool_instances, tool_types, tool_definitions,
  tool_types_validation, sessions (extracted from tool_instances)
- api/config/ — config_profiles, user_config
- api/workspace/ — workspaces, workspace_files, workspace_git,
  workspace_instances
- api/user/ — users, auth, ssh_keys
- api/project/ — projects, git_repositories
- api/system/ — health, events, notifications, dashboard, terminal,
  instance_proxy

Updated main.py imports and all __init__.py re-exports.
Sessions router extracted from tool_instances.py into api/tool/sessions.py.

Quality gates: py_compile passed, ruff passed.
2026-06-04 12:24:14 +02:00

126 lines
4.0 KiB
Python

"""Structured JSON logging configuration."""
import json
import logging
import sys
import time
import traceback
from collections.abc import Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from src.services.shared.correlation import get_correlation_id
logger = logging.getLogger(__name__)
class CorrelationIdFilter(logging.Filter):
"""Inject correlation_id into every log record from context var."""
def filter(self, record: logging.LogRecord) -> bool:
record.correlation_id = get_correlation_id() # type: ignore[attr-defined]
return True
class JSONFormatter(logging.Formatter):
"""Emit log records as single-line JSON."""
def format(self, record: logging.LogRecord) -> str:
log_obj: dict = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"correlation_id": getattr(record, "correlation_id", None),
}
# Optional extra fields
for key in ("instance_id", "event_type"):
value = getattr(record, key, None)
if value is not None:
log_obj[key] = value
if record.exc_info:
log_obj["exception"] = self.formatException(record.exc_info)
return json.dumps(log_obj, default=str)
def formatTime(self, record: logging.LogRecord, datefmt: str | None = None) -> str:
return time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created))
class RequestLoggingMiddleware(BaseHTTPMiddleware):
"""Log all HTTP requests with timing and status codes."""
async def dispatch(self, request: Request, call_next: Callable) -> Response:
start_time = time.time()
client_host = request.client.host if request.client else "unknown"
logger.info(
"→ Request: %s %s (client: %s)",
request.method,
request.url.path,
client_host,
)
try:
response = await call_next(request)
duration = time.time() - start_time
logger.info(
"← Response: %s %s%d (%dms)",
request.method,
request.url.path,
response.status_code,
int(duration * 1000),
)
return response
except Exception as exc:
duration = time.time() - start_time
logger.error(
"✗ Error: %s %s%s (%dms)\n%s",
request.method,
request.url.path,
type(exc).__name__,
int(duration * 1000),
traceback.format_exc(),
)
raise
class ExceptionLoggingMiddleware(BaseHTTPMiddleware):
"""Catch and log all unhandled exceptions."""
async def dispatch(self, request: Request, call_next: Callable) -> Response:
try:
return await call_next(request)
except Exception:
logger.critical(
"Unhandled exception in %s %s:\n%s",
request.method,
request.url.path,
traceback.format_exc(),
)
raise
def configure_logging(level: int = logging.INFO) -> None:
"""Configure structured JSON logging for the application."""
formatter = JSONFormatter()
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
console_handler.addFilter(CorrelationIdFilter())
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(level)
root_logger.handlers = [console_handler]
# Set levels for specific loggers
logging.getLogger("uvicorn").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
logger.info("Logging configured at level %s", logging.getLevelName(level))