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:
3
src/neuronetz_gateway/observability/__init__.py
Normal file
3
src/neuronetz_gateway/observability/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Observability: structured logging and Prometheus metrics."""
|
||||
|
||||
from __future__ import annotations
|
||||
48
src/neuronetz_gateway/observability/logging.py
Normal file
48
src/neuronetz_gateway/observability/logging.py
Normal 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"]
|
||||
Reference in New Issue
Block a user