Livrare LOT 1 - Didi

This commit is contained in:
Dezvoltari Evotech 2026-06-25 14:13:25 -07:00
commit 5380c3fc63
990 changed files with 133308 additions and 0 deletions

View file

@ -0,0 +1,92 @@
"""Per-request middleware that emits events to the dashboard sink.
Captures: request_id (X-Request-ID header or generated UUID), endpoint
(URL path), HTTP method, duration, status_code. Skips /health and noisy
internal paths. Endpoint string is normalized to the route template
(no params) so a high-cardinality table doesn't blow up.
"""
from __future__ import annotations
import time
import uuid
from typing import Awaitable, Callable
from starlette.requests import Request
from starlette.responses import Response
from brain_api.events.sink import get_global_sink
# Endpoints we don't want to record — too noisy / not interesting in History.
_SKIP_PREFIXES = (
"/health",
"/docs",
"/redoc",
"/openapi.json",
"/metrics",
)
def _normalize_endpoint(path: str) -> str:
"""Collapse path params so we don't blow up the request_history table.
/v1/analysis_atom/123 → /v1/analysis_atom/{id}
/v1/fact_status/45/versions → /v1/fact_status/{id}/versions
/v1/verification_cache/abc/free → /v1/verification_cache/{hash}/{tier}
"""
parts = path.split("/")
if len(parts) >= 4 and parts[1] == "v1" and parts[2] == "analysis_atom":
if len(parts) == 4 and parts[3].isdigit():
return "/v1/analysis_atom/{id}"
if len(parts) >= 4 and parts[1] == "v1" and parts[2] == "fact_status":
if parts[3].isdigit():
tail = "/" + "/".join(parts[4:]) if len(parts) > 4 else ""
return f"/v1/fact_status/{{id}}{tail}"
if (
len(parts) >= 5
and parts[1] == "v1"
and parts[2] == "verification_cache"
and parts[4] in ("free", "premium")
):
return "/v1/verification_cache/{hash}/{tier}"
return path
async def event_emit_middleware(
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Starlette HTTP middleware: emit a dashboard event per request."""
path = request.url.path
if any(path.startswith(p) for p in _SKIP_PREFIXES):
return await call_next(request)
request_id = request.headers.get("x-request-id") or uuid.uuid4().hex[:32]
started = time.monotonic()
status_code = 500
error: str | None = None
try:
response = await call_next(request)
status_code = response.status_code
return response
except Exception as e: # noqa: BLE001
error = f"{type(e).__name__}: {e}"
raise
finally:
duration_ms = int((time.monotonic() - started) * 1000)
sink = get_global_sink()
if sink is not None and sink.enabled:
sink.emit(
{
"request_id": request_id,
"tier": "n/a",
"endpoint": _normalize_endpoint(path),
# Brain has no upstream "provider" concept — it resolves
# locally (PG + atomic + LLM router). Leaving null keeps
# the column UI honest (renders as "—").
"provider": None,
"duration_ms": duration_ms,
"status_code": status_code,
"error": error,
}
)

View file

@ -0,0 +1,125 @@
"""Async event sink that forwards brain request events to the AI platform dashboard.
Fire-and-forget — dashboard outages must never affect brain latency or
availability. Pattern mirrors web-api/events/sink.py 1:1, with module='brain'
baked in so events are distinguishable in the unified Insights/History view.
"""
from __future__ import annotations
import asyncio
import contextlib
import logging
from typing import Any
import httpx
logger = logging.getLogger("brain_api.events.sink")
class DashboardEventSink:
"""POSTs request events to the dashboard /api/ingest/event endpoint.
The sink runs all writes through a bounded queue processed by a single
worker task. Overflow drops oldest. Failures are logged but swallowed.
"""
def __init__(
self,
dashboard_url: str | None,
token: str | None = None,
queue_size: int = 1000,
request_timeout: float = 5.0,
) -> None:
self.dashboard_url = dashboard_url.rstrip("/") if dashboard_url else None
self.token = token
self._queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(maxsize=queue_size)
self._client: httpx.AsyncClient | None = None
self._worker_task: asyncio.Task[None] | None = None
self._request_timeout = request_timeout
self._enabled = bool(dashboard_url)
@property
def enabled(self) -> bool:
return self._enabled
async def start(self) -> None:
if not self._enabled:
logger.info("brain DashboardEventSink disabled (no dashboard URL)")
return
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(
connect=2.0, read=self._request_timeout, write=2.0, pool=5.0
),
limits=httpx.Limits(max_connections=5, max_keepalive_connections=2),
)
self._worker_task = asyncio.create_task(self._worker())
logger.info("brain DashboardEventSink started → %s", self.dashboard_url)
async def stop(self) -> None:
if self._worker_task is not None:
self._worker_task.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await self._worker_task
self._worker_task = None
if self._client is not None and not self._client.is_closed:
await self._client.aclose()
self._client = None
def emit(self, event: dict[str, Any]) -> None:
"""Enqueue an event for async send. Never raises.
The caller does NOT have to set 'module' — we stamp it here so all
events from this process are tagged 'brain' regardless of who called.
"""
if not self._enabled:
return
event = {**event, "module": "brain"}
try:
self._queue.put_nowait(event)
except asyncio.QueueFull:
# Drop the oldest event to make room for the new one.
try:
_ = self._queue.get_nowait()
self._queue.put_nowait(event)
except Exception: # noqa: BLE001
pass
async def _worker(self) -> None:
while True:
try:
event = await self._queue.get()
except asyncio.CancelledError:
raise
try:
await self._send(event)
except Exception as e: # noqa: BLE001
logger.debug("Event dropped (%s): %s", type(e).__name__, e)
finally:
self._queue.task_done()
async def _send(self, event: dict[str, Any]) -> None:
if self._client is None or self.dashboard_url is None:
return
headers = {"Content-Type": "application/json"}
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
resp = await self._client.post(
f"{self.dashboard_url}/api/ingest/event",
json=event,
headers=headers,
)
resp.raise_for_status()
# Module-level singleton — initialized in app.py lifespan.
_sink: DashboardEventSink | None = None
def init_global_sink(sink: DashboardEventSink) -> None:
global _sink
_sink = sink
def get_global_sink() -> DashboardEventSink | None:
return _sink