mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
fix(langfuse): honor ssl_verify=False and SSL_VERIFY on the v4 OTLP exporter
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
45fc3111dc
commit
dddc1e0ad8
2 changed files with 45 additions and 7 deletions
|
|
@ -431,22 +431,24 @@ def _build_verified_span_exporter(*, public_key: object, secret_key: object, bas
|
|||
bundle and client certificate; v4 ships every observation through its own
|
||||
OTLP exporter, so a private-CA deployment would fail TLS on every export in
|
||||
a background thread while ``auth_check`` (still on the httpx client) stays
|
||||
green. Only built when custom TLS material is configured; endpoint and
|
||||
headers mirror ``langfuse._client.span_processor``.
|
||||
green. Only built when TLS is configured away from the default (a CA bundle,
|
||||
a client certificate, or verification switched off); endpoint and headers
|
||||
mirror ``langfuse._client.span_processor``.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.http_handler import get_ssl_verify
|
||||
|
||||
ca_bundle: Final = litellm.ssl_verify if isinstance(litellm.ssl_verify, str) else None
|
||||
ssl_verify: Final = get_ssl_verify()
|
||||
configured_certificate: Final = os.getenv("SSL_CERTIFICATE") or litellm.ssl_certificate
|
||||
client_certificate: Final = configured_certificate if isinstance(configured_certificate, str) else None
|
||||
if ca_bundle is None and client_certificate is None:
|
||||
if ssl_verify is True and client_certificate is None:
|
||||
return None
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
||||
|
||||
export_path: Final = os.getenv("LANGFUSE_OTEL_TRACES_EXPORT_PATH")
|
||||
endpoint: Final = f"{base_url}/{export_path}" if export_path else f"{base_url}/api/public/otel/v1/traces"
|
||||
encoded_auth: Final = b64encode(f"{public_key}:{secret_key}".encode()).decode("ascii")
|
||||
return OTLPSpanExporter(
|
||||
exporter: Final = OTLPSpanExporter(
|
||||
endpoint=endpoint,
|
||||
headers={ # mutable-ok: the exporter copies these into its session headers
|
||||
"Authorization": "Basic " + encoded_auth,
|
||||
|
|
@ -454,9 +456,12 @@ def _build_verified_span_exporter(*, public_key: object, secret_key: object, bas
|
|||
"x-langfuse-sdk-version": version("langfuse"),
|
||||
"x-langfuse-public-key": str(public_key),
|
||||
},
|
||||
certificate_file=ca_bundle,
|
||||
certificate_file=ssl_verify if isinstance(ssl_verify, str) else None,
|
||||
client_certificate_file=client_certificate,
|
||||
)
|
||||
if ssl_verify is False:
|
||||
exporter._certificate_file = False # pyright: ignore[reportPrivateUsage] # the ctor coerces a False certificate_file back to True
|
||||
return exporter
|
||||
|
||||
|
||||
def acquire_langfuse_client(
|
||||
|
|
|
|||
|
|
@ -955,7 +955,8 @@ def test_ssl_exporter_is_only_built_with_custom_tls_material(monkeypatch, tmp_pa
|
|||
import litellm
|
||||
from litellm.integrations.langfuse.langfuse_sdk import _build_verified_span_exporter
|
||||
|
||||
monkeypatch.delenv("SSL_CERTIFICATE", raising=False)
|
||||
for name in ("SSL_CERTIFICATE", "SSL_VERIFY", "SSL_CERT_FILE"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setattr(litellm, "ssl_verify", True)
|
||||
monkeypatch.setattr(litellm, "ssl_certificate", None)
|
||||
assert (
|
||||
|
|
@ -973,6 +974,38 @@ def test_ssl_exporter_is_only_built_with_custom_tls_material(monkeypatch, tmp_pa
|
|||
assert exporter._headers["x-langfuse-sdk-version"] == installed_langfuse_version()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("switch", ["attribute", "env"])
|
||||
def test_ssl_exporter_disables_verification_when_litellm_does(monkeypatch, switch):
|
||||
"""v2 exported through the httpx client, so ``ssl_verify=False`` reached ingestion; v4's exporter must match."""
|
||||
import litellm
|
||||
from litellm.integrations.langfuse.langfuse_sdk import _build_verified_span_exporter
|
||||
|
||||
for name in ("SSL_CERTIFICATE", "SSL_VERIFY", "SSL_CERT_FILE"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
monkeypatch.setattr(litellm, "ssl_certificate", None)
|
||||
if switch == "attribute":
|
||||
monkeypatch.setattr(litellm, "ssl_verify", False)
|
||||
else:
|
||||
monkeypatch.setattr(litellm, "ssl_verify", True)
|
||||
monkeypatch.setenv("SSL_VERIFY", "False")
|
||||
|
||||
exporter = _build_verified_span_exporter(public_key="pk", secret_key="sk", base_url="https://lf.internal.example")
|
||||
assert exporter is not None
|
||||
assert exporter._certificate_file is False
|
||||
assert exporter._client_cert is None
|
||||
|
||||
posted = []
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
posted.append((url, kwargs["verify"]))
|
||||
raise ConnectionError("stop before the network")
|
||||
|
||||
monkeypatch.setattr("requests.Session.post", post)
|
||||
with pytest.raises(ConnectionError):
|
||||
exporter._export(b"payload")
|
||||
assert posted[0] == ("https://lf.internal.example/api/public/otel/v1/traces", False)
|
||||
|
||||
|
||||
def test_second_client_on_the_same_key_does_not_build_another_provider():
|
||||
"""A discarded TracerProvider is pinned forever by its atexit hook."""
|
||||
import gc
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue