نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""Per-request correlation and actor context.
|
||||
|
||||
Every request gets an id before it reaches a route. That id is returned in the
|
||||
``X-Request-ID`` response header, included in every error body, and written onto
|
||||
every error record — so a user reporting "it broke" can hand over one string
|
||||
that points at exactly one row in the error console.
|
||||
|
||||
The same object carries the resolved tenant and user. Authentication happens in
|
||||
dependencies, long after the middleware has run, so the middleware seeds the
|
||||
context with what it knows (method, path, ip, user agent) and the auth
|
||||
dependencies fill in the rest via :func:`note_actor`. Error handlers then read
|
||||
one object instead of re-deriving anything.
|
||||
|
||||
Deliberately stored on ``request.state`` rather than in a ``contextvar``: it is
|
||||
the same object the handler already has in hand, it survives every task
|
||||
boundary Starlette may introduce, and it cannot leak between requests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.responses import Response
|
||||
|
||||
#: Header used to both accept a caller-supplied id and echo it back. Accepting
|
||||
#: an upstream id matters when the API sits behind a reverse proxy or a
|
||||
#: frontend BFF that already correlates its own logs.
|
||||
REQUEST_ID_HEADER = "X-Request-ID"
|
||||
|
||||
#: Long enough to be unique, short enough to paste into a bug report.
|
||||
REQUEST_ID_LENGTH = 16
|
||||
|
||||
_STATE_ATTR = "error_context"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RequestContext:
|
||||
"""What we know about the request an error happened on."""
|
||||
|
||||
request_id: str
|
||||
method: str
|
||||
path: str
|
||||
ip: str = ""
|
||||
user_agent: str = ""
|
||||
#: Filled in by the auth dependencies once the caller is authenticated.
|
||||
organization_id: str | None = None
|
||||
user_id: str | None = None
|
||||
#: Set when a failure happens inside a specific agent run.
|
||||
run_id: str | None = None
|
||||
#: Query string, kept separate from ``path`` so fingerprints group across
|
||||
#: different filters on the same endpoint.
|
||||
query: str = ""
|
||||
|
||||
|
||||
def new_request_id() -> str:
|
||||
return uuid.uuid4().hex[:REQUEST_ID_LENGTH]
|
||||
|
||||
|
||||
def client_ip(request: Request) -> str:
|
||||
"""Real client address, honouring one proxy hop.
|
||||
|
||||
Same rule as ``deps.client_meta``: a trusted reverse proxy sets
|
||||
``X-Forwarded-For``, and the left-most entry is the original caller.
|
||||
"""
|
||||
forwarded = request.headers.get("x-forwarded-for", "")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
return request.client.host if request.client else ""
|
||||
|
||||
|
||||
def context_of(request: Request) -> RequestContext | None:
|
||||
"""The context for this request, or ``None`` outside the middleware."""
|
||||
return getattr(request.state, _STATE_ATTR, None)
|
||||
|
||||
|
||||
def note_actor(
|
||||
request: Request,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
organization_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> None:
|
||||
"""Record who/which tenant this request belongs to.
|
||||
|
||||
Called from the auth dependencies. Silent when there is no context (a
|
||||
direct unit-test call to a dependency, for instance) — correlation is
|
||||
telemetry, and telemetry must never be the reason a request fails.
|
||||
"""
|
||||
context = context_of(request)
|
||||
if context is None:
|
||||
return
|
||||
if user_id:
|
||||
context.user_id = user_id
|
||||
if organization_id:
|
||||
context.organization_id = organization_id
|
||||
if run_id:
|
||||
context.run_id = run_id
|
||||
|
||||
|
||||
class RequestContextMiddleware(BaseHTTPMiddleware):
|
||||
"""Attach a :class:`RequestContext` to every request and echo its id."""
|
||||
|
||||
async def dispatch(
|
||||
self, request: Request, call_next: RequestResponseEndpoint
|
||||
) -> Response:
|
||||
incoming = request.headers.get(REQUEST_ID_HEADER, "").strip()
|
||||
# Cap the accepted value: an unbounded header would let a caller write
|
||||
# arbitrarily long ids into the error table.
|
||||
request_id = incoming[:64] if incoming else new_request_id()
|
||||
|
||||
setattr( # noqa: B010 - the attribute name is a module constant
|
||||
request.state,
|
||||
_STATE_ATTR,
|
||||
RequestContext(
|
||||
request_id=request_id,
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
query=request.url.query or "",
|
||||
ip=client_ip(request),
|
||||
user_agent=request.headers.get("user-agent", "")[:255],
|
||||
),
|
||||
)
|
||||
response = await call_next(request)
|
||||
response.headers[REQUEST_ID_HEADER] = request_id
|
||||
return response
|
||||
|
||||
|
||||
__all__ = [
|
||||
"REQUEST_ID_HEADER",
|
||||
"RequestContext",
|
||||
"RequestContextMiddleware",
|
||||
"client_ip",
|
||||
"context_of",
|
||||
"new_request_id",
|
||||
"note_actor",
|
||||
]
|
||||
Reference in New Issue
Block a user