mirror of
https://github.com/karpathy/nanochat.git
synced 2026-05-25 00:58:12 +00:00
- FastAPI service that manages conversations and messages in PostgreSQL
(SQLAlchemy 2.0 async + asyncpg) and streams assistant responses back
to the client via sse-starlette, forwarding the inference service SSE
contract unchanged.
- Auth guard validates every request against the auth service
/auth/validate endpoint (X-Internal-API-Key) and caches results in an
in-process TTL cache (5 min, 1024 entries) to absorb request bursts.
- Every query filters by authenticated user_id; cross-user access
returns 404. Message send flow auto-titles the first message,
persists the streamed assistant response after the client disconnects,
and records token_count + inference_time_ms.
- /api/models{,/swap} proxies the inference admin surface; swap
requires is_admin on the validated user.
- Structured JSON logging via structlog with trace_id + user_id
ContextVars attached to every log line.
- Test suite (pytest + aiosqlite + respx) covers CRUD, user scoping,
streaming SSE persistence, regenerate, model proxy admin gate,
and the stream proxy error path. 16/16 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
1.8 KiB
Python
75 lines
1.8 KiB
Python
"""Structured JSON logging for the chat API service."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
import uuid
|
|
from contextvars import ContextVar
|
|
|
|
import structlog
|
|
|
|
from .config import get_settings
|
|
|
|
_trace_id_ctx: ContextVar[str | None] = ContextVar("trace_id", default=None)
|
|
_user_id_ctx: ContextVar[str | None] = ContextVar("user_id", default=None)
|
|
|
|
|
|
def set_trace_id(trace_id: str | None) -> None:
|
|
_trace_id_ctx.set(trace_id)
|
|
|
|
|
|
def set_user_id(user_id: str | None) -> None:
|
|
_user_id_ctx.set(user_id)
|
|
|
|
|
|
def get_trace_id() -> str | None:
|
|
return _trace_id_ctx.get()
|
|
|
|
|
|
def get_user_id() -> str | None:
|
|
return _user_id_ctx.get()
|
|
|
|
|
|
def new_trace_id() -> str:
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
def _inject_context(_logger, _method, event_dict):
|
|
event_dict.setdefault("service", "chat-api")
|
|
trace_id = _trace_id_ctx.get()
|
|
if trace_id is not None:
|
|
event_dict.setdefault("trace_id", trace_id)
|
|
user_id = _user_id_ctx.get()
|
|
if user_id is not None:
|
|
event_dict.setdefault("user_id", user_id)
|
|
return event_dict
|
|
|
|
|
|
def configure_logging() -> None:
|
|
settings = get_settings()
|
|
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
|
|
logging.basicConfig(
|
|
format="%(message)s",
|
|
stream=sys.stdout,
|
|
level=level,
|
|
force=True,
|
|
)
|
|
|
|
structlog.configure(
|
|
processors=[
|
|
structlog.contextvars.merge_contextvars,
|
|
structlog.processors.add_log_level,
|
|
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
_inject_context,
|
|
structlog.processors.JSONRenderer(),
|
|
],
|
|
wrapper_class=structlog.make_filtering_bound_logger(level),
|
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
cache_logger_on_first_use=True,
|
|
)
|
|
|
|
|
|
def get_logger(name: str | None = None):
|
|
return structlog.get_logger(name)
|