Spaces:
Running
Running
"""OpenTelemetry metrics bootstrap for Open WebUI. | |
This module initialises a MeterProvider that sends metrics to an OTLP | |
collector. The collector is responsible for exposing a Prometheus | |
`/metrics` endpoint – WebUI does **not** expose it directly. | |
Metrics collected: | |
* http.server.requests (counter) | |
* http.server.duration (histogram, milliseconds) | |
Attributes used: http.method, http.route, http.status_code | |
If you wish to add more attributes (e.g. user-agent) you can, but beware of | |
high-cardinality label sets. | |
""" | |
from __future__ import annotations | |
import time | |
from typing import Dict, List, Sequence, Any | |
from fastapi import FastAPI, Request | |
from opentelemetry import metrics | |
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( | |
OTLPMetricExporter, | |
) | |
from opentelemetry.sdk.metrics import MeterProvider | |
from opentelemetry.sdk.metrics.view import View | |
from opentelemetry.sdk.metrics.export import ( | |
PeriodicExportingMetricReader, | |
) | |
from opentelemetry.sdk.resources import SERVICE_NAME, Resource | |
from open_webui.env import OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT | |
_EXPORT_INTERVAL_MILLIS = 10_000 # 10 seconds | |
def _build_meter_provider() -> MeterProvider: | |
"""Return a configured MeterProvider.""" | |
# Periodic reader pushes metrics over OTLP/gRPC to collector | |
readers: List[PeriodicExportingMetricReader] = [ | |
PeriodicExportingMetricReader( | |
OTLPMetricExporter(endpoint=OTEL_EXPORTER_OTLP_ENDPOINT), | |
export_interval_millis=_EXPORT_INTERVAL_MILLIS, | |
) | |
] | |
# Optional view to limit cardinality: drop user-agent etc. | |
views: List[View] = [ | |
View( | |
instrument_name="http.server.duration", | |
attribute_keys=["http.method", "http.route", "http.status_code"], | |
), | |
View( | |
instrument_name="http.server.requests", | |
attribute_keys=["http.method", "http.route", "http.status_code"], | |
), | |
] | |
provider = MeterProvider( | |
resource=Resource.create({SERVICE_NAME: OTEL_SERVICE_NAME}), | |
metric_readers=list(readers), | |
views=views, | |
) | |
return provider | |
def setup_metrics(app: FastAPI) -> None: | |
"""Attach OTel metrics middleware to *app* and initialise provider.""" | |
metrics.set_meter_provider(_build_meter_provider()) | |
meter = metrics.get_meter(__name__) | |
# Instruments | |
request_counter = meter.create_counter( | |
name="http.server.requests", | |
description="Total HTTP requests", | |
unit="1", | |
) | |
duration_histogram = meter.create_histogram( | |
name="http.server.duration", | |
description="HTTP request duration", | |
unit="ms", | |
) | |
# FastAPI middleware | |
async def _metrics_middleware(request: Request, call_next): | |
start_time = time.perf_counter() | |
response = await call_next(request) | |
elapsed_ms = (time.perf_counter() - start_time) * 1000.0 | |
# Route template e.g. "/items/{item_id}" instead of real path. | |
route = request.scope.get("route") | |
route_path = getattr(route, "path", request.url.path) | |
attrs: Dict[str, str | int] = { | |
"http.method": request.method, | |
"http.route": route_path, | |
"http.status_code": response.status_code, | |
} | |
request_counter.add(1, attrs) | |
duration_histogram.record(elapsed_ms, attrs) | |
return response | |