Stage 0: Repository und Architekturgrundlage

- Pyproject.toml mit FastAPI, Pydantic v2, SQLAlchemy, httpx, asyncio,
  BeautifulSoup4, selectolax, trafilatura, uvicorn, pytest-asyncio
- Multi-stage Dockerfile (Python 3.12-slim, Non-Root-User nsct)
- docker-compose.yml (nsct-api + postgres + optional searxng)
- .env.example mit allen Config-Parametern
- Config-System: AppSettings mit LLMConfig, VisionConfig, AudioConfig,
  DatabaseConfig — komplett aus Environment, keine Hardcodes
- Strukturiertes Logging mit research_id/llm_request_id Tracking
- Pydantic v2 Schemas: SearchQuery, Source, Claim, EvidenceRelation,
  CitationEdge, ResearchReport
- SQLAlchemy 2.0 Declarative Models + async Engine Factory
- SSRF-Schutz: URL-Validation, IP-Blocklist (RFC1918, Cloud Metadata,
  file://, ftp://)
- Provider-Interfaces: LLMProvider, VisionProvider, AudioProvider,
  SearchProvider, ContentFetcher als ABCs
- Health-Endpoints: /health, /ready (LLM-Connect-Test), /providers
- FastAPI App mit CORS, lifespan (LLM Pre-Flight)
- CLI-Stub mit Entry-Points: nsct, nsct-core, nsct-api
- 6 Test-Cases: /health, /ready, /providers + No-Secrets-Test
- Vollständige Dokumentation: README, ARCHITECTURE, SECURITY,
  METHODOLOGY, API, DEPLOYMENT
- .gitignore (Python, Docker, IDE, .env)
This commit is contained in:
NSCT Agent
2026-08-23 11:33:45 +00:00
commit e9410be941
28 changed files with 4192 additions and 0 deletions

126
src/nsct/logging_config.py Normal file
View File

@@ -0,0 +1,126 @@
"""Structured logging with research_id and llm_request_id tracking.
Uses the standard library logging module with a JSON formatter so that every
log record is machine-parseable and traceable across distributed components.
"""
from __future__ import annotations
import json
import logging
import sys
import time
import uuid
from typing import Any
from nsct import __version__
# ---------------------------------------------------------------------------
# Global request-context helpers — all structured log calls pick them up.
# ---------------------------------------------------------------------------
_request_ctx: dict[str, str] = {}
def set_request_ctx(**kwargs: str) -> None:
"""Set (merge) key-value pairs into the per-request context dict."""
_request_ctx.update(kwargs)
def clear_request_ctx() -> None:
"""Clear all per-request context entries."""
_request_ctx.clear()
# ---------------------------------------------------------------------------
# JSON Formatter
# ---------------------------------------------------------------------------
class JSONFormatter(logging.Formatter):
"""Emit log records as a single JSON object per line."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._start_ts = time.monotonic()
# no default format — we build the dict ourselves
default_fmt: str = ""
def format(self, record: logging.LogRecord) -> str:
ts = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created))
elapsed_ms = round((time.monotonic() - self._start_ts) * 1000, 2)
extra = {
"_ts": ts,
"_ms": elapsed_ms,
"_pid": record.process,
"_thread": record.thread,
"_level": record.levelname,
"_module": record.module,
"_function": record.funcName,
"_line": record.lineno,
"_version": __version__,
}
# pull in the request context
for key, val in _request_ctx.items():
extra[f"ctx.{key}"] = val
# merge with standard fields
data: dict[str, Any] = dict(extra)
data["msg"] = record.getMessage()
data["name"] = record.name
# exception info
if record.exc_info and record.exc_info[0] is not None:
data["exc_info"] = self.formatException(record.exc_info)
return json.dumps(data, default=str, ensure_ascii=False)
# ---------------------------------------------------------------------------
# Bootstrap
# ---------------------------------------------------------------------------
def setup_logging(
level: str = "INFO",
*,
json_format: bool = True,
) -> None:
"""Configure the root logger with a single console handler.
Args:
level: Log level string (DEBUG, INFO, WARNING, ERROR, CRITICAL).
json_format: If True, use JSONFormatter; else use a human-friendly format.
"""
log_level = getattr(logging, level.upper(), logging.INFO)
root = logging.getLogger()
root.setLevel(log_level)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(log_level)
if json_format:
handler.setFormatter(JSONFormatter())
else:
handler.setFormatter(
logging.Formatter(
"%(asctime)s [%(levelname)-8s] %(name)s:%(funcName)s:%(lineno)d%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
)
root.addHandler(handler)
# ---------------------------------------------------------------------------
# Convenience logger factory
# ---------------------------------------------------------------------------
def get_logger(name: str = __name__) -> logging.Logger:
"""Return a pre-configured child logger."""
return logging.getLogger(name)