Add SERVER spans for FastStream ASGI routes - #151
personage-hub wants to merge 3 commits into
Conversation
Wrap HTTP requests to the FastStream ASGI application in OpenTelemetryMiddleware, the same way the Litestar bootstrapper does, so health checks produce SERVER spans with http.route and the response status code. Lifespan events bypass the middleware. Drop the manual health-check span, which was INTERNAL and had no attributes; opentelemetry_generate_health_check_spans now controls the excluded urls. Apply opentelemetry_exclude_urls in the Litestar bootstrapper, combined with OTEL_PYTHON_LITESTAR_EXCLUDED_URLS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s from broker middleware - Match the request path against application.routes and omit http.route on unknown paths - Drop the opentelemetry_middleware_cls requirement from FastStreamOpentelemetryInstrument.is_ready - Type FastStreamBootstrapper as KwargsAsgiFastStream and document add_http_middleware Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| @@ -20,6 +21,7 @@ | |||
| from microbootstrap.instruments.opentelemetry_instrument import ( | |||
There was a problem hiding this comment.
Давай тут импортнем сразу instrument, чтобы импорт не разрастался
| def __init__(self, **kwargs: typing.Any) -> None: # noqa: ANN401 | ||
| # `broker` argument is positional-only | ||
| super().__init__(kwargs.pop("broker", None), **kwargs) | ||
| self.http_app: ASGIApp = super().__call__ |
There was a problem hiding this comment.
Выглядит как-то оч странно, почему метод - это объект asgiapp?
| super().__init__(kwargs.pop("broker", None), **kwargs) | ||
| self.http_app: ASGIApp = super().__call__ | ||
|
|
||
| def add_http_middleware(self, build_middleware: typing.Callable[[ASGIApp], ASGIApp]) -> None: |
There was a problem hiding this comment.
Поч функцию извне прокидываем? Разве нет какой-то функции, добаляющей миддлварь в фастстриме для веб приложения?
| def add_http_middleware(self, build_middleware: typing.Callable[[ASGIApp], ASGIApp]) -> None: | ||
| self.http_app = build_middleware(self.http_app) | ||
|
|
||
| async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: |
There was a problem hiding this comment.
Не оч нравится, что мы так переопределяем поведение объекта приложения, без этого никак не обойтись?
| def build_faststream_route_details_from_scope( | ||
| scope: Scope, | ||
| routes: typing.Iterable[tuple[str, ASGIApp]], | ||
| ) -> tuple[str, dict[str, str]]: | ||
| method: typing.Final = str(scope.get("method", "HTTP")).strip() | ||
| path: typing.Final = scope.get("path") | ||
| # FastStream matches ASGI routes by exact path, unmatched paths get no `http.route` to keep its cardinality low | ||
| if path is None or all(path != route_path for route_path, _ in routes): | ||
| return method, {} | ||
| return build_span_name(method, path), {"http.route": path} |
There was a problem hiding this comment.
Короче, мы тут как будто начинаем делать работу за фастстрим. Это не целевое решение. Давай закинем им в чат, что очень надо это поддержать. Пока предлагаю это не вливать и если очень надо сделать именно так, то это можно переопределить в конкретной репе (или сделать как у нас в репах)
There was a problem hiding this comment.
Можешь даже сам пойти в фастстрим и запилить там ПР. Или зайти к Роме, есть там такой чувак, который за телеметрию отвечает
Motivation
Availability SLIs are computed from health-check spans: they must be
SERVERspans withhttp.routeandhttp.response.status_code. The Litestar bootstrapper already produces them viaLitestarOpenTelemetryInstrumentationMiddleware, which wraps the app inOpenTelemetryMiddlewarefromopentelemetry-instrumentation-asgi. The FastStream bootstrapper wrapped the health handler intracer.start_as_current_span(...), which produced anINTERNALspan with no route or status, so FastStream workers could not be monitored the same way.Changes
KwargsAsgiFastStreamrouteshttpscopes throughhttp_app, which can be wrapped via the publicadd_http_middleware()(documented in the README). Lifespan and websocket scopes go straight toAsgiFastStream.__call__. The application stays anAsgiFastStream, soapplication.brokerand the rest of the public API are unchanged.FastStreamBootstrapper.bootstrap()is now typed asKwargsAsgiFastStream, a subclass, so existingAsgiFastStreamannotations keep working.FastStreamOpentelemetryInstrument.bootstrap_afterwraps HTTP handling inOpenTelemetryMiddlewarewith the instrument'stracer_provider,build_faststream_route_details_from_scopeandExcludeList(define_exclude_urls()). The status attribute followsOTEL_SEMCONV_STABILITY_OPT_INjust like in Litestar:http.status_code,http.response.status_codeor both.opentelemetry_generate_health_check_spanskeeps working: whenFalse, the health path is added to the excluded urls.build_span_namemoves toinstruments/opentelemetry_instrument.pyso the FastStream bootstrapper doesn't import the Litestar module (thelitestarextra may be missing).define_exclude_urlsmoves toBaseOpentelemetryInstrument.http.routeis set only when the request path matches an entry ofapplication.routes(FastStream matches routes by exact path). Unknown paths get a span named after the method only, withouthttp.route, so scans like/wp-admin/...don't blow up the cardinality of span-derived metrics.FastStreamOpentelemetryInstrument.is_readyno longer requiresopentelemetry_middleware_cls: HTTP server spans work without a broker telemetry middleware, which is still added only when configured.Related fix: Litestar ignored
opentelemetry_exclude_urlsLitestarOpenTelemetryInstrumentationMiddlewareread exclusions only fromOTEL_PYTHON_LITESTAR_EXCLUDED_URLS. It now combinesdefine_exclude_urls()with the env-based list (CombinedExcludeList) and builds the list once instead of per request.Breaking
opentelemetry_exclude_urlsused to be ignored. Now the default/metricsexclusion applies, so/metricsstops producing spans, andopentelemetry_generate_health_check_spans=Falsesuppresses health-check spans (the default isTrue). This affects every Litestar user and should go into the release notes.opentelemetry_middleware_clsto activate. It follows the same readiness rule as the other bootstrappers (opentelemetry_endpoint,service_debugoropentelemetry_log_traces), so FastStream apps without a broker middleware now get a tracer provider and HTTP server spans.Tests
GET /health/gives exactly oneSERVERspanGET /health/withhttp.route == "/health/", and the status attribute matches the semconv mode (unset /http/http/dup).broker.pinggives a 500 status attribute andStatusCode.ERROR.http.route.opentelemetry_middleware_cls, and no broker middlewares are added in that case./metricsproduces no spans. Withopentelemetry_generate_health_check_spans=Falsehealth has no span while other routes still do.create/process/publish.application.brokerstill works.opentelemetry_exclude_urls=["/internal"]suppresses spans without env vars, and settings and env exclusions combine.just lintandjust testpass (237 tests).🤖 Generated with Claude Code