mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
Merge 894aaac9c7 into 1df25e26cf
This commit is contained in:
commit
d01174940b
20 changed files with 381 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class ExporterOwner(str, Enum):
|
|||
LEVO = "levo"
|
||||
AGENTOPS = "agentops"
|
||||
NEWRELIC = "newrelic"
|
||||
SIGNOZ = "signoz"
|
||||
|
||||
|
||||
class _OTelV2Flag(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
66
litellm/integrations/otel/presets/signoz.py
Normal file
66
litellm/integrations/otel/presets/signoz.py
Normal file
|
|
@ -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
|
||||
|
|
@ -87,6 +87,7 @@ class CustomLoggerRegistry:
|
|||
"langtrace": OpenTelemetry,
|
||||
"weave_otel": OpenTelemetry,
|
||||
"levo": OpenTelemetry,
|
||||
"signoz": OpenTelemetry,
|
||||
"mlflow": MlflowLogger,
|
||||
"langfuse": LangfusePromptManagement,
|
||||
"otel": OpenTelemetry,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
1
litellm/proxy/_experimental/out/assets/logos/signoz.svg
Normal file
1
litellm/proxy/_experimental/out/assets/logos/signoz.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 22"><rect width="21" height="21" y=".5" fill="url(#a)" rx="4.2"/><g fill="#fff" fill-rule="evenodd" clip-rule="evenodd" filter="url(#b)"><path d="M2.31 10.988v-.005l.002-.008.022-.088q.022-.082.073-.225c.069-.188.182-.448.364-.76.363-.623.998-1.445 2.089-2.3a9.3 9.3 0 0 1 2.957-1.569A7 7 0 0 1 8.78 5.8a5 5 0 0 1 .377-.048h.009l.001-.001.028.466.028.465-.065.006q-.076.008-.225.033a6 6 0 0 0-.72.169c-.576.197-1.4.583-2.362 1.336-.96.751-1.5 1.46-1.798 1.972a4 4 0 0 0-.29.607A2 2 0 0 0 3.7 11l.01.036q.013.054.051.16c.052.142.141.35.29.607.3.512.839 1.221 1.799 1.972.962.753 1.786 1.139 2.362 1.336l.027.01c.286.082.523.13.693.16a4 4 0 0 0 .29.038l-.028.465-.028.466h-.01l-.1-.01a5 5 0 0 1-.277-.04 7 7 0 0 1-.963-.233 9.3 9.3 0 0 1-2.957-1.57c-1.091-.854-1.726-1.676-2.09-2.3a5 5 0 0 1-.363-.76 3 3 0 0 1-.095-.312l-.001-.008-.001-.003v-.026m16.381 0v-.002l-.001-.003-.002-.008a2 2 0 0 0-.095-.313 5 5 0 0 0-.363-.76c-.364-.623-.998-1.445-2.09-2.3a9.3 9.3 0 0 0-2.957-1.569 7 7 0 0 0-.963-.234 5 5 0 0 0-.376-.048h-.01l-.001-.001-.028.466-.028.465.011.001.054.005q.076.008.225.033a6 6 0 0 1 .72.169c.577.197 1.4.583 2.362 1.336.96.751 1.5 1.46 1.799 1.972.149.257.238.466.29.607a2 2 0 0 1 .061.196l-.01.036q-.013.054-.051.16c-.052.142-.141.35-.29.607-.299.512-.839 1.221-1.799 1.972-.962.753-1.785 1.139-2.362 1.336l-.027.01a6 6 0 0 1-.693.16 4 4 0 0 1-.29.038l.028.465.028.466h.011l.1-.01q.1-.01.276-.04c.235-.04.566-.11.963-.233a9.3 9.3 0 0 0 2.957-1.57c1.092-.854 1.726-1.676 2.09-2.3.181-.31.294-.571.363-.76a3 3 0 0 0 .095-.312l.002-.008v-.029"/><path d="M14.351 11c0 2.067-1.664 3.743-3.717 3.743S6.918 13.067 6.918 11s1.664-3.744 3.716-3.744S14.352 8.932 14.352 11M9.705 8.894s-.446.526-.58.936c-.087.264-.117.702-.117.702H7.847s0-.351.232-.936.465-.702.465-.702zm-.58 3.275c.134.41.58.936.58.936H8.544s-.233-.117-.465-.702-.232-.936-.232-.936h1.161s.03.439.116.702"/></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(45.69 -7.805 17.962)scale(12.3258)" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#FF5E19"/><stop offset="1" stop-color="#FF2929"/></radialGradient><filter id="b" width="18.9" height="13.02" x="1.05" y="5.33" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy=".84"/><feGaussianBlur stdDeviation=".63"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.368384 0 0 0 0 0.0623777 0 0 0 0 0.0623777 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2420_727"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_2420_727" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy=".84"/><feGaussianBlur stdDeviation=".63"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/><feBlend in2="shape" result="effect2_innerShadow_2420_727"/></filter></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
1
ui/litellm-dashboard/public/assets/logos/signoz.svg
Normal file
1
ui/litellm-dashboard/public/assets/logos/signoz.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 22"><rect width="21" height="21" y=".5" fill="url(#a)" rx="4.2"/><g fill="#fff" fill-rule="evenodd" clip-rule="evenodd" filter="url(#b)"><path d="M2.31 10.988v-.005l.002-.008.022-.088q.022-.082.073-.225c.069-.188.182-.448.364-.76.363-.623.998-1.445 2.089-2.3a9.3 9.3 0 0 1 2.957-1.569A7 7 0 0 1 8.78 5.8a5 5 0 0 1 .377-.048h.009l.001-.001.028.466.028.465-.065.006q-.076.008-.225.033a6 6 0 0 0-.72.169c-.576.197-1.4.583-2.362 1.336-.96.751-1.5 1.46-1.798 1.972a4 4 0 0 0-.29.607A2 2 0 0 0 3.7 11l.01.036q.013.054.051.16c.052.142.141.35.29.607.3.512.839 1.221 1.799 1.972.962.753 1.786 1.139 2.362 1.336l.027.01c.286.082.523.13.693.16a4 4 0 0 0 .29.038l-.028.465-.028.466h-.01l-.1-.01a5 5 0 0 1-.277-.04 7 7 0 0 1-.963-.233 9.3 9.3 0 0 1-2.957-1.57c-1.091-.854-1.726-1.676-2.09-2.3a5 5 0 0 1-.363-.76 3 3 0 0 1-.095-.312l-.001-.008-.001-.003v-.026m16.381 0v-.002l-.001-.003-.002-.008a2 2 0 0 0-.095-.313 5 5 0 0 0-.363-.76c-.364-.623-.998-1.445-2.09-2.3a9.3 9.3 0 0 0-2.957-1.569 7 7 0 0 0-.963-.234 5 5 0 0 0-.376-.048h-.01l-.001-.001-.028.466-.028.465.011.001.054.005q.076.008.225.033a6 6 0 0 1 .72.169c.577.197 1.4.583 2.362 1.336.96.751 1.5 1.46 1.799 1.972.149.257.238.466.29.607a2 2 0 0 1 .061.196l-.01.036q-.013.054-.051.16c-.052.142-.141.35-.29.607-.299.512-.839 1.221-1.799 1.972-.962.753-1.785 1.139-2.362 1.336l-.027.01a6 6 0 0 1-.693.16 4 4 0 0 1-.29.038l.028.465.028.466h.011l.1-.01q.1-.01.276-.04c.235-.04.566-.11.963-.233a9.3 9.3 0 0 0 2.957-1.57c1.092-.854 1.726-1.676 2.09-2.3.181-.31.294-.571.363-.76a3 3 0 0 0 .095-.312l.002-.008v-.029"/><path d="M14.351 11c0 2.067-1.664 3.743-3.717 3.743S6.918 13.067 6.918 11s1.664-3.744 3.716-3.744S14.352 8.932 14.352 11M9.705 8.894s-.446.526-.58.936c-.087.264-.117.702-.117.702H7.847s0-.351.232-.936.465-.702.465-.702zm-.58 3.275c.134.41.58.936.58.936H8.544s-.233-.117-.465-.702-.232-.936-.232-.936h1.161s.03.439.116.702"/></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientTransform="rotate(45.69 -7.805 17.962)scale(12.3258)" gradientUnits="userSpaceOnUse"><stop offset=".33" stop-color="#FF5E19"/><stop offset="1" stop-color="#FF2929"/></radialGradient><filter id="b" width="18.9" height="13.02" x="1.05" y="5.33" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy=".84"/><feGaussianBlur stdDeviation=".63"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0.368384 0 0 0 0 0.0623777 0 0 0 0 0.0623777 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2420_727"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_2420_727" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy=".84"/><feGaussianBlur stdDeviation=".63"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/><feBlend in2="shape" result="effect2_innerShadow_2420_727"/></filter></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -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",
|
||||
|
|
|
|||
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
2
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue