2025-05-23 06:18:15 +08:00
|
|
|
|
"""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
|
2025-08-02 16:30:34 +08:00
|
|
|
|
from base64 import b64encode
|
2025-05-23 06:18:15 +08:00
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Request
|
|
|
|
|
from opentelemetry import metrics
|
|
|
|
|
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
|
|
|
OTLPMetricExporter,
|
|
|
|
|
)
|
2025-08-02 16:30:34 +08:00
|
|
|
|
|
|
|
|
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
|
|
|
OTLPMetricExporter as OTLPHttpMetricExporter,
|
|
|
|
|
)
|
2025-05-23 06:18:15 +08:00
|
|
|
|
from opentelemetry.sdk.metrics import MeterProvider
|
|
|
|
|
from opentelemetry.sdk.metrics.view import View
|
|
|
|
|
from opentelemetry.sdk.metrics.export import (
|
|
|
|
|
PeriodicExportingMetricReader,
|
|
|
|
|
)
|
2025-08-02 16:30:34 +08:00
|
|
|
|
from opentelemetry.sdk.resources import Resource
|
|
|
|
|
|
|
|
|
|
from open_webui.env import (
|
|
|
|
|
OTEL_SERVICE_NAME,
|
|
|
|
|
OTEL_METRICS_EXPORTER_OTLP_ENDPOINT,
|
|
|
|
|
OTEL_METRICS_BASIC_AUTH_USERNAME,
|
|
|
|
|
OTEL_METRICS_BASIC_AUTH_PASSWORD,
|
|
|
|
|
OTEL_METRICS_OTLP_SPAN_EXPORTER,
|
|
|
|
|
OTEL_METRICS_EXPORTER_OTLP_INSECURE,
|
|
|
|
|
)
|
2025-07-17 03:06:36 +08:00
|
|
|
|
from open_webui.socket.main import get_active_user_ids
|
|
|
|
|
from open_webui.models.users import Users
|
2025-05-23 06:18:15 +08:00
|
|
|
|
|
|
|
|
|
_EXPORT_INTERVAL_MILLIS = 10_000 # 10 seconds
|
|
|
|
|
|
|
|
|
|
|
2025-08-02 16:30:34 +08:00
|
|
|
|
def _build_meter_provider(resource: Resource) -> MeterProvider:
|
2025-05-23 06:18:15 +08:00
|
|
|
|
"""Return a configured MeterProvider."""
|
2025-08-02 16:30:34 +08:00
|
|
|
|
headers = []
|
|
|
|
|
if OTEL_METRICS_BASIC_AUTH_USERNAME and OTEL_METRICS_BASIC_AUTH_PASSWORD:
|
|
|
|
|
auth_string = (
|
|
|
|
|
f"{OTEL_METRICS_BASIC_AUTH_USERNAME}:{OTEL_METRICS_BASIC_AUTH_PASSWORD}"
|
|
|
|
|
)
|
|
|
|
|
auth_header = b64encode(auth_string.encode()).decode()
|
|
|
|
|
headers = [("authorization", f"Basic {auth_header}")]
|
2025-05-23 06:18:15 +08:00
|
|
|
|
|
|
|
|
|
# Periodic reader pushes metrics over OTLP/gRPC to collector
|
2025-08-02 16:30:34 +08:00
|
|
|
|
if OTEL_METRICS_OTLP_SPAN_EXPORTER == "http":
|
|
|
|
|
readers: List[PeriodicExportingMetricReader] = [
|
|
|
|
|
PeriodicExportingMetricReader(
|
|
|
|
|
OTLPHttpMetricExporter(
|
|
|
|
|
endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, headers=headers
|
|
|
|
|
),
|
|
|
|
|
export_interval_millis=_EXPORT_INTERVAL_MILLIS,
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
readers: List[PeriodicExportingMetricReader] = [
|
|
|
|
|
PeriodicExportingMetricReader(
|
|
|
|
|
OTLPMetricExporter(
|
|
|
|
|
endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT,
|
|
|
|
|
insecure=OTEL_METRICS_EXPORTER_OTLP_INSECURE,
|
|
|
|
|
headers=headers,
|
|
|
|
|
),
|
|
|
|
|
export_interval_millis=_EXPORT_INTERVAL_MILLIS,
|
|
|
|
|
)
|
|
|
|
|
]
|
2025-05-23 06:18:15 +08:00
|
|
|
|
|
|
|
|
|
# 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"],
|
|
|
|
|
),
|
2025-07-17 03:06:36 +08:00
|
|
|
|
View(
|
|
|
|
|
instrument_name="webui.users.total",
|
|
|
|
|
),
|
|
|
|
|
View(
|
|
|
|
|
instrument_name="webui.users.active",
|
|
|
|
|
),
|
2025-05-23 06:18:15 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
provider = MeterProvider(
|
2025-08-02 16:30:34 +08:00
|
|
|
|
resource=resource,
|
2025-05-23 06:18:15 +08:00
|
|
|
|
metric_readers=list(readers),
|
|
|
|
|
views=views,
|
|
|
|
|
)
|
|
|
|
|
return provider
|
|
|
|
|
|
|
|
|
|
|
2025-08-02 16:30:34 +08:00
|
|
|
|
def setup_metrics(app: FastAPI, resource: Resource) -> None:
|
2025-05-23 06:18:15 +08:00
|
|
|
|
"""Attach OTel metrics middleware to *app* and initialise provider."""
|
|
|
|
|
|
2025-08-02 16:30:34 +08:00
|
|
|
|
metrics.set_meter_provider(_build_meter_provider(resource))
|
2025-05-23 06:18:15 +08:00
|
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-17 03:06:36 +08:00
|
|
|
|
def observe_active_users(
|
|
|
|
|
options: metrics.CallbackOptions,
|
|
|
|
|
) -> Sequence[metrics.Observation]:
|
|
|
|
|
return [
|
|
|
|
|
metrics.Observation(
|
|
|
|
|
value=len(get_active_user_ids()),
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def observe_total_registered_users(
|
|
|
|
|
options: metrics.CallbackOptions,
|
|
|
|
|
) -> Sequence[metrics.Observation]:
|
|
|
|
|
return [
|
|
|
|
|
metrics.Observation(
|
|
|
|
|
value=len(Users.get_users()["users"]),
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
meter.create_observable_gauge(
|
|
|
|
|
name="webui.users.total",
|
|
|
|
|
description="Total number of registered users",
|
|
|
|
|
unit="users",
|
|
|
|
|
callbacks=[observe_total_registered_users],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
meter.create_observable_gauge(
|
|
|
|
|
name="webui.users.active",
|
|
|
|
|
description="Number of currently active users",
|
|
|
|
|
unit="users",
|
|
|
|
|
callbacks=[observe_active_users],
|
|
|
|
|
)
|
|
|
|
|
|
2025-05-23 06:18:15 +08:00
|
|
|
|
# FastAPI middleware
|
|
|
|
|
@app.middleware("http")
|
|
|
|
|
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
|