scaffold: project skeleton, schema, healthz/readyz, CI

Initial project structure for neuronetz-gateway per scope-docs/SPEC.md:

- Python 3.12 / FastAPI / SQLAlchemy 2.0 (async) / Redis / Postgres stack
  managed by uv. Multi-stage non-root Dockerfile, prod + dev compose files
  (ollama service is NEVER published in either), Caddyfile + systemd unit,
  justfile, GitHub Actions CI (ruff, mypy --strict, pytest, bandit, pip-audit).
- Pydantic-Settings config covering every env var from SPEC §7, including the
  MODEL_DISCOVERY_* keys for the dynamic-discovery feature (§4.6).
- Alembic 0001_initial creates the full gateway schema (8 tables, 3 enums,
  notify_key_revoked() trigger), incl. allow_all_models on tenant_limits and
  key_limits for the per-tenant auto-grant toggle.
- Working /healthz, /readyz (fail-closed when deps unreachable), and a
  Prometheus /metrics stub. Sanitizing error handlers that attach X-Request-ID
  to every response and never leak upstream internals.
- SPEC + AGENT_PROMPT included under scope-docs/ (source of truth).
This commit is contained in:
Stephan Berbig
2026-05-26 20:50:35 +02:00
commit d79f17b3bb
32 changed files with 3610 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
"""Observability: structured logging and Prometheus metrics."""
from __future__ import annotations

View File

@@ -0,0 +1,48 @@
"""structlog configuration.
Renders JSON in production (``GATEWAY_LOG_FORMAT=json``) and a human-friendly
console format in development. No secrets are ever logged; processors here
must not introduce any.
"""
from __future__ import annotations
import logging
from typing import Any
import structlog
def configure_logging(level: str = "INFO", fmt: str = "json") -> None:
"""Configure stdlib logging and structlog according to settings."""
log_level = getattr(logging, level.upper(), logging.INFO)
logging.basicConfig(format="%(message)s", level=log_level)
shared_processors: list[structlog.types.Processor] = [
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
]
renderer: structlog.types.Processor
if fmt == "console":
renderer = structlog.dev.ConsoleRenderer()
else:
renderer = structlog.processors.JSONRenderer()
structlog.configure(
processors=[*shared_processors, renderer],
wrapper_class=structlog.make_filtering_bound_logger(log_level),
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
def get_logger(name: str | None = None) -> Any: # noqa: ANN401 - structlog returns a dynamic proxy
"""Return a bound structlog logger."""
return structlog.get_logger(name)
__all__ = ["configure_logging", "get_logger"]