37ccaa4fdc
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.
126 lines
4.0 KiB
Python
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))
|