From ee6a6d8f6819533ac6f70b17a7632d18c91ce3e8 Mon Sep 17 00:00:00 2001 From: Nagesh Bansal Date: Tue, 25 Aug 2026 22:07:55 +0530 Subject: [PATCH] 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",