add
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
|
||||
try:
|
||||
from opentelemetry import metrics, propagate, trace
|
||||
from opentelemetry.trace import SpanKind, Status, StatusCode
|
||||
except ImportError: # pragma: no cover
|
||||
raise ImportError("The `opentelemetry-api` package is required to use `OpenTelemetryMiddleware`.") from None
|
||||
|
||||
from starlette import __version__
|
||||
from starlette.datastructures import URL
|
||||
from starlette.routing import Mount
|
||||
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||
|
||||
|
||||
class OpenTelemetryMiddleware:
|
||||
"""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 against the full request URL.
|
||||
Pass a comma-separated string or a sequence. By default, exclude no URLs.
|
||||
tracer_provider: Optional tracer provider. If omitted, use the global tracer provider.
|
||||
meter_provider: Optional meter provider. If omitted, use the global meter provider.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: ASGIApp,
|
||||
*,
|
||||
excluded_urls: str | Sequence[str] = (),
|
||||
tracer_provider: trace.TracerProvider | None = None,
|
||||
meter_provider: metrics.MeterProvider | None = None,
|
||||
) -> None:
|
||||
self.app = app
|
||||
self._meter_provider = meter_provider
|
||||
if isinstance(excluded_urls, str):
|
||||
excluded_urls = [pattern.strip() for pattern in excluded_urls.split(",")] if excluded_urls else ()
|
||||
self._excluded_urls = tuple(re.compile(pattern) for pattern in excluded_urls)
|
||||
self._tracer_provider = tracer_provider or trace.get_tracer_provider()
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] != "http" or scope.get("starlette.opentelemetry"):
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
scope["starlette.opentelemetry"] = True
|
||||
try:
|
||||
tracer_provider = self._tracer_provider
|
||||
if isinstance(tracer_provider, trace.NoOpTracerProvider):
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
url = URL(scope=scope)
|
||||
if any(pattern.search(str(url)) for pattern in self._excluded_urls):
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
original_method = scope.get("method", "")
|
||||
method = original_method.upper()
|
||||
|
||||
headers: dict[str, list[str]] = {}
|
||||
for name, value in scope.get("headers", []):
|
||||
headers.setdefault(name.decode("latin-1").lower(), []).append(value.decode("latin-1"))
|
||||
|
||||
attributes: dict[str, str | int] = {
|
||||
"http.request.method": method,
|
||||
"url.path": scope.get("path", ""),
|
||||
"url.scheme": scope.get("scheme", "http"),
|
||||
}
|
||||
if original_method != method:
|
||||
attributes["http.request.method_original"] = original_method
|
||||
if url.query:
|
||||
attributes["url.query"] = url.query
|
||||
if url.hostname is not None:
|
||||
attributes["server.address"] = url.hostname
|
||||
server = scope.get("server")
|
||||
server_port = url.port if url.port is not None else server[1] if server is not None else None
|
||||
if server_port is not None:
|
||||
attributes["server.port"] = server_port
|
||||
if scope.get("http_version"):
|
||||
attributes["network.protocol.version"] = scope["http_version"]
|
||||
if scope.get("client") is not None:
|
||||
attributes["client.address"] = scope["client"][0]
|
||||
if headers.get("user-agent"):
|
||||
attributes["user_agent.original"] = headers["user-agent"][0]
|
||||
|
||||
with tracer_provider.get_tracer("starlette", __version__).start_as_current_span(
|
||||
method,
|
||||
context=propagate.extract(headers),
|
||||
kind=SpanKind.SERVER,
|
||||
attributes=attributes,
|
||||
) as span:
|
||||
|
||||
async def send_with_telemetry(message: Message) -> None:
|
||||
if message["type"] == "http.response.start":
|
||||
status_code = message["status"]
|
||||
span.set_attribute("http.response.status_code", status_code)
|
||||
if status_code >= 500:
|
||||
span.set_attribute("error.type", str(status_code))
|
||||
span.set_status(Status(StatusCode.ERROR))
|
||||
await send(message)
|
||||
|
||||
try:
|
||||
await self.app(scope, receive, send_with_telemetry)
|
||||
except Exception as exc:
|
||||
span.set_attribute("error.type", type(exc).__qualname__)
|
||||
raise
|
||||
finally:
|
||||
route = scope.get("route")
|
||||
if isinstance(route, Mount):
|
||||
route_path = scope.get("root_path") or "/"
|
||||
else:
|
||||
path_format = getattr(route, "path_format", None)
|
||||
route_path = (
|
||||
scope.get("root_path", "").rstrip("/") + path_format or "/"
|
||||
if isinstance(path_format, str)
|
||||
else None
|
||||
)
|
||||
if route_path is not None:
|
||||
span.update_name(f"{method} {route_path}")
|
||||
span.set_attribute("http.route", route_path)
|
||||
finally:
|
||||
del scope["starlette.opentelemetry"]
|
||||
Reference in New Issue
Block a user