diff --git a/litellm/__init__.py b/litellm/__init__.py index eebd2dad91e..09a36388347 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -130,6 +130,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "arize", "arize_phoenix", "langtrace", + "telemetry_dev", "gcs_bucket", "azure_storage", "opik", diff --git a/litellm/integrations/telemetry_dev/__init__.py b/litellm/integrations/telemetry_dev/__init__.py new file mode 100644 index 00000000000..71d27a8fc4a --- /dev/null +++ b/litellm/integrations/telemetry_dev/__init__.py @@ -0,0 +1,3 @@ +from litellm.integrations.telemetry_dev.telemetry_dev import TelemetryDevLogger + +__all__ = ["TelemetryDevLogger"] # mutable-ok: module export list diff --git a/litellm/integrations/telemetry_dev/telemetry_dev.py b/litellm/integrations/telemetry_dev/telemetry_dev.py new file mode 100644 index 00000000000..b3475dbf809 --- /dev/null +++ b/litellm/integrations/telemetry_dev/telemetry_dev.py @@ -0,0 +1,38 @@ +import os + +from litellm.integrations.opentelemetry import OpenTelemetry +from litellm.types.integrations.arize import Protocol + + +class TelemetryDevConfig: + """Configuration for the telemetry.dev OTLP integration.""" + + def __init__( + self, + otlp_auth_headers: str, + protocol: Protocol, + endpoint: str, + ) -> None: + self.otlp_auth_headers = otlp_auth_headers + self.protocol = protocol + self.endpoint = endpoint + + +class TelemetryDevLogger(OpenTelemetry): + """telemetry.dev logger that extends OpenTelemetry for OTLP export.""" + + @staticmethod + def get_telemetry_dev_config() -> TelemetryDevConfig: + api_key = os.environ.get("TELEMETRY_DEV_API_KEY") + if not api_key: + raise ValueError( + "TELEMETRY_DEV_API_KEY environment variable is required for the telemetry.dev integration." + ) + + base_url = os.environ.get("TELEMETRY_DEV_BASE_URL", "https://ingest.telemetry.dev").rstrip("/") + + return TelemetryDevConfig( + otlp_auth_headers=f"Authorization=Bearer {api_key}", + protocol="otlp_http", + endpoint=f"{base_url}/v1/traces", + ) diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index 6449aa4d46e..6c2620db2bd 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -47,6 +47,7 @@ from litellm.integrations.posthog import PostHogLogger from litellm.integrations.prometheus import PrometheusLogger from litellm.integrations.s3_v2 import S3Logger from litellm.integrations.sqs import SQSLogger +from litellm.integrations.telemetry_dev.telemetry_dev import TelemetryDevLogger from litellm.integrations.vantage.vantage_logger import VantageLogger from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import ( VectorStorePreCallHook, @@ -85,6 +86,7 @@ class CustomLoggerRegistry: "langfuse_otel": OpenTelemetry, "arize_phoenix": OpenTelemetry, "langtrace": OpenTelemetry, + "telemetry_dev": TelemetryDevLogger, "weave_otel": OpenTelemetry, "levo": OpenTelemetry, "mlflow": MlflowLogger, diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 3018f0c4d24..dcb44090f5a 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -4327,6 +4327,26 @@ def _init_custom_logger_compatible_class( _levo_otel_logger: Final = LevoLogger(config=otel_config, callback_name="levo") _in_memory_loggers.append(_levo_otel_logger) return _levo_otel_logger + elif logging_integration == "telemetry_dev": + from litellm.integrations.opentelemetry import OpenTelemetryConfig + from litellm.integrations.telemetry_dev.telemetry_dev import ( + TelemetryDevLogger, + ) + + telemetry_dev_config = TelemetryDevLogger.get_telemetry_dev_config() + otel_config = OpenTelemetryConfig( + exporter=telemetry_dev_config.protocol, + endpoint=telemetry_dev_config.endpoint, + headers=telemetry_dev_config.otlp_auth_headers, + ) + + for callback in _in_memory_loggers: + if isinstance(callback, TelemetryDevLogger) and callback.callback_name == "telemetry_dev": + return callback + + telemetry_dev_logger = TelemetryDevLogger(config=otel_config, callback_name="telemetry_dev") + _in_memory_loggers.append(telemetry_dev_logger) + return telemetry_dev_logger elif logging_integration == "otel": # Gate the new typed V2 adapter behind LITELLM_OTEL_V2. When off, # the legacy 3,227-line god-class is used unchanged. The two are @@ -4926,6 +4946,15 @@ def get_custom_logger_compatible_class( if isinstance(callback, OpenTelemetry) and callback.callback_name == "langtrace": return callback + elif logging_integration == "telemetry_dev": + from litellm.integrations.telemetry_dev.telemetry_dev import ( + TelemetryDevLogger, + ) + + for callback in _in_memory_loggers: + if isinstance(callback, TelemetryDevLogger) and callback.callback_name == "telemetry_dev": + return callback + elif logging_integration == "mlflow": for callback in _in_memory_loggers: if isinstance(callback, MlflowLogger): diff --git a/tests/logging_callback_tests/test_unit_tests_init_callbacks.py b/tests/logging_callback_tests/test_unit_tests_init_callbacks.py index f8917ddee78..30fe09416da 100644 --- a/tests/logging_callback_tests/test_unit_tests_init_callbacks.py +++ b/tests/logging_callback_tests/test_unit_tests_init_callbacks.py @@ -38,6 +38,7 @@ expected_env_vars = { "GOOGLE_APPLICATION_CREDENTIALS": "gcs_credentials", "OPIK_API_KEY": "opik_api_key", "LANGTRACE_API_KEY": "langtrace_api_key", + "TELEMETRY_DEV_API_KEY": "td_live_test", "LOGFIRE_TOKEN": "logfire_token", "ARIZE_SPACE_KEY": "arize_space_key", "ARIZE_API_KEY": "arize_api_key", @@ -314,3 +315,36 @@ def test_get_combined_callback_list_returns_copy_when_dynamic_is_none(): combined_callbacks.append("new_callback") assert global_callbacks == ["langfuse"] + + +def test_telemetry_dev_callback_initialization(monkeypatch): + from litellm.integrations.telemetry_dev.telemetry_dev import TelemetryDevLogger + from litellm.litellm_core_utils.litellm_logging import ( + _init_custom_logger_compatible_class, + get_custom_logger_compatible_class, + ) + + monkeypatch.setenv("TELEMETRY_DEV_API_KEY", "td_live_test") + monkeypatch.delenv("TELEMETRY_DEV_BASE_URL", raising=False) + + config = TelemetryDevLogger.get_telemetry_dev_config() + assert config.endpoint == "https://ingest.telemetry.dev/v1/traces" + assert config.otlp_auth_headers == "Authorization=Bearer td_live_test" + assert config.protocol == "otlp_http" + + monkeypatch.setenv("TELEMETRY_DEV_BASE_URL", "https://example.com/") + assert TelemetryDevLogger.get_telemetry_dev_config().endpoint == "https://example.com/v1/traces" + monkeypatch.delenv("TELEMETRY_DEV_BASE_URL") + + logger = _init_custom_logger_compatible_class("telemetry_dev", internal_usage_cache=None, llm_router=None) + assert isinstance(logger, TelemetryDevLogger) + assert logger.callback_name == "telemetry_dev" + assert CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE["telemetry_dev"] is TelemetryDevLogger + + # init is cached and lookup resolves to the same instance + assert _init_custom_logger_compatible_class("telemetry_dev", internal_usage_cache=None, llm_router=None) is logger + assert get_custom_logger_compatible_class("telemetry_dev") is logger + + monkeypatch.delenv("TELEMETRY_DEV_API_KEY") + with pytest.raises(ValueError, match="TELEMETRY_DEV_API_KEY"): + TelemetryDevLogger.get_telemetry_dev_config()