diff --git a/docs/my-website/docs/observability/logfire_integration.md b/docs/my-website/docs/observability/logfire_integration.md index b75c5bfd496..a1bd43a4bc4 100644 --- a/docs/my-website/docs/observability/logfire_integration.md +++ b/docs/my-website/docs/observability/logfire_integration.md @@ -40,6 +40,10 @@ import os # from https://logfire.pydantic.dev/ os.environ["LOGFIRE_TOKEN"] = "" +# Optionally customize the base url +# from https://logfire.pydantic.dev/ +os.environ["LOGFIRE_BASE_URL"] = "" + # LLM API Keys os.environ['OPENAI_API_KEY']="" diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 6c5c45dc90c..ab405fd204b 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -744,6 +744,7 @@ router_settings: | LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD | If true, prints the standard logging payload to the console - useful for debugging | LITELM_ENVIRONMENT | Environment for LiteLLM Instance. This is currently only logged to DeepEval to determine the environment for DeepEval integration. | LOGFIRE_TOKEN | Token for Logfire logging service +| LOGFIRE_BASE_URL | Base URL for Logfire logging service (useful for self hosted deployments) | LOGGING_WORKER_CONCURRENCY | Maximum number of concurrent coroutine slots for the logging worker on the asyncio event loop. Default is 100. Setting too high will flood the event loop with logging tasks which will lower the overall latency of the requests. | LOGGING_WORKER_MAX_QUEUE_SIZE | Maximum size of the logging worker queue. When the queue is full, the worker aggressively clears tasks to make room instead of dropping logs. Default is 50,000 | LOGGING_WORKER_MAX_TIME_PER_COROUTINE | Maximum time in seconds allowed for each coroutine in the logging worker before timing out. Default is 20.0 diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 619c5d1cf00..0580da8e1b8 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3743,10 +3743,10 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915 OpenTelemetry, OpenTelemetryConfig, ) - + logfire_base_url = os.getenv("LOGFIRE_BASE_URL", "https://logfire-api.pydantic.dev") otel_config = OpenTelemetryConfig( exporter="otlp_http", - endpoint="https://logfire-api.pydantic.dev/v1/traces", + endpoint = f"{logfire_base_url.rstrip('/')}/v1/traces", headers=f"Authorization={os.getenv('LOGFIRE_TOKEN')}", ) for callback in _in_memory_loggers: 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 a4d3206fdc7..e035e193fe1 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -196,6 +196,53 @@ async def test_datadog_logger_not_shadowed_by_llm_obs(monkeypatch): logging_module._in_memory_loggers.clear() +@pytest.mark.asyncio +async def test_logfire_logger_accepts_env_vars_for_base_url(monkeypatch): + """Ensure Logfire logger uses LOGFIRE_BASE_URL to build the OTLP HTTP endpoint (/v1/traces).""" + + # Required env vars for Logfire integration + monkeypatch.setenv("LOGFIRE_TOKEN", "test-token") + monkeypatch.setenv("LOGFIRE_BASE_URL", "https://logfire-api-custom.pydantic.dev") # no trailing slash on purpose + + # Import after env vars are set (important if module-level caching exists) + from litellm.litellm_core_utils import litellm_logging as logging_module + from litellm.integrations.opentelemetry import OpenTelemetry # logger class + + logging_module._in_memory_loggers.clear() + + try: + # Instantiate via the same mechanism LiteLLM uses for callbacks=["logfire"] + logger = logging_module._init_custom_logger_compatible_class( + logging_integration="logfire", + internal_usage_cache=None, + llm_router=None, + custom_logger_init_args={}, + ) + + # Sanity: we got the right logger type and it is cached + assert type(logger) is OpenTelemetry + assert any(type(cb) is OpenTelemetry for cb in logging_module._in_memory_loggers) + + # Core regression check: base URL env var should influence the exporter endpoint. + # + # OpenTelemetry integration has historically stored config on the instance. + # We defensively check a few common attribute names to avoid brittle coupling. + cfg = ( + getattr(logger, "otel_config", None) + or getattr(logger, "config", None) + or getattr(logger, "_otel_config", None) + ) + assert cfg is not None, "Expected OpenTelemetry logger to keep an otel config on the instance" + + endpoint = getattr(cfg, "endpoint", None) or getattr(cfg, "otlp_endpoint", None) + assert endpoint is not None, "Expected otel config to expose the OTLP endpoint" + + assert endpoint == "https://logfire-api-custom.pydantic.dev/v1/traces" + + finally: + logging_module._in_memory_loggers.clear() + + @pytest.mark.asyncio async def test_logging_result_for_bridge_calls(logging_obj): """