fix(langtrace): use live app.langtrace.ai host and x-api-key header

The langtrace v1 exporter was hardcoded to https://langtrace.ai/api/trace with
an api_key= auth header. That host now 404s; langtrace's live OTLP ingest is
https://app.langtrace.ai/api/trace and it authenticates with x-api-key. Update
both, and teach the OTEL endpoint normalizer to treat a full /api/trace path as
a complete ingest URL so it is not rewritten to /api/trace/v1/traces.
This commit is contained in:
Yucheng Zhu 2026-07-27 17:33:11 -07:00
parent 8f86c87f8e
commit 4928001fce
3 changed files with 58 additions and 3 deletions

View file

@ -3093,6 +3093,10 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
if signal_type == "traces" and "/v2/trace/otlp" in endpoint:
return endpoint
# Langtrace ingests traces at /api/trace (a complete path, not an OTLP base). Do not rewrite.
if signal_type == "traces" and endpoint.endswith("/api/trace"):
return endpoint
# Check if endpoint already ends with the correct signal path
target_path = f"/v1/{signal_type}"
if endpoint.endswith(target_path):

View file

@ -3992,9 +3992,9 @@ def _init_custom_logger_compatible_class(
otel_config = OpenTelemetryConfig(
exporter="otlp_http",
endpoint="https://langtrace.ai/api/trace",
endpoint="https://app.langtrace.ai/api/trace",
)
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = f"api_key={os.getenv('LANGTRACE_API_KEY')}"
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = f"x-api-key={os.getenv('LANGTRACE_API_KEY')}"
for callback in _in_memory_loggers:
if isinstance(callback, OpenTelemetry) and callback.callback_name == "langtrace":
return callback # type: ignore

View file

@ -1928,12 +1928,21 @@ class TestOpenTelemetryEndpointNormalization(unittest.TestCase):
"https://example.com/prefix/v2/trace/otlp",
"https://example.com/prefix/v2/trace/otlp",
),
(
"https://app.langtrace.ai/api/trace",
"https://app.langtrace.ai/api/trace",
),
(
"https://app.langtrace.ai/api/trace/",
"https://app.langtrace.ai/api/trace",
),
]
)
def test_normalize_traces_nonstandard_otlp_ingest_urls_unchanged(
self, input_url: str, expected: str
) -> None:
"""Splunk-style /v2/trace/otlp endpoints must not get /v1/traces appended."""
"""Vendor full-path ingest URLs (Splunk /v2/trace/otlp, Langtrace /api/trace)
must not get /v1/traces appended that 404s."""
otel = OpenTelemetry()
self.assertEqual(
otel._normalize_otel_endpoint(input_url, "traces"),
@ -5852,3 +5861,45 @@ class TestOpenTelemetryMetricAttributeFiltering(unittest.TestCase):
exporter="console", attributes=attributes
)
)
class LangtraceV1ConfigTest(unittest.TestCase):
def test_langtrace_uses_live_host_and_x_api_key_header(self):
"""Regression: litellm hardcoded the stale https://langtrace.ai/api/trace (now 404)
with header ``api_key=``. The live ingest is https://app.langtrace.ai/api/trace and
the auth header is ``x-api-key``. Pins both so the stale values can't return."""
import litellm
import litellm.litellm_core_utils.litellm_logging as ll
from litellm.integrations.opentelemetry import OpenTelemetry
from litellm.integrations.otel.model.config import is_otel_v2_enabled
saved = {
k: os.environ.get(k)
for k in (
"LANGTRACE_API_KEY",
"LITELLM_OTEL_V2",
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
)
}
try:
ll._in_memory_loggers.clear() # force a fresh build, not a cached logger
os.environ["LANGTRACE_API_KEY"] = "lt-test"
os.environ.pop("LITELLM_OTEL_V2", None)
is_otel_v2_enabled.cache_clear()
litellm.credential_list = []
logger = ll._init_custom_logger_compatible_class("langtrace", None, None)
assert isinstance(logger, OpenTelemetry)
self.assertEqual(
logger.config.endpoint, "https://app.langtrace.ai/api/trace"
)
self.assertEqual(
os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"], "x-api-key=lt-test"
)
finally:
is_otel_v2_enabled.cache_clear()
ll._in_memory_loggers.clear()
for key, value in saved.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value