From c3b80de7f8210a7917bf552f64bf2c39e4ccd68c Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 17:57:14 +0530 Subject: [PATCH 1/7] feat(otel): add SigNoz preset for OpenTelemetry v2 Adds a `signoz` callback so LiteLLM can export traces to SigNoz over OTLP, alongside the existing vendor presets. SigNoz Cloud and self-hosted share one endpoint field, `SIGNOZ_INGESTION_ENDPOINT`, with an optional `SIGNOZ_INGESTION_KEY`. There is no default host: the dispatch branch raises a ValueError naming the missing variable, matching how LangTrace, Logfire and Arize validate, so nothing is exported until a destination is set. `requires_headers` follows the key, since SigNoz Cloud rejects unauthenticated exports while a self-hosted collector accepts them. Per-team credentials are supported through `signoz_ingestion_key`, with a tile in the proxy admin UI. Teams supply a key and never a destination, so the operator's endpoint stays authoritative and no per-request endpoint resolver is registered. The legacy fallback resolves the signal path through `_otlp_traces_endpoint` and passes the key on `OpenTelemetryConfig.headers` rather than exporting it to `OTEL_EXPORTER_OTLP_TRACES_HEADERS`, keeping it off a process-global var that another backend's exporter would read. --- litellm/__init__.py | 1 + litellm/integrations/callback_configs.json | 15 +++++ litellm/integrations/otel/model/config.py | 1 + litellm/integrations/otel/presets/__init__.py | 7 +++ litellm/integrations/otel/presets/signoz.py | 57 +++++++++++++++++++ .../initialize_dynamic_callback_params.py | 3 + litellm/litellm_core_utils/litellm_logging.py | 34 +++++++++++ .../_experimental/out/assets/logos/signoz.svg | 1 + litellm/types/utils.py | 4 ++ .../integrations/otel/test_otel_v2_dynamic.py | 21 +++++++ .../integrations/otel/test_otel_v2_presets.py | 56 ++++++++++++++++++ .../public/assets/logos/signoz.svg | 1 + 12 files changed, 201 insertions(+) create mode 100644 litellm/integrations/otel/presets/signoz.py create mode 100644 litellm/proxy/_experimental/out/assets/logos/signoz.svg create mode 100644 ui/litellm-dashboard/public/assets/logos/signoz.svg diff --git a/litellm/__init__.py b/litellm/__init__.py index e95b553c5d4..5c8e87738d6 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -163,6 +163,7 @@ _custom_logger_compatible_callbacks_literal = Literal[ "levo", "compression_interception", "newrelic", + "signoz", ] cold_storage_custom_logger: Optional[_custom_logger_compatible_callbacks_literal] = None logged_real_time_event_types: Optional[Union[List[str], Literal["*"]]] = None diff --git a/litellm/integrations/callback_configs.json b/litellm/integrations/callback_configs.json index 6d2bcea8bae..a1a6c2324f5 100644 --- a/litellm/integrations/callback_configs.json +++ b/litellm/integrations/callback_configs.json @@ -404,6 +404,21 @@ }, "description": "S3 Bucket (AWS) Logging Integration" }, + { + "id": "signoz", + "displayName": "SigNoz", + "logo": "signoz.svg", + "supports_key_team_logging": true, + "dynamic_params": { + "signoz_ingestion_key": { + "type": "password", + "ui_name": "SigNoz Ingestion Key", + "description": "Per-team ingestion key. Team traces export to this key's SigNoz account over OTLP.", + "required": false + } + }, + "description": "SigNoz OpenTelemetry Observability Integration" + }, { "id": "sqs", "displayName": "SQS", diff --git a/litellm/integrations/otel/model/config.py b/litellm/integrations/otel/model/config.py index 9e3064c2bff..bb8e67e826c 100644 --- a/litellm/integrations/otel/model/config.py +++ b/litellm/integrations/otel/model/config.py @@ -40,6 +40,7 @@ class ExporterOwner(str, Enum): LEVO = "levo" AGENTOPS = "agentops" NEWRELIC = "newrelic" + SIGNOZ = "signoz" class _OTelV2Flag(BaseSettings): diff --git a/litellm/integrations/otel/presets/__init__.py b/litellm/integrations/otel/presets/__init__.py index a0cd5b3fd98..1e5177b2320 100644 --- a/litellm/integrations/otel/presets/__init__.py +++ b/litellm/integrations/otel/presets/__init__.py @@ -30,6 +30,10 @@ from litellm.integrations.otel.presets.phoenix import ( phoenix_preset, phoenix_project_headers, ) +from litellm.integrations.otel.presets.signoz import ( + signoz_dynamic_headers, + signoz_preset, +) from litellm.integrations.otel.presets.weave import weave_dynamic_headers, weave_preset from litellm.types.utils import StandardCallbackDynamicParams @@ -44,6 +48,7 @@ PRESET_BY_CALLBACK: Final[Mapping[str, Preset]] = MappingProxyType( "langtrace": langtrace_preset, "levo": levo_preset, "newrelic": newrelic_preset, + "signoz": signoz_preset, "weave_otel": weave_preset, } ) @@ -58,6 +63,7 @@ DYNAMIC_HEADERS_BY_CALLBACK: Final[Mapping[str, Callable[[StandardCallbackDynami "arize": arize_dynamic_headers, "langfuse_otel": langfuse_dynamic_headers, "newrelic": newrelic_dynamic_headers, + "signoz": signoz_dynamic_headers, "weave_otel": weave_dynamic_headers, } ) @@ -153,5 +159,6 @@ __all__ = [ "newrelic_preset", "phoenix_preset", "project_routing_headers", + "signoz_preset", "weave_preset", ] diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py new file mode 100644 index 00000000000..ac67b639bee --- /dev/null +++ b/litellm/integrations/otel/presets/signoz.py @@ -0,0 +1,57 @@ +"""SigNoz preset — OTLP/HTTP exporter to SigNoz + GenAI vocabulary.""" + +from typing import Final + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + +from litellm.integrations.otel.model.config import ( + ExporterOwner, + ExporterSpec, + OpenTelemetryV2Config, +) +from litellm.integrations.otel.presets.utils import ensure_mappers +from litellm.types.utils import StandardCallbackDynamicParams + +SIGNOZ_INGESTION_ENDPOINT_ENV: Final = "SIGNOZ_INGESTION_ENDPOINT" + + +class _SigNozSettings(BaseSettings): + model_config = SettingsConfigDict(case_sensitive=False, extra="ignore") + + # One endpoint for both SigNoz Cloud and self-hosted, with no default host, so + # nothing exports until the operator names a destination. + endpoint: str | None = Field(default=None, validation_alias=SIGNOZ_INGESTION_ENDPOINT_ENV) + ingestion_key: str | None = Field(default=None, validation_alias="SIGNOZ_INGESTION_KEY") + + +def signoz_preset( + *, + config_overrides: OpenTelemetryV2Config | None = None, +) -> OpenTelemetryV2Config: + settings: Final = _SigNozSettings() + base: Final = config_overrides or OpenTelemetryV2Config() + key: Final = settings.ingestion_key + return base.model_copy( + update={ + "exporters": [ + *base.exporters, + ExporterSpec( + kind="otlp_http", + endpoint=settings.endpoint, + headers=(f"signoz-ingestion-key={key}" if key else None), + owner=ExporterOwner.SIGNOZ, + # Cloud rejects keyless exports; a self-hosted collector accepts them. + requires_headers=bool(key), + ), + ], + # SigNoz ingests the OTLP GenAI semantic conventions natively. + "mapper_names": ensure_mappers(base.mapper_names, "genai"), + } + ) + + +def signoz_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]: + """Per-request SigNoz OTLP headers from team/key dynamic params.""" + ingestion_key: Final = params.get("signoz_ingestion_key") + return {header: value for header, value in (("signoz-ingestion-key", ingestion_key),) if value} diff --git a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py index 3b42ca4eaaf..1658c4cb01d 100644 --- a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py +++ b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py @@ -74,6 +74,7 @@ _supported_callback_params: Final[tuple[str, ...]] = ( "dd_agent_port", "newrelic_api_key", "newrelic_region", + "signoz_ingestion_key", "turn_off_message_logging", ) @@ -87,6 +88,7 @@ _request_blocked_callback_params: Final = frozenset( "dd_agent_port", "newrelic_api_key", "newrelic_region", + "signoz_ingestion_key", } ) @@ -99,6 +101,7 @@ _trusted_overlay_callback_params: Final = frozenset( { "newrelic_api_key", "newrelic_region", + "signoz_ingestion_key", } ) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 3ad4c187b6d..0cc74092730 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -4338,6 +4338,40 @@ def _init_custom_logger_compatible_class( _in_memory_loggers.append(_otel_logger) return _otel_logger + elif logging_integration == "signoz": + from litellm.integrations.otel.presets.signoz import ( + SIGNOZ_INGESTION_ENDPOINT_ENV, + ) + + _signoz_endpoint = os.getenv(SIGNOZ_INGESTION_ENDPOINT_ENV) + if not _signoz_endpoint: + raise ValueError(f"{SIGNOZ_INGESTION_ENDPOINT_ENV} not found in environment variables") + + _v2 = _maybe_construct_otel_v2("signoz", _in_memory_loggers) + if _v2 is not None: + return _v2 + + from litellm.integrations.opentelemetry import ( + OpenTelemetry, + OpenTelemetryConfig, + ) + from litellm.integrations.otel.plumbing.providers import ( + _otlp_traces_endpoint, + ) + + _signoz_key = os.getenv("SIGNOZ_INGESTION_KEY") + otel_config = OpenTelemetryConfig( + exporter="otlp_http", + endpoint=_otlp_traces_endpoint(_signoz_endpoint), + headers=(f"signoz-ingestion-key={_signoz_key}" if _signoz_key else None), + ) + for callback in _in_memory_loggers: + if isinstance(callback, OpenTelemetry) and callback.callback_name == "signoz": + return callback + _signoz_otel_logger = OpenTelemetry(config=otel_config, callback_name="signoz") + _in_memory_loggers.append(_signoz_otel_logger) + return _signoz_otel_logger + elif logging_integration == "mlflow": for callback in _in_memory_loggers: if isinstance(callback, MlflowLogger): diff --git a/litellm/proxy/_experimental/out/assets/logos/signoz.svg b/litellm/proxy/_experimental/out/assets/logos/signoz.svg new file mode 100644 index 00000000000..9064cb86bd6 --- /dev/null +++ b/litellm/proxy/_experimental/out/assets/logos/signoz.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 67eae2b4f21..af186e7c1bd 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3305,6 +3305,10 @@ class StandardCallbackDynamicParams(TypedDict, total=False): newrelic_api_key: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict newrelic_region: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict + # SigNoz dynamic params (proxy-stamped team/key callback vars only; + # request-supplied values are blocked) + signoz_ingestion_key: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict + # Logging settings turn_off_message_logging: bool | None # when true will not log messages litellm_disabled_callbacks: list[str] | None diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py index 633be9f105f..334dfb2f2d1 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py @@ -458,3 +458,24 @@ def test_newrelic_key_only_team_routes_to_us_not_operator_region(monkeypatch): ) owned = next(e for e in new_cfg.exporters if e.owner == "newrelic") assert owned.endpoint == "https://otlp.nr-data.net" + + +def test_signoz_dynamic_headers_stamp_ingestion_key(): + from litellm.integrations.otel.presets import dynamic_otlp_headers + + assert dynamic_otlp_headers("signoz", {"signoz_ingestion_key": "team-key"}) == { + "signoz-ingestion-key": "team-key" + } + # No key means no per-request routing; the caller keeps its default tracer. + assert dynamic_otlp_headers("signoz", {}) is None + + +def test_signoz_registers_no_dynamic_endpoint_resolver(): + # A team supplies a key, never a destination, so the operator's endpoint wins. + from litellm.integrations.otel.presets import ( + DYNAMIC_ENDPOINT_BY_CALLBACK, + dynamic_otlp_endpoint, + ) + + assert "signoz" not in DYNAMIC_ENDPOINT_BY_CALLBACK + assert dynamic_otlp_endpoint("signoz", {"signoz_ingestion_key": "k"}) is None diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_presets.py b/tests/test_litellm/integrations/otel/test_otel_v2_presets.py index 58cfc1ceb3f..32ffa777a76 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_presets.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_presets.py @@ -212,3 +212,59 @@ def test_newrelic_preset_unset_content_knob_keeps_default(monkeypatch): from litellm.integrations.otel.presets.newrelic import newrelic_preset assert newrelic_preset().capture_span_content is False + + +def test_signoz_preset_reads_env_endpoint_and_key(monkeypatch): + monkeypatch.setenv("SIGNOZ_INGESTION_ENDPOINT", "https://ingest.eu.signoz.cloud:443") + monkeypatch.setenv("SIGNOZ_INGESTION_KEY", "env-ingestion-key") + from litellm.integrations.otel.model.config import ExporterOwner + from litellm.integrations.otel.presets.signoz import signoz_preset + + cfg = signoz_preset() + spec = next(e for e in cfg.exporters if e.owner == ExporterOwner.SIGNOZ) + assert spec.kind == "otlp_http" + assert spec.endpoint == "https://ingest.eu.signoz.cloud:443" + assert spec.headers == "signoz-ingestion-key=env-ingestion-key" + assert spec.requires_headers is True + assert "genai" in cfg.mapper_names + + +def test_signoz_preset_without_key_is_self_hosted(monkeypatch): + # A self-hosted collector accepts unauthenticated OTLP, so requiring headers + # would drop exports that would have succeeded. + monkeypatch.setenv("SIGNOZ_INGESTION_ENDPOINT", "http://signoz-collector.internal:4318") + monkeypatch.delenv("SIGNOZ_INGESTION_KEY", raising=False) + from litellm.integrations.otel.model.config import ExporterOwner + from litellm.integrations.otel.presets.signoz import signoz_preset + + cfg = signoz_preset() + spec = next(e for e in cfg.exporters if e.owner == ExporterOwner.SIGNOZ) + assert spec.endpoint == "http://signoz-collector.internal:4318" + assert spec.headers is None + assert spec.requires_headers is False + + +def test_signoz_preset_has_no_default_endpoint(monkeypatch): + # No region table and no default host: the preset never invents a destination. + monkeypatch.delenv("SIGNOZ_INGESTION_ENDPOINT", raising=False) + monkeypatch.delenv("SIGNOZ_INGESTION_KEY", raising=False) + from litellm.integrations.otel.model.config import ExporterOwner + from litellm.integrations.otel.presets.signoz import signoz_preset + + cfg = signoz_preset() + spec = next(e for e in cfg.exporters if e.owner == ExporterOwner.SIGNOZ) + assert spec.endpoint is None + + +def test_signoz_preset_endpoint_passed_through_verbatim(monkeypatch): + # The plumbing appends the signal path, so pre-appending would double it. + monkeypatch.setenv("SIGNOZ_INGESTION_ENDPOINT", "https://ingest.us.signoz.cloud:443/v1/traces") + monkeypatch.delenv("SIGNOZ_INGESTION_KEY", raising=False) + from litellm.integrations.otel.model.config import ExporterOwner + from litellm.integrations.otel.plumbing.providers import _otlp_traces_endpoint + from litellm.integrations.otel.presets.signoz import signoz_preset + + cfg = signoz_preset() + spec = next(e for e in cfg.exporters if e.owner == ExporterOwner.SIGNOZ) + assert spec.endpoint == "https://ingest.us.signoz.cloud:443/v1/traces" + assert _otlp_traces_endpoint(spec.endpoint) == "https://ingest.us.signoz.cloud:443/v1/traces" diff --git a/ui/litellm-dashboard/public/assets/logos/signoz.svg b/ui/litellm-dashboard/public/assets/logos/signoz.svg new file mode 100644 index 00000000000..9064cb86bd6 --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/signoz.svg @@ -0,0 +1 @@ + \ No newline at end of file From 0e811770d645b8cf9b92f5116189c61f20d3ef1b Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 19:46:27 +0530 Subject: [PATCH 2/7] fix(otel): annotate SigNoz mutable collections for the type-discipline gate The dict and list constructions and the header builder's return annotation each tripped LIT001/LIT002. Each is required by the surrounding contract, so they carry a `mutable-ok` reason rather than being restructured. --- litellm/integrations/otel/presets/signoz.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py index ac67b639bee..6d7bcac0046 100644 --- a/litellm/integrations/otel/presets/signoz.py +++ b/litellm/integrations/otel/presets/signoz.py @@ -33,8 +33,8 @@ def signoz_preset( base: Final = config_overrides or OpenTelemetryV2Config() key: Final = settings.ingestion_key return base.model_copy( - update={ - "exporters": [ + update={ # mutable-ok: model_copy takes a dict of field updates + "exporters": [ # mutable-ok: matches the config's exporters list *base.exporters, ExporterSpec( kind="otlp_http", @@ -51,7 +51,7 @@ def signoz_preset( ) -def signoz_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]: +def signoz_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]: # mutable-ok: registry type """Per-request SigNoz OTLP headers from team/key dynamic params.""" - ingestion_key: Final = params.get("signoz_ingestion_key") - return {header: value for header, value in (("signoz-ingestion-key", ingestion_key),) if value} + key: Final = params.get("signoz_ingestion_key") + return {header: value for header, value in (("signoz-ingestion-key", key),) if value} # mutable-ok: registry type From f5e2d68e453549e34d29ea70e6df5623d6ad9acc Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 19:46:28 +0530 Subject: [PATCH 3/7] test(otel): cover the SigNoz callback dispatch branch Adds the three cases the newrelic dispatch already has: the OTel v2 logger when the flag is on, the legacy OpenTelemetry logger when it is off, and no logger at all when no endpoint is configured. --- .../test_litellm_logging.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 1c706be51fa..761e054bf4c 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -5278,3 +5278,100 @@ def test_get_custom_logger_compatible_class_finds_v2_newrelic(monkeypatch): logging_module._in_memory_loggers.clear() monkeypatch.delenv("LITELLM_OTEL_V2", raising=False) is_otel_v2_enabled.cache_clear() + + +def test_signoz_dispatch_prefers_otel_v2_when_flag_on(monkeypatch): + """With LITELLM_OTEL_V2 on, the "signoz" callback builds the OTel v2 logger + carrying the preset's exporter; the same name must resolve to one instance.""" + from litellm.integrations.otel.logger import OpenTelemetryV2 + from litellm.integrations.otel.model.config import ExporterOwner, is_otel_v2_enabled + from litellm.litellm_core_utils import litellm_logging as logging_module + + logging_module._in_memory_loggers.clear() + monkeypatch.setenv("LITELLM_OTEL_V2", "true") + monkeypatch.setenv("SIGNOZ_INGESTION_ENDPOINT", "https://ingest.eu.signoz.cloud:443") + monkeypatch.setenv("SIGNOZ_INGESTION_KEY", "test-key") + is_otel_v2_enabled.cache_clear() + try: + v2_logger = logging_module._init_custom_logger_compatible_class( + logging_integration="signoz", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + assert isinstance(v2_logger, OpenTelemetryV2) + assert v2_logger.callback_name == "signoz" + spec = next(e for e in v2_logger.config.exporters if e.owner == ExporterOwner.SIGNOZ) + assert spec.endpoint == "https://ingest.eu.signoz.cloud:443" + assert spec.headers == "signoz-ingestion-key=test-key" + again = logging_module._init_custom_logger_compatible_class( + logging_integration="signoz", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + assert again is v2_logger + finally: + logging_module._in_memory_loggers.clear() + monkeypatch.delenv("LITELLM_OTEL_V2", raising=False) + is_otel_v2_enabled.cache_clear() + + +def test_signoz_dispatch_keeps_legacy_otel_when_flag_off(monkeypatch): + """With the flag off the callback still works, through the generic OTel + logger. The signal path is resolved and the key rides on the config rather + than OTEL_EXPORTER_OTLP_TRACES_HEADERS, which any other exporter would read.""" + from litellm.integrations.opentelemetry import OpenTelemetry + from litellm.integrations.otel.model.config import is_otel_v2_enabled + from litellm.litellm_core_utils import litellm_logging as logging_module + + logging_module._in_memory_loggers.clear() + monkeypatch.delenv("LITELLM_OTEL_V2", raising=False) + monkeypatch.setenv("SIGNOZ_INGESTION_ENDPOINT", "http://signoz-collector.internal:4318") + monkeypatch.setenv("SIGNOZ_INGESTION_KEY", "legacy-key") + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_HEADERS", raising=False) + is_otel_v2_enabled.cache_clear() + try: + legacy = logging_module._init_custom_logger_compatible_class( + logging_integration="signoz", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + assert isinstance(legacy, OpenTelemetry) + assert legacy.callback_name == "signoz" + assert legacy.config.endpoint == "http://signoz-collector.internal:4318/v1/traces" + assert legacy.config.headers == "signoz-ingestion-key=legacy-key" + assert "OTEL_EXPORTER_OTLP_TRACES_HEADERS" not in os.environ + finally: + logging_module._in_memory_loggers.clear() + is_otel_v2_enabled.cache_clear() + + +def test_signoz_dispatch_requires_an_endpoint(monkeypatch): + """There is no default host, so an unset endpoint has nowhere to export. The + branch rejects it before constructing anything, and the caller's non-blocking + handler turns that into no logger at all rather than a silent console exporter.""" + from litellm.integrations.otel.model.config import is_otel_v2_enabled + from litellm.litellm_core_utils import litellm_logging as logging_module + + logging_module._in_memory_loggers.clear() + monkeypatch.setenv("LITELLM_OTEL_V2", "true") + monkeypatch.delenv("SIGNOZ_INGESTION_ENDPOINT", raising=False) + monkeypatch.delenv("SIGNOZ_INGESTION_KEY", raising=False) + is_otel_v2_enabled.cache_clear() + try: + created = logging_module._init_custom_logger_compatible_class( + logging_integration="signoz", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + assert created is None + assert not [ + cb for cb in logging_module._in_memory_loggers if getattr(cb, "callback_name", None) == "signoz" + ] + finally: + logging_module._in_memory_loggers.clear() + monkeypatch.delenv("LITELLM_OTEL_V2", raising=False) + is_otel_v2_enabled.cache_clear() From ee6a6d8f6819533ac6f70b17a7632d18c91ce3e8 Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 22:07:55 +0530 Subject: [PATCH 4/7] feat(otel): register SigNoz across the remaining callback surfaces Walking every file that mentions an existing preset turned up registration points the first pass missed, each failing somewhere different: the callback was absent from the logger registry, from the `AllCallbacks` list behind `/get/config/callbacks`, and from the `/health/services` service list, so the Admin UI's Test button returned a 400. The proxy tile also offered only an ingestion key, leaving no way to say where traces should go; the endpoint is now a team/key param alongside it, as every other logging tile has one. Left blank it keeps the proxy's configured endpoint. `signoz_*` callback vars are scoped to the `signoz` callback, matching the existing per-vendor guard: those vars reach the tracer through the trusted overlay with no callback-name check, so a team that saved them under a different callback never asked for SigNoz and must not export to it. Adds the callback to the dashboard's own callback list too, which is what the per-team logging settings read. --- litellm/integrations/callback_configs.json | 12 +++++-- litellm/integrations/otel/presets/__init__.py | 2 ++ litellm/integrations/otel/presets/signoz.py | 9 ++++++ .../custom_logger_registry.py | 1 + .../initialize_dynamic_callback_params.py | 3 ++ litellm/proxy/_types.py | 9 ++++++ .../health_endpoints/_health_endpoints.py | 2 ++ litellm/proxy/litellm_pre_call_utils.py | 2 ++ litellm/types/utils.py | 1 + .../integrations/otel/test_otel_v2_dynamic.py | 15 ++++----- .../test_litellm_logging.py | 8 +++++ .../proxy/test_litellm_pre_call_utils.py | 31 +++++++++++++++++++ .../src/components/callback_info_helpers.tsx | 12 +++++++ 13 files changed, 97 insertions(+), 10 deletions(-) diff --git a/litellm/integrations/callback_configs.json b/litellm/integrations/callback_configs.json index a1a6c2324f5..6695333e623 100644 --- a/litellm/integrations/callback_configs.json +++ b/litellm/integrations/callback_configs.json @@ -410,14 +410,20 @@ "logo": "signoz.svg", "supports_key_team_logging": true, "dynamic_params": { + "signoz_ingestion_endpoint": { + "type": "text", + "ui_name": "SigNoz Ingestion Endpoint", + "description": "Ingestion endpoint for this team, e.g. https://ingest.us.signoz.cloud:443 for SigNoz Cloud or your own collector. Leave blank to use the proxy's configured endpoint. Regions: https://signoz.io/docs/ingestion/signoz-cloud/overview/", + "required": false + }, "signoz_ingestion_key": { "type": "password", - "ui_name": "SigNoz Ingestion Key", - "description": "Per-team ingestion key. Team traces export to this key's SigNoz account over OTLP.", + "ui_name": "SigNoz Ingestion Key (optional)", + "description": "Ingestion key for this team, so its traces land in its own SigNoz account. Not needed for self-hosted SigNoz. Keys: https://signoz.io/docs/ingestion/signoz-cloud/keys/", "required": false } }, - "description": "SigNoz OpenTelemetry Observability Integration" + "description": "SigNoz Logging Integration. Setup: https://signoz.io/docs/litellm-observability/" }, { "id": "sqs", diff --git a/litellm/integrations/otel/presets/__init__.py b/litellm/integrations/otel/presets/__init__.py index 1e5177b2320..7c891c29409 100644 --- a/litellm/integrations/otel/presets/__init__.py +++ b/litellm/integrations/otel/presets/__init__.py @@ -31,6 +31,7 @@ from litellm.integrations.otel.presets.phoenix import ( phoenix_project_headers, ) from litellm.integrations.otel.presets.signoz import ( + signoz_dynamic_endpoint, signoz_dynamic_headers, signoz_preset, ) @@ -77,6 +78,7 @@ DYNAMIC_ENDPOINT_BY_CALLBACK: Final[Mapping[str, Callable[[StandardCallbackDynam MappingProxyType( { "newrelic": newrelic_dynamic_endpoint, + "signoz": signoz_dynamic_endpoint, } ) ) diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py index 6d7bcac0046..7008905481b 100644 --- a/litellm/integrations/otel/presets/signoz.py +++ b/litellm/integrations/otel/presets/signoz.py @@ -51,6 +51,15 @@ def signoz_preset( ) +def signoz_dynamic_endpoint(params: StandardCallbackDynamicParams) -> str | None: + """Per-request SigNoz endpoint from team/key dynamic params. + + ``None`` keeps the operator's endpoint, so a team that saved only a key + still exports to the configured destination. + """ + return params.get("signoz_ingestion_endpoint") + + def signoz_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]: # mutable-ok: registry type """Per-request SigNoz OTLP headers from team/key dynamic params.""" key: Final = params.get("signoz_ingestion_key") diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index 6449aa4d46e..f7128055edf 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -87,6 +87,7 @@ class CustomLoggerRegistry: "langtrace": OpenTelemetry, "weave_otel": OpenTelemetry, "levo": OpenTelemetry, + "signoz": OpenTelemetry, "mlflow": MlflowLogger, "langfuse": LangfusePromptManagement, "otel": OpenTelemetry, diff --git a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py index 1658c4cb01d..3c440e35fc4 100644 --- a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py +++ b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py @@ -74,6 +74,7 @@ _supported_callback_params: Final[tuple[str, ...]] = ( "dd_agent_port", "newrelic_api_key", "newrelic_region", + "signoz_ingestion_endpoint", "signoz_ingestion_key", "turn_off_message_logging", ) @@ -88,6 +89,7 @@ _request_blocked_callback_params: Final = frozenset( "dd_agent_port", "newrelic_api_key", "newrelic_region", + "signoz_ingestion_endpoint", "signoz_ingestion_key", } ) @@ -101,6 +103,7 @@ _trusted_overlay_callback_params: Final = frozenset( { "newrelic_api_key", "newrelic_region", + "signoz_ingestion_endpoint", "signoz_ingestion_key", } ) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0840d37ffa1..c904cdb5ecb 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3500,6 +3500,15 @@ class AllCallbacks(LiteLLMPydanticObjectBase): ], ) + signoz: CallbackOnUI = CallbackOnUI( + litellm_callback_name="signoz", + ui_callback_name="SigNoz", + litellm_callback_params=[ + "SIGNOZ_INGESTION_ENDPOINT", + "SIGNOZ_INGESTION_KEY", + ], + ) + class SpendLogsMetadata(TypedDict): """ diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 33894777bc3..e49ba9361a1 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -174,6 +174,7 @@ services = ( "arize", "galileo", "newrelic", + "signoz", "sqs", ] | str @@ -251,6 +252,7 @@ async def health_services_endpoint( "arize", "galileo", "newrelic", + "signoz", "sqs", ]: raise HTTPException( diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index cb5002e431b..9ba70d870c3 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -760,6 +760,8 @@ def convert_key_logging_metadata_to_callback( # must not export to it. if var.startswith("newrelic_") and data.callback_name != "newrelic": continue + if var.startswith("signoz_") and data.callback_name != "signoz": + continue if team_callback_settings_obj.callback_vars is None: team_callback_settings_obj.callback_vars = {} team_callback_settings_obj.callback_vars[var] = str(value) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index af186e7c1bd..ee5d3e7c241 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3307,6 +3307,7 @@ class StandardCallbackDynamicParams(TypedDict, total=False): # SigNoz dynamic params (proxy-stamped team/key callback vars only; # request-supplied values are blocked) + signoz_ingestion_endpoint: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict signoz_ingestion_key: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict # Logging settings diff --git a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py index 334dfb2f2d1..d17c38f3f3f 100644 --- a/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py +++ b/tests/test_litellm/integrations/otel/test_otel_v2_dynamic.py @@ -470,12 +470,13 @@ def test_signoz_dynamic_headers_stamp_ingestion_key(): assert dynamic_otlp_headers("signoz", {}) is None -def test_signoz_registers_no_dynamic_endpoint_resolver(): - # A team supplies a key, never a destination, so the operator's endpoint wins. - from litellm.integrations.otel.presets import ( - DYNAMIC_ENDPOINT_BY_CALLBACK, - dynamic_otlp_endpoint, - ) +def test_signoz_dynamic_endpoint_comes_from_team_config(): + from litellm.integrations.otel.presets import dynamic_otlp_endpoint - assert "signoz" not in DYNAMIC_ENDPOINT_BY_CALLBACK + assert ( + dynamic_otlp_endpoint("signoz", {"signoz_ingestion_endpoint": "https://ingest.eu.signoz.cloud:443"}) + == "https://ingest.eu.signoz.cloud:443" + ) + # A team that saved only a key keeps the operator's configured endpoint. assert dynamic_otlp_endpoint("signoz", {"signoz_ingestion_key": "k"}) is None + assert dynamic_otlp_endpoint("signoz", {}) is None diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 761e054bf4c..64581a7bb2b 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -5343,6 +5343,14 @@ def test_signoz_dispatch_keeps_legacy_otel_when_flag_off(monkeypatch): assert legacy.config.endpoint == "http://signoz-collector.internal:4318/v1/traces" assert legacy.config.headers == "signoz-ingestion-key=legacy-key" assert "OTEL_EXPORTER_OTLP_TRACES_HEADERS" not in os.environ + # Same name resolves to the same instance, not a second exporter. + again = logging_module._init_custom_logger_compatible_class( + logging_integration="signoz", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + assert again is legacy finally: logging_module._in_memory_loggers.clear() is_otel_v2_enabled.cache_clear() diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 50ef6f29ec2..b7847d04eff 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -7418,3 +7418,34 @@ def test_newrelic_vars_scoped_to_newrelic_callback_entry(): None, ) assert legit.callback_vars == {"newrelic_api_key": "REAL", "newrelic_region": "us"} + + +def test_signoz_callback_vars_are_scoped_to_the_signoz_callback(): + """``signoz_*`` vars reach the tracer through the trusted overlay with no + callback-name check, so a team that saved them under a different callback + never asked for SigNoz and must not export to it.""" + from litellm.proxy._types import AddTeamCallback + from litellm.proxy.litellm_pre_call_utils import convert_key_logging_metadata_to_callback + + under_signoz = convert_key_logging_metadata_to_callback( + data=AddTeamCallback( + callback_name="signoz", + callback_type="success", + callback_vars={"signoz_ingestion_key": "team-key", "signoz_ingestion_endpoint": "https://ingest.eu.signoz.cloud:443"}, + ), + team_callback_settings_obj=None, + ) + assert under_signoz.callback_vars == { + "signoz_ingestion_key": "team-key", + "signoz_ingestion_endpoint": "https://ingest.eu.signoz.cloud:443", + } + + under_other = convert_key_logging_metadata_to_callback( + data=AddTeamCallback( + callback_name="langfuse", + callback_type="success", + callback_vars={"signoz_ingestion_key": "team-key", "langfuse_host": "https://cloud.langfuse.com"}, + ), + team_callback_settings_obj=None, + ) + assert under_other.callback_vars == {"langfuse_host": "https://cloud.langfuse.com"} diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx index 3906aa744f7..046d67107c6 100644 --- a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx @@ -9,6 +9,7 @@ import langsmithLogo from "../../public/assets/logos/langsmith.png"; import newrelicLogo from "../../public/assets/logos/newrelic.png"; import openmeterLogo from "../../public/assets/logos/openmeter.png"; import otelLogo from "../../public/assets/logos/otel.png"; +import signozLogo from "../../public/assets/logos/signoz.svg"; interface CallbackConfig { id: string; @@ -172,6 +173,17 @@ export const CALLBACK_CONFIGS: CallbackConfig[] = [ }, description: "S3 Bucket (AWS) Logging Integration", }, + { + id: "signoz", + displayName: "SigNoz", + logo: signozLogo.src, + supports_key_team_logging: true, + dynamic_params: { + signoz_ingestion_endpoint: "text", + signoz_ingestion_key: "password", + }, + description: "SigNoz Logging Integration. Setup: https://signoz.io/docs/litellm-observability/", + }, { id: "SQS", displayName: "SQS", From 5f6130188548554343bc7c17e0c744b36c382bc8 Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 23:23:05 +0530 Subject: [PATCH 5/7] fix(otel): satisfy the repo lint gates for the SigNoz preset Builds the preset config with a tuple literal and a MappingProxyType instead of mutable literals, per the LIT002 guidance, which drops two of the four `mutable-ok` suppressions. The two that remain are forced by the `DYNAMIC_HEADERS_BY_CALLBACK` signature, which returns a dict. Keeps the `writable-ok` reason on the field's own line, since the formatter had wrapped the annotation and detached it, and regenerates the dashboard API types for the new `/health/services` service value. --- litellm/integrations/otel/presets/signoz.py | 42 ++++++++++--------- litellm/proxy/_types.py | 5 +-- litellm/types/utils.py | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 +- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py index 7008905481b..a2dcd3eb79e 100644 --- a/litellm/integrations/otel/presets/signoz.py +++ b/litellm/integrations/otel/presets/signoz.py @@ -1,5 +1,6 @@ """SigNoz preset — OTLP/HTTP exporter to SigNoz + GenAI vocabulary.""" +from types import MappingProxyType from typing import Final from pydantic import Field @@ -20,7 +21,7 @@ class _SigNozSettings(BaseSettings): model_config = SettingsConfigDict(case_sensitive=False, extra="ignore") # One endpoint for both SigNoz Cloud and self-hosted, with no default host, so - # nothing exports until the operator names a destination. + # nothing exports until the operator names a destination endpoint: str | None = Field(default=None, validation_alias=SIGNOZ_INGESTION_ENDPOINT_ENV) ingestion_key: str | None = Field(default=None, validation_alias="SIGNOZ_INGESTION_KEY") @@ -32,35 +33,36 @@ def signoz_preset( settings: Final = _SigNozSettings() base: Final = config_overrides or OpenTelemetryV2Config() key: Final = settings.ingestion_key + spec: Final = ExporterSpec( + kind="otlp_http", + endpoint=settings.endpoint, + headers=(f"signoz-ingestion-key={key}" if key else None), + owner=ExporterOwner.SIGNOZ, + # Cloud rejects keyless exports; a self-hosted collector accepts them + requires_headers=bool(key), + ) return base.model_copy( - update={ # mutable-ok: model_copy takes a dict of field updates - "exporters": [ # mutable-ok: matches the config's exporters list - *base.exporters, - ExporterSpec( - kind="otlp_http", - endpoint=settings.endpoint, - headers=(f"signoz-ingestion-key={key}" if key else None), - owner=ExporterOwner.SIGNOZ, - # Cloud rejects keyless exports; a self-hosted collector accepts them. - requires_headers=bool(key), - ), - ], - # SigNoz ingests the OTLP GenAI semantic conventions natively. - "mapper_names": ensure_mappers(base.mapper_names, "genai"), - } + update=MappingProxyType( + { + "exporters": (*base.exporters, spec), + "mapper_names": ensure_mappers(base.mapper_names, "genai"), + } + ) ) def signoz_dynamic_endpoint(params: StandardCallbackDynamicParams) -> str | None: """Per-request SigNoz endpoint from team/key dynamic params. - ``None`` keeps the operator's endpoint, so a team that saved only a key - still exports to the configured destination. + ``None`` keeps the operator's endpoint, so a team that saved only a key still + exports to the configured destination. """ return params.get("signoz_ingestion_endpoint") -def signoz_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]: # mutable-ok: registry type +def signoz_dynamic_headers( + params: StandardCallbackDynamicParams, +) -> dict[str, str]: # mutable-ok: DYNAMIC_HEADERS_BY_CALLBACK returns a dict """Per-request SigNoz OTLP headers from team/key dynamic params.""" key: Final = params.get("signoz_ingestion_key") - return {header: value for header, value in (("signoz-ingestion-key", key),) if value} # mutable-ok: registry type + return {"signoz-ingestion-key": key} if key else {} # mutable-ok: same registry contract diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c904cdb5ecb..57cef2b1566 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3503,10 +3503,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase): signoz: CallbackOnUI = CallbackOnUI( litellm_callback_name="signoz", ui_callback_name="SigNoz", - litellm_callback_params=[ - "SIGNOZ_INGESTION_ENDPOINT", - "SIGNOZ_INGESTION_KEY", - ], + litellm_callback_params=("SIGNOZ_INGESTION_ENDPOINT", "SIGNOZ_INGESTION_KEY"), ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index ee5d3e7c241..537da823e94 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3307,7 +3307,7 @@ class StandardCallbackDynamicParams(TypedDict, total=False): # SigNoz dynamic params (proxy-stamped team/key callback vars only; # request-supplied values are blocked) - signoz_ingestion_endpoint: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict + signoz_ingestion_endpoint: str | None # writable-ok: assigned by initialize_standard_callback_dynamic_params signoz_ingestion_key: str | None # writable-ok: initialize_standard_callback_dynamic_params assigns into the dict # Logging settings diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index d311bfa3cbc..7de3800c711 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -45388,7 +45388,7 @@ export interface operations { parameters: { query: { /** @description Specify the service being hit. */ - service: ("slack_budget_alerts" | "langfuse" | "langfuse_otel" | "slack" | "openmeter" | "webhook" | "email" | "braintrust" | "datadog" | "datadog_llm_observability" | "generic_api" | "arize" | "galileo" | "newrelic" | "sqs") | string; + service: ("slack_budget_alerts" | "langfuse" | "langfuse_otel" | "slack" | "openmeter" | "webhook" | "email" | "braintrust" | "datadog" | "datadog_llm_observability" | "generic_api" | "arize" | "galileo" | "newrelic" | "signoz" | "sqs") | string; }; header?: never; path?: never; From 2ddd7a7d67843dcf11bb53dd4bb5f9e50c29b82e Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Wed, 26 Aug 2026 18:07:19 +0530 Subject: [PATCH 6/7] refactor: resolve review comments --- litellm/integrations/otel/presets/signoz.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py index a2dcd3eb79e..adba6fdca89 100644 --- a/litellm/integrations/otel/presets/signoz.py +++ b/litellm/integrations/otel/presets/signoz.py @@ -20,8 +20,7 @@ SIGNOZ_INGESTION_ENDPOINT_ENV: Final = "SIGNOZ_INGESTION_ENDPOINT" class _SigNozSettings(BaseSettings): model_config = SettingsConfigDict(case_sensitive=False, extra="ignore") - # One endpoint for both SigNoz Cloud and self-hosted, with no default host, so - # nothing exports until the operator names a destination + # No default host: nothing exports until the operator names a destination endpoint: str | None = Field(default=None, validation_alias=SIGNOZ_INGESTION_ENDPOINT_ENV) ingestion_key: str | None = Field(default=None, validation_alias="SIGNOZ_INGESTION_KEY") @@ -52,12 +51,11 @@ def signoz_preset( def signoz_dynamic_endpoint(params: StandardCallbackDynamicParams) -> str | None: - """Per-request SigNoz endpoint from team/key dynamic params. - - ``None`` keeps the operator's endpoint, so a team that saved only a key still - exports to the configured destination. - """ - return params.get("signoz_ingestion_endpoint") + """Per-request SigNoz endpoint from team/key dynamic params; ``None`` keeps the operator's.""" + endpoint: Final = params.get("signoz_ingestion_endpoint") + if not endpoint or not endpoint.startswith(("http://", "https://")): + return None + return endpoint def signoz_dynamic_headers( From 894aaac9c746d37391ba50485744d0ef45e1f05d Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Wed, 26 Aug 2026 18:18:50 +0530 Subject: [PATCH 7/7] refactor: fix LIT010 in litelllm_logging.py --- litellm/litellm_core_utils/litellm_logging.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 44e3bc2dd38..22927480d73 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -4476,34 +4476,32 @@ def _init_custom_logger_compatible_class( SIGNOZ_INGESTION_ENDPOINT_ENV, ) - _signoz_endpoint = os.getenv(SIGNOZ_INGESTION_ENDPOINT_ENV) + _signoz_endpoint: Final = os.getenv(SIGNOZ_INGESTION_ENDPOINT_ENV) if not _signoz_endpoint: raise ValueError(f"{SIGNOZ_INGESTION_ENDPOINT_ENV} not found in environment variables") - _v2 = _maybe_construct_otel_v2("signoz", _in_memory_loggers) - if _v2 is not None: - return _v2 + _signoz_v2: Final = _maybe_construct_otel_v2("signoz", _in_memory_loggers) + if _signoz_v2 is not None: + return _signoz_v2 from litellm.integrations.opentelemetry import ( OpenTelemetry, OpenTelemetryConfig, ) - from litellm.integrations.otel.plumbing.providers import ( - _otlp_traces_endpoint, - ) - _signoz_key = os.getenv("SIGNOZ_INGESTION_KEY") - otel_config = OpenTelemetryConfig( + _signoz_base: Final = _signoz_endpoint.rstrip("/") + _signoz_key: Final = os.getenv("SIGNOZ_INGESTION_KEY") + _signoz_config: Final = OpenTelemetryConfig( exporter="otlp_http", - endpoint=_otlp_traces_endpoint(_signoz_endpoint), + endpoint=(_signoz_base if _signoz_base.endswith("/v1/traces") else f"{_signoz_base}/v1/traces"), headers=(f"signoz-ingestion-key={_signoz_key}" if _signoz_key else None), ) for callback in _in_memory_loggers: if isinstance(callback, OpenTelemetry) and callback.callback_name == "signoz": return callback - _signoz_otel_logger = OpenTelemetry(config=otel_config, callback_name="signoz") - _in_memory_loggers.append(_signoz_otel_logger) - return _signoz_otel_logger + _signoz_logger: Final = OpenTelemetry(config=_signoz_config, callback_name="signoz") + _in_memory_loggers.append(_signoz_logger) + return _signoz_logger elif logging_integration == "mlflow": for callback in _in_memory_loggers: