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