fastapi/telemetry/_asgi.py PYTHON 458 lines View on github.com → Search inside
1import os2from contextlib import nullcontext3from time import perf_counter, time_ns4from typing import Any5from urllib.parse import parse_qsl, urlencode, urlsplit67from fastapi import __version__8from fastapi.telemetry._api import (9    _HTTP_METHODS,10    _REQUEST_TELEMETRY_KEY,11    TelemetryConfig,12    _exception_type,13    _normal_websocket_disconnect,14    _RequestTelemetry,15    _unconfigured,16)17from opentelemetry import _logs, metrics, propagate, trace18from opentelemetry import context as otel_context19from opentelemetry._logs import SeverityNumber20from opentelemetry.metrics import Histogram, UpDownCounter21from opentelemetry.propagators.textmap import Getter22from opentelemetry.trace import SpanKind, StatusCode23from starlette.datastructures import Headers24from starlette.types import ASGIApp, Message, Receive, Scope, Send2526_DURATION_BUCKETS = (27    0.005,28    0.01,29    0.025,30    0.05,31    0.075,32    0.1,33    0.25,34    0.5,35    0.75,36    1.0,37    2.5,38    5.0,39    7.5,40    10.0,41)42_SCHEMA_URL = "https://opentelemetry.io/schemas/1.44.0"43# https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-span44_SENSITIVE_QUERY_PARAMETERS = frozenset(45    {46        "X-Amz-Signature",47        "X-Amz-Credential",48        "X-Amz-Security-Token",49        "sig",50        "X-Goog-Signature",51    }52)535455class _HeadersGetter(Getter[Headers]):56    def get(self, carrier: Headers, key: str) -> list[str] | None:57        values = carrier.getlist(key)58        if not values:59            return None60        if key.lower() == "baggage":61            # The baggage propagator reads only the first value.62            return [",".join(values)]63        return values6465    def keys(self, carrier: Headers) -> list[str]:66        return carrier.keys()676869_HEADERS_GETTER = _HeadersGetter()707172def _legacy_otel(stack: Any) -> bool:73    # Test the built stack, not contrib's flag: that flag also exists when its74    # stack patch failed, or when instrument_app was called after stack creation.75    seen: set[int] = set()76    while stack is not None and id(stack) not in seen:77        seen.add(id(stack))78        if (79            type(stack).__module__ == "opentelemetry.instrumentation.asgi"80            and type(stack).__name__ == "OpenTelemetryMiddleware"81        ):82            return True83        stack = getattr(stack, "app", None)84    return False858687def _exception(*, scope: Scope, exc: BaseException) -> None:88    if scope["type"] == "websocket" and _normal_websocket_disconnect(exc):89        return90    request_telemetry = scope.get("fastapi.telemetry")91    if isinstance(request_telemetry, _RequestTelemetry):92        seen = request_telemetry._exceptions93        if not any(previous is exc for previous in seen):94            seen.append(exc)95            if request_telemetry.logger is not None and isinstance(exc, Exception):96                attributes = (97                    {"http.route": request_telemetry.route}98                    if request_telemetry.route99                    else None100                )101                request_telemetry.logger.emit(102                    exception=exc,103                    event_name="fastapi.websocket.exception"104                    if scope["type"] == "websocket"105                    else "http.server.request.exception",106                    timestamp=time_ns(),107                    severity_number=SeverityNumber.ERROR,108                    severity_text="ERROR",109                    body="Unhandled exception in FastAPI WebSocket connection"110                    if scope["type"] == "websocket"111                    else "Unhandled exception in FastAPI request",112                    attributes=attributes,113                )114115116def _server_attributes(scope: Scope) -> dict[str, Any]:117    # ASGI exposes HTTP/2 and HTTP/3 authority as Host. Proxy normalization is118    # handled by the server or middleware, as it is for the request URL.119    authority = Headers(scope=scope).get("host")120    if authority is not None:121        try:122            url = urlsplit("//" + authority)123            address, port = url.hostname, url.port124        except ValueError:125            # Malformed authority must not prevent handling the request.126            return {}127        if url.username is not None or url.path or url.query or url.fragment:128            return {}129        if port is None:130            port = {"http": 80, "https": 443, "ws": 80, "wss": 443}.get(131                scope.get("scheme", "http")132            )133    else:134        address, port = scope.get("server") or (None, None)135    if address is None:136        return {}137    attributes: dict[str, Any] = {"server.address": address}138    if port is not None:139        attributes["server.port"] = port140    return attributes141142143class ExceptionTelemetryMiddleware:144    """Observe exceptions before error handlers send and complete a response."""145146    def __init__(self, app: ASGIApp) -> None:147        self.app = app148149    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:150        try:151            await self.app(scope, receive, send)152        except Exception as exc:153            _exception(scope=scope, exc=exc)154            raise155156157class NativeTelemetry:158    def __init__(self, config: TelemetryConfig) -> None:159        self.config = config160        known_methods = os.getenv("OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS", "").strip()161        self._known_methods = (162            frozenset(163                method.strip() for method in known_methods.split(",") if method.strip()164            )165            if known_methods166            else _HTTP_METHODS167        )168        self._provider: metrics.MeterProvider | None = None169        self._duration: Histogram | None = None170        self._active: UpDownCounter | None = None171        self._tracer_provider: trace.TracerProvider | None = None172        self._tracer: trace.Tracer | None = None173174    def enabled(self) -> bool:175        config = self.config176        return bool(177            (178                config["tracing"]179                and (180                    config["tracer_provider"] is not None181                    or not _unconfigured(trace.get_tracer_provider())182                )183            )184            or (185                config["metrics"]186                and (187                    config["meter_provider"] is not None188                    or not _unconfigured(metrics.get_meter_provider())189                )190            )191            or (192                config["logs"]193                and (194                    config["logger_provider"] is not None195                    or not _unconfigured(_logs.get_logger_provider())196                )197            )198        )199200    def _instruments(self) -> tuple[Histogram, UpDownCounter]:201        provider = self.config["meter_provider"] or metrics.get_meter_provider()202        if provider is not self._provider:203            meter = provider.get_meter("fastapi", __version__, schema_url=_SCHEMA_URL)204            self._duration = meter.create_histogram(205                "http.server.request.duration",206                unit="s",207                description="Duration of HTTP server requests.",208                explicit_bucket_boundaries_advisory=_DURATION_BUCKETS,209            )210            self._active = meter.create_up_down_counter(211                "http.server.active_requests",212                unit="{request}",213                description="Number of active HTTP server requests.",214            )215            self._provider = provider216        assert self._duration is not None and self._active is not None217        return self._duration, self._active218219    def _get_tracer(self, *, provider: trace.TracerProvider) -> trace.Tracer:220        if provider is not self._tracer_provider:221            self._tracer = provider.get_tracer(222                "fastapi", __version__, schema_url=_SCHEMA_URL223            )224            self._tracer_provider = provider225        assert self._tracer is not None226        return self._tracer227228    async def __call__(229        self,230        *,231        app: ASGIApp,232        scope: Scope,233        receive: Receive,234        send: Send,235        legacy_otel: bool = False,236    ) -> None:237        config = self.config238        if config["exclude"] is not None and config["exclude"](scope):239            # Keep mounted FastAPI apps from observing this excluded request.240            scope["fastapi.telemetry"] = None241            # An in-process request can inherit another request's telemetry.242            token = otel_context.attach(243                otel_context.set_value(_REQUEST_TELEMETRY_KEY, None)244            )245            try:246                await app(scope, receive, send)247            finally:248                otel_context.detach(token)249                scope.pop("fastapi.telemetry", None)250            return251        tracing = config["tracing"] and not legacy_otel252        logging = config["logs"] and not legacy_otel253        if logging:254            logger_provider = config["logger_provider"] or _logs.get_logger_provider()255            logging = not (256                (config["logger_provider"] is None and _unconfigured(logger_provider))257                or isinstance(logger_provider, _logs.NoOpLoggerProvider)258            )259        is_websocket = scope["type"] == "websocket"260        metering = config["metrics"] and not legacy_otel and not is_websocket261        if tracing:262            provider = config["tracer_provider"] or trace.get_tracer_provider()263            tracing = not (264                (config["tracer_provider"] is None and _unconfigured(provider))265                or isinstance(provider, trace.NoOpTracerProvider)266            )267        if metering:268            meter_provider = config["meter_provider"] or metrics.get_meter_provider()269            metering = not (270                (config["meter_provider"] is None and _unconfigured(meter_provider))271                or isinstance(meter_provider, metrics.NoOpMeterProvider)272            )273        if not tracing and not metering and not logging:274            await app(scope, receive, send)275            return276        original_method = scope.get("method", "")277        method = original_method if original_method in self._known_methods else "_OTHER"278        span_method = "WS" if is_websocket else "HTTP" if method == "_OTHER" else method279        attributes: dict[str, Any] = {280            "url.scheme": scope.get("scheme", "ws" if is_websocket else "http"),281        }282        if is_websocket:283            attributes["network.protocol.name"] = "websocket"284        else:285            attributes["http.request.method"] = method286            if scope.get("http_version"):287                attributes["network.protocol.version"] = scope["http_version"]288        duration, active = self._instruments() if metering else (None, None)289        active_attributes = {290            k: v for k, v in attributes.items() if k != "network.protocol.version"291        }292        if active is not None:293            active.add(1, active_attributes)294        started = perf_counter()295        span = None296        tracer = None297        parent_token = None298        if tracing:299            parent = propagate.extract(Headers(scope=scope), getter=_HEADERS_GETTER)300            parent_token = otel_context.attach(parent)301            tracer = self._get_tracer(provider=provider)302            span_attributes = {**attributes, **_server_attributes(scope)}303            if not is_websocket:304                span_attributes["url.path"] = scope["path"]305                if scope.get("query_string"):306                    span_attributes["url.query"] = urlencode(307                        [308                            (309                                key,310                                "REDACTED"311                                if key in _SENSITIVE_QUERY_PARAMETERS312                                else value,313                            )314                            for key, value in parse_qsl(315                                scope["query_string"].decode("latin-1"),316                                keep_blank_values=True,317                            )318                        ]319                    )320                if original_method != method:321                    span_attributes["http.request.method_original"] = original_method322            span = tracer.start_span(323                span_method,324                context=parent,325                kind=SpanKind.SERVER,326                attributes=span_attributes,327            )328        request_telemetry = _RequestTelemetry(329            span=span,330            span_method=span_method,331            tracer=tracer if config["operation_spans"] else None,332            logger=_logs.get_logger(333                "fastapi",334                __version__,335                config["logger_provider"],336                schema_url=_SCHEMA_URL,337            )338            if logging339            else None,340            _mount_prefix=scope.get("app_root_path", scope.get("root_path", "")).rstrip(341                "/"342            ),343        )344        scope["fastapi.telemetry"] = request_telemetry345        token = otel_context.attach(346            otel_context.set_value(_REQUEST_TELEMETRY_KEY, request_telemetry)347        )348        finished = False349        trailers = False350        disconnected = False351352        def finish(error: BaseException | None = None) -> None:353            nonlocal finished354            if finished:355                return356            finished = True357            if request_telemetry.route is not None:358                attributes["http.route"] = request_telemetry.route359            if request_telemetry.status_code is not None:360                attributes["http.response.status_code"] = request_telemetry.status_code361            failed = (362                error is not None363                or not is_websocket364                and (365                    request_telemetry.status_code is None366                    or request_telemetry.status_code >= 500367                )368            )369            if failed:370                errors = request_telemetry._exceptions371                failure = error or (errors[-1] if errors else None)372                attributes["error.type"] = (373                    _exception_type(failure)374                    if failure375                    else str(request_telemetry.status_code or "incomplete_response")376                )377            if span is not None:378                span.set_attributes(attributes)379                if failed:380                    span.set_status(StatusCode.ERROR)381            # Record while the span is current, so exemplars can correlate it.382            if duration is not None:383                duration.record(max(0, perf_counter() - started), attributes)384            if active is not None:385                active.add(-1, active_attributes)386            if span is not None:387                span.end()388389        async def wrapped_send(message: Message) -> None:390            nonlocal trailers391            if message["type"] == "http.response.start":392                request_telemetry.status_code = message["status"]393                trailers = message.get("trailers", False)394            await send(message)395            if (396                (397                    message["type"] == "http.response.body"398                    and not message.get("more_body", False)399                    and not trailers400                )401                or (402                    message["type"] == "http.response.trailers"403                    and not message.get("more_trailers", False)404                )405                or message["type"] == "http.response.pathsend"406            ):407                finish()408409        async def wrapped_receive() -> Message:410            nonlocal disconnected411            message = await receive()412            if message["type"] == "http.disconnect":413                disconnected = True414            return message415416        context = (417            trace.use_span(418                span,419                end_on_exit=False,420                record_exception=False,421                set_status_on_exception=False,422            )423            if span is not None424            else nullcontext()425        )426        try:427            with context:428                try:429                    await app(430                        scope,431                        receive if is_websocket else wrapped_receive,432                        send if is_websocket else wrapped_send,433                    )434                except BaseException as exc:435                    _exception(scope=scope, exc=exc)436                    finish(437                        None438                        if is_websocket and _normal_websocket_disconnect(exc)439                        else exc440                    )441                    raise442                finally:443                    if not finished:444                        finish(445                            None446                            if is_websocket447                            else ConnectionError("Client disconnected")448                            if disconnected449                            else RuntimeError("Incomplete ASGI response")450                        )451        finally:452            request_telemetry.data = None453            request_telemetry._exceptions.clear()454            otel_context.detach(token)455            scope.pop("fastapi.telemetry", None)456            if parent_token is not None:457                otel_context.detach(parent_token)

Code quality findings 6

Ensure functions have docstrings for documentation
info maintainability missing-docstring
def get(self, carrier: Headers, key: str) -> list[str] | None:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def keys(self, carrier: Headers) -> list[str]:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def enabled(self) -> bool:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
def finish(error: BaseException | None = None) -> None:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
async def wrapped_send(message: Message) -> None:
Ensure functions have docstrings for documentation
info maintainability missing-docstring
async def wrapped_receive() -> Message:

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.

Generated by searchcode.com · powered by code_get_file + code_get_findings