Files
video/server/.venv/lib/python3.11/site-packages/fastapi/telemetry/_api.py
T
2026-10-15 20:36:59 +08:00

281 lines
9.7 KiB
Python

from collections.abc import Callable, Iterator, MutableMapping, Sequence
from contextlib import AbstractContextManager, contextmanager, nullcontext
from dataclasses import dataclass, field
from time import time_ns
from typing import Annotated, Any
from annotated_doc import Doc
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from opentelemetry import context as otel_context
from opentelemetry._logs import Logger, LoggerProvider, SeverityNumber
from opentelemetry.context import Context
from opentelemetry.metrics import MeterProvider
from opentelemetry.trace import Span, StatusCode, Tracer, TracerProvider
from starlette.exceptions import HTTPException, WebSocketException
from starlette.requests import Request
from starlette.websockets import WebSocket, WebSocketDisconnect
from typing_extensions import TypedDict
_HTTP_METHODS = frozenset(
{
"CONNECT",
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT",
"QUERY",
"TRACE",
}
)
class TelemetryConfig(TypedDict, total=False):
"""Optional settings for `FastAPI(telemetry={...})`.
Omitted settings keep their defaults. FastAPI preserves existing providers
and their exporters. Environment setup can add exporters when `auto_configure`
is `True`. Providers supplied by the application are never shut down by FastAPI.
"""
tracer_provider: Annotated[
TracerProvider | None,
Doc("Use this provider instead of the global tracer provider."),
]
meter_provider: Annotated[
MeterProvider | None,
Doc("Use this provider instead of the global meter provider."),
]
logger_provider: Annotated[
LoggerProvider | None,
Doc("Use this provider instead of the global logger provider."),
]
tracing: Annotated[
bool,
Doc("Enable HTTP request and WebSocket connection spans. Defaults to `True`."),
]
metrics: Annotated[bool, Doc("Enable HTTP request metrics. Defaults to `True`.")]
logs: Annotated[
bool,
Doc(
"Record validation failures and unhandled exceptions, including exception messages and stack traces. Defaults to `True`."
),
]
operation_spans: Annotated[
bool,
Doc(
"Trace dependency resolution, endpoints, serialization, and background tasks. Defaults to `True`."
),
]
auto_configure: Annotated[
bool,
Doc(
"Add OTLP exporters from environment variables. Defaults to `False` unless "
"`FASTAPI_OTEL_AUTO_CONFIGURE=true` is set. An explicit value overrides "
"the environment variable."
),
]
exclude: Annotated[
Callable[[MutableMapping[str, Any]], bool] | None,
Doc(
"A function that receives the ASGI scope. Return `True` to skip FastAPI telemetry for that request or connection."
),
]
@dataclass(kw_only=True)
class TelemetryData:
"""Local request or connection data for synchronous OpenTelemetry processors.
FastAPI populates these fields as it handles the request or connection.
Integrations should treat the data as read-only and apply their capture and
redaction settings before exporting any of it.
"""
request: Annotated[
Request | None, Doc("The request, available when its route handler starts.")
] = None
websocket: Annotated[
WebSocket | None,
Doc("The WebSocket, available when its route handler starts."),
] = None
body: Annotated[
Any, Doc("The body read by FastAPI, before parameter validation.")
] = None
values: Annotated[
dict[str, Any] | None,
Doc("The original parsed arguments, available after dependency resolution."),
] = None
errors: Annotated[
Sequence[Any] | None,
Doc("Request validation errors, including the original input values."),
] = None
@dataclass(kw_only=True)
class _RequestTelemetry:
span: Span | None
span_method: str
tracer: Tracer | None = None
logger: Logger | None = None
route: str | None = None
status_code: int | None = None
data: TelemetryData | None = field(default_factory=TelemetryData)
_mount_prefix: str = ""
_exceptions: list[BaseException] = field(default_factory=list, repr=False)
_REQUEST_TELEMETRY_KEY = otel_context.create_key("fastapi.request")
_NO_OPERATION = nullcontext()
def _get_request_telemetry(context: Context | None = None) -> _RequestTelemetry | None:
value = otel_context.get_value(_REQUEST_TELEMETRY_KEY, context)
return value if isinstance(value, _RequestTelemetry) else None
def get_telemetry_data(context: Context | None = None) -> TelemetryData | None:
"""Read local FastAPI data from an OpenTelemetry context.
Defaults to the current context. Log processors can pass `record.context`.
Read the data synchronously while FastAPI handles the request or connection.
After it finishes, this returns `None`, including for retained contexts.
This data is not automatically added to exported spans or logs.
"""
request_telemetry = _get_request_telemetry(context)
return request_telemetry.data if request_telemetry is not None else None
def _validation_failed(
exc: RequestValidationError | WebSocketRequestValidationError,
) -> None:
request_telemetry = _get_request_telemetry()
if request_telemetry is None:
return
if request_telemetry.data is not None:
if isinstance(exc, RequestValidationError):
request_telemetry.data.body = exc.body
request_telemetry.data.errors = exc.errors()
if request_telemetry.logger is not None:
attributes: dict[str, Any] = {
"fastapi.validation.error_count": len(exc.errors())
}
if request_telemetry.route is not None:
attributes["http.route"] = request_telemetry.route
request_telemetry.logger.emit(
event_name="fastapi.validation.failed",
timestamp=time_ns(),
severity_number=SeverityNumber.WARN,
severity_text="WARN",
body="Request validation failed",
attributes=attributes,
)
def _exception_type(exc: BaseException) -> str:
cls = type(exc)
return (
f"{cls.__module__}.{cls.__qualname__}"
if cls.__module__ != "builtins"
else cls.__qualname__
)
def _normal_websocket_disconnect(exc: BaseException) -> bool:
return isinstance(exc, WebSocketDisconnect) and exc.code in (1000, 1001)
def _operation(
*, name: str, function: Callable[..., Any] | None = None
) -> AbstractContextManager[None]:
request_telemetry = _get_request_telemetry()
if request_telemetry is None or request_telemetry.tracer is None:
return _NO_OPERATION
return _traced_operation(
request_telemetry=request_telemetry, name=name, function=function
)
@contextmanager
def _traced_operation(
*,
request_telemetry: _RequestTelemetry,
name: str,
function: Callable[..., Any] | None,
) -> Iterator[None]:
assert request_telemetry.tracer is not None
attributes = {}
if function is not None:
module = getattr(function, "__module__", type(function).__module__)
qualname = getattr(function, "__qualname__", type(function).__qualname__)
attributes["code.function.name"] = f"{module}.{qualname}"
with request_telemetry.tracer.start_as_current_span(
f"fastapi.{name}",
attributes=attributes,
record_exception=False,
set_status_on_exception=False,
) as span:
try:
yield
except Exception as exc:
if name == "background_task" or (
not isinstance(
exc,
(
HTTPException,
RequestValidationError,
WebSocketException,
WebSocketRequestValidationError,
),
)
and not _normal_websocket_disconnect(exc)
):
span.set_attribute("error.type", _exception_type(exc))
span.set_status(StatusCode.ERROR)
raise
def _run_sync_endpoint(
*, function: Callable[..., Any], arguments: dict[str, Any]
) -> Any:
with _operation(name="endpoint", function=function):
return function(**arguments)
def _route_selected(
*, scope: MutableMapping[str, Any], path: str | None, mount: bool = False
) -> None:
request_telemetry = scope.get("fastapi.telemetry")
if not isinstance(request_telemetry, _RequestTelemetry) or path is None:
return
# Mount's path_format ends in /{path}. The child contributes its own path.
if mount:
path = path.removesuffix("/{path}")
request_telemetry._mount_prefix += path
request_telemetry.route = request_telemetry._mount_prefix + "/{path}"
else:
request_telemetry.route = request_telemetry._mount_prefix + path
if request_telemetry.span is not None:
request_telemetry.span.set_attribute("http.route", request_telemetry.route)
request_telemetry.span.update_name(
f"{request_telemetry.span_method} {request_telemetry.route}"
)
_DEFERRED_PROVIDERS = frozenset(
{
("opentelemetry.trace", "ProxyTracerProvider"),
("opentelemetry.metrics._internal", "_ProxyMeterProvider"),
("opentelemetry._logs._internal", "ProxyLoggerProvider"),
}
)
def _unconfigured(provider: Any) -> bool:
# Python's API has no public "is configured" query. Limit this bridge to its
# known deferred providers, never SDK implementations or vendor proxies.
cls = type(provider)
return (cls.__module__, cls.__name__) in _DEFERRED_PROVIDERS