fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
"""FastAPI server-span instrumentation — the proxy mounts this at app creation.
|
|
|
|
``opentelemetry-instrumentation-fastapi`` creates the SERVER span for each HTTP
|
|
route and extracts inbound ``traceparent`` headers. This module owns the one call
|
|
site that attaches it to the proxy app, plus the passthrough span-naming hook, so
|
|
``proxy_server`` stays free of OTel details.
|
|
|
|
The ``FastAPIInstrumentor`` import is kept lazy (inside :func:`instrument_fastapi_app`,
|
|
after the gate check) so importing this module never requires the optional
|
|
``opentelemetry-instrumentation-fastapi`` package and pulls in nothing OTel-related
|
|
when the feature gate is off.
|
|
"""
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
from litellm._logging import verbose_logger
|
|
from litellm.integrations.otel.model.config import is_otel_v2_enabled
|
|
|
|
# Routes excluded from server-span tracing by default: high-frequency pollers and
|
|
# static UI/docs assets, none of which are LLM traffic. Entries are substring-matched
|
|
# against the request path (unanchored, so they survive a ``server_root_path`` prefix
|
|
# and each entry also covers everything beneath it — e.g. ``/health`` covers
|
|
# ``/health/readiness``). Operators override the whole set via the standard
|
|
# ``OTEL_PYTHON_FASTAPI_EXCLUDED_URLS`` env var (set "" to trace everything).
|
|
_DEFAULT_EXCLUDED_ROUTES = (
|
|
"/health", # load-balancer liveness/readiness polling
|
|
"/metrics", # Prometheus scrape (also drops the /model/metrics admin analytics)
|
|
"/litellm-asset-prefix", # hashed UI asset bundles
|
|
"/_next", # Next.js static JS/CSS chunks (root-level mount)
|
|
"/ui", # admin UI single-page app
|
|
"/swagger", # static Swagger UI assets
|
|
"/docs", # FastAPI Swagger docs page
|
|
"/redoc", # FastAPI ReDoc docs page
|
|
"/openapi.json", # OpenAPI schema
|
|
"favicon", # /favicon.ico + /get_favicon
|
|
"/.well-known", # UI config discovery
|
|
)
|
|
_DEFAULT_EXCLUDED_URLS = ",".join(_DEFAULT_EXCLUDED_ROUTES)
|
|
|
|
# Passthrough routes are catch-alls (e.g. "/openai/{endpoint:path}"), so the
|
|
# default OTel server-span name "{method} {route}" collapses every upstream
|
|
# endpoint into "POST /openai/{endpoint:path}". The hook below renames those spans
|
|
# to the real request path so each endpoint is distinguishable. Non-catch-all
|
|
# routes keep their low-cardinality template name.
|
|
PASSTHROUGH_PREFIXES = frozenset(
|
|
{
|
|
"openai",
|
|
"openai_passthrough",
|
|
"anthropic",
|
|
"azure",
|
|
"azure_ai",
|
|
"bedrock",
|
|
"cohere",
|
|
"cursor",
|
|
"gemini",
|
|
"mistral",
|
|
"vllm",
|
|
"vertex_ai",
|
|
"vertex-ai",
|
|
"assemblyai",
|
|
"eu.assemblyai",
|
|
"milvus",
|
|
}
|
|
)
|
|
|
|
|
|
def _passthrough_span_name_hook(span: Any, scope: dict) -> None:
|
|
"""FastAPI ``server_request_hook``: give passthrough server spans a useful name.
|
|
|
|
The instrumentation matches the route at span creation, so both the span name
|
|
and ``http.route`` are set to the catch-all template (``/openai/{endpoint:path}``)
|
|
before this hook runs. Rewrite both to the real request path so each upstream
|
|
endpoint is distinguishable. (The ASGI ``http receive``/``http send`` sub-spans
|
|
can't be renamed from here — their name is captured at creation — so they are
|
|
dropped via ``exclude_spans`` at instrumentation time.)
|
|
"""
|
|
try:
|
|
if span is None or not span.is_recording():
|
|
return
|
|
path = scope.get("path") or ""
|
|
method = scope.get("method") or ""
|
|
first_segment = path.lstrip("/").split("/", 1)[0]
|
|
if first_segment in PASSTHROUGH_PREFIXES:
|
|
span.update_name(f"{method} {path}".strip())
|
|
span.set_attribute("http.route", path)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def instrument_fastapi_app(app: Any) -> None:
|
|
"""Attach OTel server-span instrumentation to the proxy FastAPI app.
|
|
|
|
Safe no-op when the V2 gate is off or ``opentelemetry-instrumentation-fastapi``
|
|
is unavailable. This MUST be called at app-creation time — once the lifespan
|
|
runs, the middleware stack is frozen and ``instrument_app`` raises "Cannot add
|
|
middleware after an application has started".
|
|
|
|
No ``TracerProvider`` is passed, so the instrumentation binds to the OTel global
|
|
``ProxyTracerProvider``; the proxy publishes the real provider as the global
|
|
after config load (see ``proxy_startup_event``), and the proxy delegates to it.
|
|
That way server spans and gen-ai spans share one provider and the same trace.
|
|
"""
|
|
try:
|
|
if not is_otel_v2_enabled():
|
|
return
|
|
|
|
# Lazy: only the V2-enabled path needs the optional
|
|
# ``opentelemetry-instrumentation-fastapi`` package, which is not part of the
|
|
# base ``litellm[proxy]`` install. Importing it at module top would make
|
|
# ``proxy_server``'s unconditional ``import`` of this module crash when the
|
|
# package is absent, even with the gate off.
|
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
|
|
excluded_urls = (
|
|
os.environ.get("OTEL_PYTHON_FASTAPI_EXCLUDED_URLS")
|
|
if "OTEL_PYTHON_FASTAPI_EXCLUDED_URLS" in os.environ
|
|
else _DEFAULT_EXCLUDED_URLS
|
|
)
|
|
FastAPIInstrumentor.instrument_app(
|
|
app,
|
|
excluded_urls=excluded_urls,
|
|
server_request_hook=_passthrough_span_name_hook,
|
|
# Drop the ASGI "http receive"/"http send" lifecycle sub-spans: they
|
|
# are low-value noise and (for passthrough) carry the catch-all route
|
|
# template in their name, which can't be rewritten from a hook.
|
|
exclude_spans=["receive", "send"],
|
|
)
|
|
except Exception as e:
|
|
verbose_logger.debug("Skipping OTel V2 FastAPI instrumentation: %s", e)
|