This commit is contained in:
Timothy Jaeryang Baek
2026-03-17 17:58:01 -05:00
parent fcf7208352
commit de3317e26b
220 changed files with 17200 additions and 22836 deletions
+16 -16
View File
@@ -1,12 +1,12 @@
from opentelemetry.semconv.trace import SpanAttributes as _SpanAttributes
# Span Tags
SPAN_DB_TYPE = "mysql"
SPAN_REDIS_TYPE = "redis"
SPAN_DURATION = "duration"
SPAN_SQL_STR = "sql"
SPAN_SQL_EXPLAIN = "explain"
SPAN_ERROR_TYPE = "error"
SPAN_DB_TYPE = 'mysql'
SPAN_REDIS_TYPE = 'redis'
SPAN_DURATION = 'duration'
SPAN_SQL_STR = 'sql'
SPAN_SQL_EXPLAIN = 'explain'
SPAN_ERROR_TYPE = 'error'
class SpanAttributes(_SpanAttributes):
@@ -14,13 +14,13 @@ class SpanAttributes(_SpanAttributes):
Span Attributes
"""
DB_INSTANCE = "db.instance"
DB_TYPE = "db.type"
DB_IP = "db.ip"
DB_PORT = "db.port"
ERROR_KIND = "error.kind"
ERROR_OBJECT = "error.object"
ERROR_MESSAGE = "error.message"
RESULT_CODE = "result.code"
RESULT_MESSAGE = "result.message"
RESULT_ERRORS = "result.errors"
DB_INSTANCE = 'db.instance'
DB_TYPE = 'db.type'
DB_IP = 'db.ip'
DB_PORT = 'db.port'
ERROR_KIND = 'error.kind'
ERROR_OBJECT = 'error.object'
ERROR_MESSAGE = 'error.message'
RESULT_CODE = 'result.code'
RESULT_MESSAGE = 'result.message'
RESULT_ERRORS = 'result.errors'
@@ -38,7 +38,7 @@ def requests_hook(span: Span, request: PreparedRequest):
Http Request Hook
"""
span.update_name(f"{request.method} {request.url}")
span.update_name(f'{request.method} {request.url}')
span.set_attributes(
attributes={
SpanAttributes.HTTP_URL: request.url,
@@ -70,8 +70,8 @@ def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args,
# - redis.cluster.RedisCluster
# Instead of checking the type, we check if the instance has a nodes_manager attribute.
try:
db = ""
if hasattr(instance, "nodes_manager"):
db = ''
if hasattr(instance, 'nodes_manager'):
default_node = instance.nodes_manager.default_node
if not default_node:
return
@@ -79,17 +79,17 @@ def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args,
port = default_node.port
else:
connection_kwargs: dict = instance.connection_pool.connection_kwargs
host = connection_kwargs.get("host")
port = connection_kwargs.get("port")
db = connection_kwargs.get("db")
host = connection_kwargs.get('host')
port = connection_kwargs.get('port')
db = connection_kwargs.get('db')
span.set_attributes(
{
SpanAttributes.DB_INSTANCE: f"{host}/{db}",
SpanAttributes.DB_NAME: f"{host}/{db}",
SpanAttributes.DB_INSTANCE: f'{host}/{db}',
SpanAttributes.DB_NAME: f'{host}/{db}',
SpanAttributes.DB_TYPE: SPAN_REDIS_TYPE,
SpanAttributes.DB_PORT: port,
SpanAttributes.DB_IP: host,
SpanAttributes.DB_STATEMENT: " ".join([str(i) for i in args]),
SpanAttributes.DB_STATEMENT: ' '.join([str(i) for i in args]),
SpanAttributes.DB_OPERATION: str(args[0]),
}
)
@@ -102,7 +102,7 @@ def httpx_request_hook(span: Span, request: RequestInfo):
HTTPX Request Hook
"""
span.update_name(f"{request.method.decode()} {str(request.url)}")
span.update_name(f'{request.method.decode()} {str(request.url)}')
span.set_attributes(
attributes={
SpanAttributes.HTTP_URL: str(request.url),
@@ -117,11 +117,7 @@ def httpx_response_hook(span: Span, request: RequestInfo, response: ResponseInfo
"""
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code)
span.set_status(
StatusCode.ERROR
if response.status_code >= status.HTTP_400_BAD_REQUEST
else StatusCode.OK
)
span.set_status(StatusCode.ERROR if response.status_code >= status.HTTP_400_BAD_REQUEST else StatusCode.OK)
async def httpx_async_request_hook(span: Span, request: RequestInfo):
@@ -132,9 +128,7 @@ async def httpx_async_request_hook(span: Span, request: RequestInfo):
httpx_request_hook(span, request)
async def httpx_async_response_hook(
span: Span, request: RequestInfo, response: ResponseInfo
):
async def httpx_async_response_hook(span: Span, request: RequestInfo, response: ResponseInfo):
"""
Async Response Hook
"""
@@ -147,7 +141,7 @@ def aiohttp_request_hook(span: Span, request: TraceRequestStartParams):
Aiohttp Request Hook
"""
span.update_name(f"{request.method} {str(request.url)}")
span.update_name(f'{request.method} {str(request.url)}')
span.set_attributes(
attributes={
SpanAttributes.HTTP_URL: str(request.url),
@@ -156,20 +150,14 @@ def aiohttp_request_hook(span: Span, request: TraceRequestStartParams):
)
def aiohttp_response_hook(
span: Span, response: Union[TraceRequestExceptionParams, TraceRequestEndParams]
):
def aiohttp_response_hook(span: Span, response: Union[TraceRequestExceptionParams, TraceRequestEndParams]):
"""
Aiohttp Response Hook
"""
if isinstance(response, TraceRequestEndParams):
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.response.status)
span.set_status(
StatusCode.ERROR
if response.response.status >= status.HTTP_400_BAD_REQUEST
else StatusCode.OK
)
span.set_status(StatusCode.ERROR if response.response.status >= status.HTTP_400_BAD_REQUEST else StatusCode.OK)
elif isinstance(response, TraceRequestExceptionParams):
span.set_status(StatusCode.ERROR)
span.set_attribute(SpanAttributes.ERROR_MESSAGE, str(response.exception))
@@ -191,9 +179,7 @@ class Instrumentor(BaseInstrumentor):
instrument_fastapi(app=self.app)
SQLAlchemyInstrumentor().instrument(engine=self.db_engine)
RedisInstrumentor().instrument(request_hook=redis_request_hook)
RequestsInstrumentor().instrument(
request_hook=requests_hook, response_hook=response_hook
)
RequestsInstrumentor().instrument(request_hook=requests_hook, response_hook=response_hook)
LoggingInstrumentor().instrument()
HTTPXClientInstrumentor().instrument(
request_hook=httpx_request_hook,
@@ -208,7 +194,7 @@ class Instrumentor(BaseInstrumentor):
SystemMetricsInstrumentor().instrument()
def _uninstrument(self, **kwargs):
if getattr(self, "instrumentors", None) is None:
if getattr(self, 'instrumentors', None) is None:
return
for instrumentor in self.instrumentors:
instrumentor.uninstrument()
+3 -3
View File
@@ -24,12 +24,12 @@ from open_webui.env import (
def setup_logging():
headers = []
if OTEL_LOGS_BASIC_AUTH_USERNAME and OTEL_LOGS_BASIC_AUTH_PASSWORD:
auth_string = f"{OTEL_LOGS_BASIC_AUTH_USERNAME}:{OTEL_LOGS_BASIC_AUTH_PASSWORD}"
auth_string = f'{OTEL_LOGS_BASIC_AUTH_USERNAME}:{OTEL_LOGS_BASIC_AUTH_PASSWORD}'
auth_header = b64encode(auth_string.encode()).decode()
headers = [("authorization", f"Basic {auth_header}")]
headers = [('authorization', f'Basic {auth_header}')]
resource = Resource.create(attributes={SERVICE_NAME: OTEL_SERVICE_NAME})
if OTEL_LOGS_OTLP_SPAN_EXPORTER == "http":
if OTEL_LOGS_OTLP_SPAN_EXPORTER == 'http':
exporter = HttpOTLPLogExporter(
endpoint=OTEL_LOGS_EXPORTER_OTLP_ENDPOINT,
headers=headers,
+33 -37
View File
@@ -53,19 +53,15 @@ def _build_meter_provider(resource: Resource) -> MeterProvider:
"""Return a configured MeterProvider."""
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_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}")]
headers = [('authorization', f'Basic {auth_header}')]
# Periodic reader pushes metrics over OTLP/gRPC to collector
if OTEL_METRICS_OTLP_SPAN_EXPORTER == "http":
if OTEL_METRICS_OTLP_SPAN_EXPORTER == 'http':
readers: List[PeriodicExportingMetricReader] = [
PeriodicExportingMetricReader(
OTLPHttpMetricExporter(
endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, headers=headers
),
OTLPHttpMetricExporter(endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, headers=headers),
export_interval_millis=OTEL_METRICS_EXPORT_INTERVAL_MILLIS,
)
]
@@ -84,21 +80,21 @@ def _build_meter_provider(resource: Resource) -> MeterProvider:
# 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"],
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"],
instrument_name='http.server.requests',
attribute_keys=['http.method', 'http.route', 'http.status_code'],
),
View(
instrument_name="webui.users.total",
instrument_name='webui.users.total',
),
View(
instrument_name="webui.users.active",
instrument_name='webui.users.active',
),
View(
instrument_name="webui.users.active.today",
instrument_name='webui.users.active.today',
),
]
@@ -118,14 +114,14 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
# Instruments
request_counter = meter.create_counter(
name="http.server.requests",
description="Counts the total number of inbound HTTP requests.",
unit="1",
name='http.server.requests',
description='Counts the total number of inbound HTTP requests.',
unit='1',
)
duration_histogram = meter.create_histogram(
name="http.server.duration",
description="Measures the duration of inbound HTTP requests.",
unit="ms",
name='http.server.duration',
description='Measures the duration of inbound HTTP requests.',
unit='ms',
)
def observe_active_users(
@@ -150,16 +146,16 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
]
meter.create_observable_gauge(
name="webui.users.total",
description="Total number of registered users",
unit="users",
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",
name='webui.users.active',
description='Number of currently active users',
unit='users',
callbacks=[observe_active_users],
)
@@ -169,21 +165,21 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
return [metrics.Observation(value=Users.get_num_users_active_today())]
meter.create_observable_gauge(
name="webui.users.active.today",
description="Number of users active since midnight today",
unit="users",
name='webui.users.active.today',
description='Number of users active since midnight today',
unit='users',
callbacks=[observe_users_active_today],
)
# FastAPI middleware
@app.middleware("http")
@app.middleware('http')
async def _metrics_middleware(request: Request, call_next):
start_time = time.perf_counter()
status_code = None
try:
response = await call_next(request)
status_code = getattr(response, "status_code", 500)
status_code = getattr(response, 'status_code', 500)
return response
except Exception:
status_code = 500
@@ -192,13 +188,13 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
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)
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": status_code,
'http.method': request.method,
'http.route': route_path,
'http.status_code': status_code,
}
request_counter.add(1, attrs)
+3 -3
View File
@@ -34,12 +34,12 @@ def setup(app: FastAPI, db_engine: Engine):
# Add basic auth header only if both username and password are not empty
headers = []
if OTEL_BASIC_AUTH_USERNAME and OTEL_BASIC_AUTH_PASSWORD:
auth_string = f"{OTEL_BASIC_AUTH_USERNAME}:{OTEL_BASIC_AUTH_PASSWORD}"
auth_string = f'{OTEL_BASIC_AUTH_USERNAME}:{OTEL_BASIC_AUTH_PASSWORD}'
auth_header = b64encode(auth_string.encode()).decode()
headers = [("authorization", f"Basic {auth_header}")]
headers = [('authorization', f'Basic {auth_header}')]
# otlp export
if OTEL_OTLP_SPAN_EXPORTER == "http":
if OTEL_OTLP_SPAN_EXPORTER == 'http':
exporter = HttpOTLPSpanExporter(
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
headers=headers,