mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
feat(otel): make the OTel v2 trace export URL configurable (#40286)
Add traces_endpoint (env OTEL_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, per-exporter key traces_endpoint, Admin UI field) as a complete OTLP/HTTP trace URL used verbatim, so collectors that do not serve /v1/traces can receive traces. endpoint keeps its existing base-URL + signal-path normalization. Resolves LIT-7218 Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
c899912f49
commit
29fe1e895f
5 changed files with 108 additions and 3 deletions
|
|
@ -356,6 +356,12 @@
|
|||
"description": "OpenTelemetry collector endpoint URL",
|
||||
"required": true
|
||||
},
|
||||
"otel_traces_endpoint": {
|
||||
"type": "text",
|
||||
"ui_name": "Traces Endpoint URL",
|
||||
"description": "Complete trace export URL used verbatim when the collector does not serve /v1/traces (OTel v2 only)",
|
||||
"required": false
|
||||
},
|
||||
"otel_headers": {
|
||||
"type": "text",
|
||||
"ui_name": "Headers",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ class ExporterSpec(BaseModel):
|
|||
description="console | in_memory | otlp_http | otlp_grpc | <factory kind>",
|
||||
)
|
||||
endpoint: str | None = None
|
||||
traces_endpoint: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Complete OTLP/HTTP trace URL, used verbatim. Set this when the "
|
||||
"collector serves traces on a path other than ``/v1/traces``; "
|
||||
"``endpoint`` is a base URL the signal path is appended to."
|
||||
),
|
||||
)
|
||||
headers: str | None = None
|
||||
owner: ExporterOwner | None = Field(
|
||||
default=None,
|
||||
|
|
@ -127,6 +135,14 @@ class OpenTelemetryV2Config(BaseSettings):
|
|||
default=None,
|
||||
validation_alias=AliasChoices("OTEL_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT"),
|
||||
)
|
||||
traces_endpoint: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("OTEL_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"),
|
||||
description=(
|
||||
"Complete OTLP/HTTP trace URL for the single-destination shorthand, "
|
||||
"used verbatim instead of ``endpoint`` + ``/v1/traces``."
|
||||
),
|
||||
)
|
||||
headers: str | None = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("OTEL_HEADERS", "OTEL_EXPORTER_OTLP_HEADERS"),
|
||||
|
|
@ -250,7 +266,7 @@ class OpenTelemetryV2Config(BaseSettings):
|
|||
@model_validator(mode="after")
|
||||
def _normalize(self) -> "OpenTelemetryV2Config":
|
||||
# An endpoint with the default exporter kind implies OTLP/HTTP.
|
||||
if self.endpoint and self.exporter == "console":
|
||||
if (self.endpoint or self.traces_endpoint) and self.exporter == "console":
|
||||
self.exporter = "otlp_http"
|
||||
# When no explicit destinations are given, fold the single-destination
|
||||
# shorthand into one spec so the provider always has a destination.
|
||||
|
|
@ -259,6 +275,7 @@ class OpenTelemetryV2Config(BaseSettings):
|
|||
ExporterSpec(
|
||||
kind=self.exporter,
|
||||
endpoint=self.endpoint,
|
||||
traces_endpoint=self.traces_endpoint,
|
||||
headers=self.headers,
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
|
|||
)
|
||||
|
||||
return HTTPExporter(
|
||||
endpoint=_otlp_traces_endpoint(spec.endpoint),
|
||||
endpoint=spec.traces_endpoint or _otlp_traces_endpoint(spec.endpoint),
|
||||
headers=parse_headers(spec.headers),
|
||||
)
|
||||
if kind in _OTLP_GRPC_KINDS:
|
||||
|
|
@ -201,7 +201,14 @@ def build_span_exporter(config: OpenTelemetryV2Config) -> SpanExporter:
|
|||
``exporter`` / ``endpoint`` / ``headers`` fields. To configure multiple
|
||||
exporters, populate ``config.exporters`` directly.
|
||||
"""
|
||||
return _exporter_from_spec(ExporterSpec(kind=config.exporter, endpoint=config.endpoint, headers=config.headers))
|
||||
return _exporter_from_spec(
|
||||
ExporterSpec(
|
||||
kind=config.exporter,
|
||||
endpoint=config.endpoint,
|
||||
traces_endpoint=config.traces_endpoint,
|
||||
headers=config.headers,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _otlp_metrics_endpoint(endpoint: str | None) -> str | None:
|
||||
|
|
|
|||
|
|
@ -3585,6 +3585,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase):
|
|||
litellm_callback_params=[
|
||||
"OTEL_EXPORTER",
|
||||
"OTEL_ENDPOINT",
|
||||
"OTEL_TRACES_ENDPOINT",
|
||||
"OTEL_HEADERS",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ baggage helpers, metrics, the typed coercion helpers, mapper branches, span-name
|
|||
builders, and the registry validator's failure paths. Needs the OTel SDK."""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import replace
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -538,6 +541,77 @@ def test_build_span_exporter_variants():
|
|||
assert "OTLPSpanExporter" in type(http_exporter).__name__
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def otlp_collector() -> Iterator[tuple[str, list[str]]]:
|
||||
received_paths: list[str] = []
|
||||
|
||||
class RecordingHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self) -> None:
|
||||
self.rfile.read(int(self.headers.get("Content-Length", "0")))
|
||||
received_paths.append(self.path)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
pass
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), RecordingHandler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
try:
|
||||
yield f"http://127.0.0.1:{server.server_port}", received_paths
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def _export_one_span(cfg: OpenTelemetryV2Config) -> None:
|
||||
provider = providers.build_tracer_provider(cfg)
|
||||
provider.get_tracer("probe").start_span("probe").end()
|
||||
assert provider.force_flush()
|
||||
provider.shutdown()
|
||||
|
||||
|
||||
def test_traces_endpoint_env_posts_to_the_configured_url_verbatim(monkeypatch, otlp_collector):
|
||||
base_url, received_paths = otlp_collector
|
||||
for var in ("OTEL_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_ENDPOINT"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setenv("OTEL_ENDPOINT", f"{base_url}/services/collector")
|
||||
monkeypatch.setenv("OTEL_TRACES_ENDPOINT", f"{base_url}/services/collector/traces")
|
||||
|
||||
cfg = OpenTelemetryV2Config.from_env()
|
||||
assert cfg.exporter == "otlp_http"
|
||||
_export_one_span(cfg)
|
||||
assert received_paths == ["/services/collector/traces"]
|
||||
|
||||
|
||||
def test_traces_endpoint_alias_alone_implies_otlp_http(monkeypatch, otlp_collector):
|
||||
base_url, received_paths = otlp_collector
|
||||
for var in ("OTEL_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", f"{base_url}/custom/traces")
|
||||
|
||||
cfg = OpenTelemetryV2Config.from_env()
|
||||
assert cfg.exporter == "otlp_http"
|
||||
_export_one_span(cfg)
|
||||
assert received_paths == ["/custom/traces"]
|
||||
|
||||
|
||||
def test_traces_endpoint_per_exporter_coexists_with_default_normalization(otlp_collector):
|
||||
base_url, received_paths = otlp_collector
|
||||
cfg = OpenTelemetryV2Config(
|
||||
exporters=[
|
||||
{"kind": "otlp_http", "endpoint": base_url},
|
||||
{
|
||||
"kind": "otlp_http",
|
||||
"endpoint": f"{base_url}/services/collector",
|
||||
"traces_endpoint": f"{base_url}/services/collector/traces",
|
||||
},
|
||||
]
|
||||
)
|
||||
_export_one_span(cfg)
|
||||
assert sorted(received_paths) == ["/services/collector/traces", "/v1/traces"]
|
||||
|
||||
|
||||
def test_otlp_metric_exporter_uses_cumulative_histogram_temporality():
|
||||
"""Histograms must export as cumulative, not delta.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue