litellm/tests/test_litellm/integrations/conftest.py
devin-ai-integration[bot] b5b116a519
fix(otel): honor SSL_CERT_FILE and ssl_verify in OTLP HTTP exporters (#42106)
* fix(otel): honor SSL_CERT_FILE and ssl_verify in OTLP HTTP exporters

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(otel): assert OTLP HTTP TLS behavior against a real TLS sink

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(otel): assert rejected exports by outcome, not by exception type

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(otel): honor SSL_CERT_FILE and ssl_verify in the v2 OTLP HTTP exporters

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(otel): hoist otlp_tls imports and type the TLS sink fixture

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(e2e): gate the OTLP TLS export test behind an otel_tls opt-in

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs(e2e): drop CONTRIBUTING.md edit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng <yucheng@berri.ai>
2026-09-22 20:50:53 +00:00

96 lines
3.4 KiB
Python

import functools
import http.server
import ipaddress
import queue
import ssl
import threading
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Final
import pytest
@dataclass(frozen=True, slots=True)
class TlsSink:
url: str
certificate_path: str
received: "queue.Queue[str]"
class _RecordingOtelHandler(http.server.BaseHTTPRequestHandler):
def __init__(self, *args: object, received: "queue.Queue[str]", **kwargs: object) -> None:
self._received: Final = received
super().__init__(*args, **kwargs)
def do_POST(self) -> None:
length: Final = int(self.headers.get("Content-Length") or 0)
if length:
self.rfile.read(length)
self._received.put(self.path)
self.send_response(200)
self.send_header("Content-Type", "application/x-protobuf")
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, format: str, *args: object) -> None:
pass
def write_self_signed_cert(directory: Path, stem: str) -> tuple[Path, Path]:
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
key: Final = rsa.generate_private_key(public_exponent=65537, key_size=2048)
name: Final = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")])
certificate: Final = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(timezone.utc) - timedelta(minutes=1))
.not_valid_after(datetime.now(timezone.utc) + timedelta(hours=1))
.add_extension(
x509.SubjectAlternativeName([x509.DNSName("localhost"), x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]),
critical=False,
)
.sign(key, hashes.SHA256())
)
certificate_path: Final = directory / f"{stem}.crt"
certificate_path.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
key_path: Final = directory / f"{stem}.key"
key_path.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
)
)
return certificate_path, key_path
@pytest.fixture
def tls_sink(tmp_path: Path) -> Iterator[TlsSink]:
certificate_path, key_path = write_self_signed_cert(tmp_path, "sink")
received: queue.Queue[str] = queue.Queue()
context: Final = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(str(certificate_path), str(key_path))
server: Final = http.server.ThreadingHTTPServer(
("127.0.0.1", 0), functools.partial(_RecordingOtelHandler, received=received)
)
server.socket = context.wrap_socket(server.socket, server_side=True)
thread: Final = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
yield TlsSink(
url=f"https://127.0.0.1:{server.server_port}",
certificate_path=str(certificate_path),
received=received,
)
server.shutdown()
server.server_close()
thread.join(timeout=5)