This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
@@ -0,0 +1,238 @@
import atexit
import os
import threading
from collections.abc import Callable
from typing import Any, cast
from urllib.parse import urlsplit
from anyio.to_thread import run_sync
from fastapi.exceptions import FastAPIError
from fastapi.logger import logger
from fastapi.telemetry._api import TelemetryConfig, _unconfigured
from opentelemetry import _logs, metrics, trace
from starlette.types import ASGIApp, Message, Receive, Scope, Send
_lock = threading.RLock()
_owned: list[Any] = []
_configured: list[tuple[str, Any]] = []
def _flush() -> None:
for component in tuple(_owned):
try:
component.force_flush()
except Exception:
logger.exception("FastAPI telemetry cleanup failed")
def _shutdown() -> None:
with _lock:
owned = tuple(_owned)
_owned.clear()
for component in owned:
try:
component.shutdown()
except Exception:
logger.exception("FastAPI telemetry cleanup failed")
atexit.register(_shutdown)
def _export_endpoint(signal: str) -> str | None:
exporter = (os.getenv(f"OTEL_{signal}_EXPORTER") or "otlp").strip().lower()
endpoint = os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_ENDPOINT")
if not endpoint:
endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
if endpoint:
endpoint = f"{endpoint.removesuffix('/')}/v1/{signal.lower()}"
if not endpoint or exporter == "none":
return None
if exporter != "otlp":
raise FastAPIError(
f"FastAPI automatic telemetry supports OTEL_{signal}_EXPORTER=otlp or none. "
"Configure other exporters explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
)
protocol = (
os.getenv(f"OTEL_EXPORTER_OTLP_{signal}_PROTOCOL")
or os.getenv("OTEL_EXPORTER_OTLP_PROTOCOL")
or "http/protobuf"
)
if protocol != "http/protobuf":
raise FastAPIError(
"FastAPI automatic telemetry requires the OTLP http/protobuf protocol. Configure other transports explicitly and pass telemetry={'auto_configure': False} to FastAPI()."
)
parsed = urlsplit(endpoint)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise FastAPIError(
f"Invalid OTLP {signal.lower()} endpoint. Use an absolute HTTP or HTTPS URL."
)
return endpoint
def _registration_provider(provider: Any) -> Any:
# Older Logfire meter wrappers do not expose add_metric_reader(). Attach
# the reader to their SDK provider. Measurements still use the original wrapper.
if (
type(provider).__module__ == "logfire._internal.metrics"
and type(provider).__name__ == "ProxyMeterProvider"
and not hasattr(provider, "add_metric_reader")
):
return provider.provider
return provider
def _configure_from_environment(config: TelemetryConfig) -> None:
"""Add OTLP exporters from the environment to the selected providers.
Called automatically before ASGI lifespan startup. Existing providers and
their exporters are preserved. FastAPI registers its own export components once
per provider. It does not inspect or deduplicate other components' exporters.
Enable with `FASTAPI_OTEL_AUTO_CONFIGURE=true` or pass
`telemetry={"auto_configure": True}` to `FastAPI()`.
"""
if (
not config["auto_configure"]
or os.getenv("OTEL_SDK_DISABLED", "").lower() == "true"
):
return
with _lock:
signals = [
("TRACES", config["tracing"], config["tracer_provider"]),
("METRICS", config["metrics"], config["meter_provider"]),
("LOGS", config["logs"], config["logger_provider"]),
]
requested = [
(signal, provider, endpoint)
for signal, enabled, provider in signals
if enabled and (endpoint := _export_endpoint(signal)) is not None
]
if not requested:
return
try:
from opentelemetry.exporter.otlp.proto.http._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter,
)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
except ImportError as exc:
raise FastAPIError(
"Automatic OpenTelemetry export requires fastapi[opentelemetry] or fastapi[standard]. Install the extra, configure providers yourself, or pass telemetry={'auto_configure': False} to FastAPI()."
) from exc
# The SDK's registration methods also allow vendor wrappers to forward
# additions. Keep a record of our own registrations, without examining
# another component's exporters or their destinations.
providers = {
"TRACES": ("tracer", trace.get_tracer_provider),
"METRICS": ("meter", metrics.get_meter_provider),
"LOGS": ("logger", _logs.get_logger_provider),
}
def create_component(*, signal: str, endpoint: str) -> Any:
if signal == "TRACES":
return BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))
if signal == "METRICS":
return PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=endpoint)
)
return BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint))
for signal, explicit, endpoint in requested:
name, get_provider = providers[signal]
provider = _registration_provider(
explicit if explicit is not None else get_provider()
)
if any(s == signal and p is provider for s, p in _configured):
continue
if explicit is None and _unconfigured(provider):
component = create_component(signal=signal, endpoint=endpoint)
if signal == "TRACES":
provider = TracerProvider(shutdown_on_exit=False)
provider.add_span_processor(component)
trace.set_tracer_provider(provider)
elif signal == "METRICS":
provider = MeterProvider(
metric_readers=[component], shutdown_on_exit=False
)
metrics.set_meter_provider(provider)
else:
provider = LoggerProvider(shutdown_on_exit=False)
provider.add_log_record_processor(component)
_logs.set_logger_provider(provider)
current = get_provider()
if current is provider:
_configured.append((signal, provider))
_owned.append(provider)
continue
# Another component configured the global provider concurrently.
# Close the unused pipeline and attach a new component to theirs.
provider.shutdown()
provider = _registration_provider(current)
# Vendor wrappers can expose the SDK registration methods without
# inheriting its provider classes. Check each method against its SDK
# class statically and its availability on the provider at runtime.
register: Callable[[Any], None] | None
try:
if signal == "TRACES":
register = cast(TracerProvider, provider).add_span_processor
elif signal == "METRICS":
register = cast(MeterProvider, provider).add_metric_reader
else:
register = cast(LoggerProvider, provider).add_log_record_processor
except AttributeError:
register = None
if not callable(register):
raise FastAPIError(
f"The configured OpenTelemetry {name} provider does not support "
"adding an OTLP exporter. Configure environment export through "
"that provider and pass telemetry={'auto_configure': False} to FastAPI()."
)
component = create_component(signal=signal, endpoint=endpoint)
try:
register(component)
except Exception:
component.shutdown()
raise
_configured.append((signal, provider))
_owned.append(component)
async def lifespan(
*, config: TelemetryConfig, app: ASGIApp, scope: Scope, receive: Receive, send: Send
) -> None:
async def wrapped_receive() -> Message:
message = await receive()
if message["type"] == "lifespan.startup":
try:
_configure_from_environment(config)
except Exception as exc:
# Optional telemetry setup must not prevent application startup.
logger.warning(
"FastAPI automatic telemetry configuration failed: %s", exc
)
return message
async def wrapped_send(message: Message) -> None:
if (
message["type"]
in {
"lifespan.shutdown.complete",
"lifespan.shutdown.failed",
"lifespan.startup.failed",
}
and _owned
):
await run_sync(_flush)
await send(message)
await app(scope, wrapped_receive, wrapped_send)