feat(logging): structured JSON logs, uvicorn access log included
Hold Slayer's logs are shipped to Loki by the host's Alloy agent, which reads container stdout. Text lines arrive there as an opaque blob: filtering on a status code meant regex over a formatted string. This adds LOG_FORMAT=json (default "text", so local dev stays readable) rendering one JSON object per line. Two parts were less obvious than a format= argument would suggest, and both are why this is a module rather than a basicConfig tweak: Uvicorn attaches its own handlers to `uvicorn` and `uvicorn.access` with propagate=False, so configuring only the root logger would have left the access log — the highest-volume, most useful stream — as colourised text next to our JSON. configure_logging clears those handlers and re-enables propagation, and is called both at import (for startup config checks) and in lifespan (uvicorn configures itself after importing the app). The __main__ path passes log_config=None so uvicorn never applies its own. The access record's payload lives in record.args as a 5-tuple, not in the message. Formatting it would throw the structure away and force Loki to parse it back out, so the tuple is unpacked into real fields and status_code is emitted as a number for range filtering. Also drops uvicorn's `color_message` extra, an ANSI-coloured duplicate of the message that generic extra-promotion would otherwise copy into every startup line — the same unreadable-in-Grafana problem recently fixed for the lab's Asterisk logs. Verified against a real uvicorn server: 39/39 lines valid JSON, zero ANSI escapes, no duplicates, access lines structured with correct status codes; text mode unchanged. Thread name is included off the main thread, since "which execution context logged this" is the first question when debugging across the asyncio/Sippy/PJSUA2 boundary. SecretStr extras stay masked. README Phase 4 item ticked; LOG_FORMAT and the previously-undocumented LOG_LEVEL added to the config table and .env.example. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
24
main.py
24
main.py
@@ -26,6 +26,7 @@ from api import call_flows, call_history, calls, devices, routing, tokens, webso
|
||||
from auth import get_current_owner, init_jwks_client, is_owner, resolve_from_header_or_query
|
||||
from config import Settings, get_settings
|
||||
from core.gateway import AIPSTNGateway, build_sip_engine
|
||||
from core.logging_config import configure_logging
|
||||
from db.database import close_db, init_db, session_scope
|
||||
from mcp_server.server import create_mcp_server
|
||||
from models.call import CallMode
|
||||
@@ -39,13 +40,12 @@ from services.routing import RoutingService
|
||||
from services.transcription import TranscriptionService
|
||||
from services.tts import TTSService
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
stream=sys.stdout,
|
||||
)
|
||||
# Configure logging at import so anything logged during module import and
|
||||
# startup config checks is formatted. Uvicorn installs its own handlers *after*
|
||||
# importing this module, so `lifespan` calls `configure_logging` again to take
|
||||
# them over — see core/logging_config.py.
|
||||
_startup_settings = get_settings()
|
||||
configure_logging(_startup_settings.log_format, _startup_settings.log_level)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -150,6 +150,12 @@ def _check_startup_config(settings: Settings) -> None:
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Startup: Initialize database, SIP engine, and services."""
|
||||
settings = get_settings()
|
||||
|
||||
# Re-apply: under `uvicorn main:app` the server installs its own handlers on
|
||||
# `uvicorn`/`uvicorn.access` after importing this module, which would emit
|
||||
# colourised text alongside our JSON. This takes them back over.
|
||||
configure_logging(settings.log_format, settings.log_level)
|
||||
|
||||
_check_startup_config(settings)
|
||||
|
||||
# Prefetch Casdoor's JWKS so the first authenticated request doesn't pay
|
||||
@@ -622,4 +628,8 @@ if __name__ == "__main__":
|
||||
port=settings.port,
|
||||
reload=settings.debug,
|
||||
log_level=settings.log_level,
|
||||
# Suppress uvicorn's own dictConfig: ours is installed at import and in
|
||||
# lifespan, and letting uvicorn apply its default would attach a second,
|
||||
# colourised handler — every line twice, one of them not JSON.
|
||||
log_config=None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user