Create OpenTelemetry server spans for incoming HTTP requests. This middleware is experimental. Its API and emitted telemetry may change in minor releases without a deprecation period. Args: app: The ASGI application to wrap. excluded_urls: Regular expressions matched ag
| 16 | |
| 17 | |
| 18 | class OpenTelemetryMiddleware: |
| 19 | """Create OpenTelemetry server spans for incoming HTTP requests. |
| 20 | |
| 21 | This middleware is experimental. Its API and emitted telemetry may change in minor |
| 22 | releases without a deprecation period. |
| 23 | |
| 24 | Args: |
| 25 | app: The ASGI application to wrap. |
| 26 | excluded_urls: Regular expressions matched against the full request URL. |
| 27 | Pass a comma-separated string or a sequence. By default, exclude no URLs. |
| 28 | tracer_provider: Optional tracer provider. If omitted, use the global tracer provider. |
| 29 | meter_provider: Optional meter provider. If omitted, use the global meter provider. |
| 30 | """ |
| 31 | |
| 32 | def __init__( |
| 33 | self, |
| 34 | app: ASGIApp, |
| 35 | *, |
| 36 | excluded_urls: str | Sequence[str] = (), |
| 37 | tracer_provider: trace.TracerProvider | None = None, |
| 38 | meter_provider: metrics.MeterProvider | None = None, |
| 39 | ) -> None: |
| 40 | self.app = app |
| 41 | self._meter_provider = meter_provider |
| 42 | if isinstance(excluded_urls, str): |
| 43 | excluded_urls = [pattern.strip() for pattern in excluded_urls.split(",")] if excluded_urls else () |
| 44 | self._excluded_urls = tuple(re.compile(pattern) for pattern in excluded_urls) |
| 45 | self._tracer_provider = tracer_provider or trace.get_tracer_provider() |
| 46 | |
| 47 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: |
| 48 | if scope["type"] != "http" or scope.get("starlette.opentelemetry"): |
| 49 | return await self.app(scope, receive, send) |
| 50 | |
| 51 | scope["starlette.opentelemetry"] = True |
| 52 | try: |
| 53 | tracer_provider = self._tracer_provider |
| 54 | if isinstance(tracer_provider, trace.NoOpTracerProvider): |
| 55 | return await self.app(scope, receive, send) |
| 56 | |
| 57 | url = URL(scope=scope) |
| 58 | if any(pattern.search(str(url)) for pattern in self._excluded_urls): |
| 59 | return await self.app(scope, receive, send) |
| 60 | |
| 61 | original_method = scope.get("method", "") |
| 62 | method = original_method.upper() |
| 63 | |
| 64 | headers: dict[str, list[str]] = {} |
| 65 | for name, value in scope.get("headers", []): |
| 66 | headers.setdefault(name.decode("latin-1").lower(), []).append(value.decode("latin-1")) |
| 67 | |
| 68 | attributes: dict[str, str | int] = { |
| 69 | "http.request.method": method, |
| 70 | "url.path": scope.get("path", ""), |
| 71 | "url.scheme": scope.get("scheme", "http"), |
| 72 | } |
| 73 | if original_method != method: |
| 74 | attributes["http.request.method_original"] = original_method |
| 75 | if url.query: |
no outgoing calls