diff --git a/litellm/__init__.py b/litellm/__init__.py
index eebd2dad91e..26d8358c253 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 7a2295a35ae..82530d1d37a 100644
--- a/litellm/integrations/callback_configs.json
+++ b/litellm/integrations/callback_configs.json
@@ -416,6 +416,27 @@
},
"description": "S3 Bucket (AWS) Logging Integration"
},
+ {
+ "id": "signoz",
+ "displayName": "SigNoz",
+ "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 (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 Logging Integration. Setup: https://signoz.io/docs/litellm-observability/"
+ },
{
"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..7c891c29409 100644
--- a/litellm/integrations/otel/presets/__init__.py
+++ b/litellm/integrations/otel/presets/__init__.py
@@ -30,6 +30,11 @@ from litellm.integrations.otel.presets.phoenix import (
phoenix_preset,
phoenix_project_headers,
)
+from litellm.integrations.otel.presets.signoz import (
+ signoz_dynamic_endpoint,
+ 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 +49,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 +64,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,
}
)
@@ -71,6 +78,7 @@ DYNAMIC_ENDPOINT_BY_CALLBACK: Final[Mapping[str, Callable[[StandardCallbackDynam
MappingProxyType(
{
"newrelic": newrelic_dynamic_endpoint,
+ "signoz": signoz_dynamic_endpoint,
}
)
)
@@ -153,5 +161,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..adba6fdca89
--- /dev/null
+++ b/litellm/integrations/otel/presets/signoz.py
@@ -0,0 +1,66 @@
+"""SigNoz preset — OTLP/HTTP exporter to SigNoz + GenAI vocabulary."""
+
+from types import MappingProxyType
+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")
+
+ # 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")
+
+
+def signoz_preset(
+ *,
+ config_overrides: OpenTelemetryV2Config | None = None,
+) -> OpenTelemetryV2Config:
+ 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=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: Final = params.get("signoz_ingestion_endpoint")
+ if not endpoint or not endpoint.startswith(("http://", "https://")):
+ return None
+ return endpoint
+
+
+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 {"signoz-ingestion-key": key} if key else {} # mutable-ok: same registry contract
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 65c5b0d9799..d20a214904f 100644
--- a/litellm/litellm_core_utils/initialize_dynamic_callback_params.py
+++ b/litellm/litellm_core_utils/initialize_dynamic_callback_params.py
@@ -92,6 +92,8 @@ _supported_callback_params: Final[tuple[str, ...]] = (
"dd_agent_port",
"newrelic_api_key",
"newrelic_region",
+ "signoz_ingestion_endpoint",
+ "signoz_ingestion_key",
"turn_off_message_logging",
)
@@ -105,6 +107,8 @@ _request_blocked_callback_params: Final = frozenset(
"dd_agent_port",
"newrelic_api_key",
"newrelic_region",
+ "signoz_ingestion_endpoint",
+ "signoz_ingestion_key",
}
)
@@ -117,6 +121,8 @@ _trusted_overlay_callback_params: Final = frozenset(
{
"newrelic_api_key",
"newrelic_region",
+ "signoz_ingestion_endpoint",
+ "signoz_ingestion_key",
}
)
diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py
index fd2200c59cc..409b6a8046f 100644
--- a/litellm/litellm_core_utils/litellm_logging.py
+++ b/litellm/litellm_core_utils/litellm_logging.py
@@ -4471,6 +4471,38 @@ 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: Final = os.getenv(SIGNOZ_INGESTION_ENDPOINT_ENV)
+ if not _signoz_endpoint:
+ raise ValueError(f"{SIGNOZ_INGESTION_ENDPOINT_ENV} not found in environment variables")
+
+ _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,
+ )
+
+ _signoz_base: Final = _signoz_endpoint.rstrip("/")
+ _signoz_key: Final = os.getenv("SIGNOZ_INGESTION_KEY")
+ _signoz_config: Final = OpenTelemetryConfig(
+ exporter="otlp_http",
+ 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_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:
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/proxy/_types.py b/litellm/proxy/_types.py
index bb26350e1b1..fd9ccdb59fb 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -3525,6 +3525,12 @@ 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 72688ade228..33314dcdf10 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 064b53e07b7..4e5dff5abf1 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 a7629fb2488..17614db3fe0 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -3328,6 +3328,11 @@ 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_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
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..d17c38f3f3f 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,25 @@ 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_dynamic_endpoint_comes_from_team_config():
+ from litellm.integrations.otel.presets import dynamic_otlp_endpoint
+
+ 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/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/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py
index 29b283ec009..67c61fd91d3 100644
--- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py
+++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py
@@ -5782,3 +5782,108 @@ def test_failure_handler_helper_fn_builds_payload_once_per_exception():
other_exc = _raise_and_catch(_ClientError(status_code=429, message="rate limited"))
obj._failure_handler_helper_fn(exception=other_exc, traceback_exception="")
assert obj.model_call_details["standard_logging_object"] is not first_payload
+
+
+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
+ # 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()
+
+
+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()
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/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
diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx
index 4b6f6233fe8..14fd3e8e345 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;
@@ -174,6 +175,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",
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 4ccf7b59fbc..d2b3591e99d 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -45490,7 +45490,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;