diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 3c5a2fc28c8..8b3f7951466 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -348,7 +348,8 @@ async def health_services_endpoint( # noqa: PLR0915 akto_logger = _akto if _akto is not None else AktoLogger() response = await akto_logger.async_health_check() except Exception as e: - return {"status": "unhealthy", "error_message": str(e)} + verbose_proxy_logger.warning("Akto health check error: %s", e) + return {"status": "unhealthy", "error_message": "Akto health check failed"} return { "status": response["status"], "message": ( diff --git a/tests/logging_callback_tests/test_akto_logger.py b/tests/logging_callback_tests/test_akto_logger.py index 90f485a99cc..15ff03fc15e 100644 --- a/tests/logging_callback_tests/test_akto_logger.py +++ b/tests/logging_callback_tests/test_akto_logger.py @@ -218,11 +218,85 @@ async def test_async_log_failure_event(logger, sample_kwargs): assert payload["statusCode"] == "500" +# ── Async failure with response_obj ── + + +@pytest.mark.asyncio +async def test_async_log_failure_with_response_obj(logger, sample_kwargs): + logger.async_http_handler.post = AsyncMock(return_value=MagicMock(status_code=200)) + mock_resp = MagicMock() + mock_resp.model_dump.return_value = {"partial": "data"} + + await logger.async_log_failure_event( + kwargs=sample_kwargs, response_obj=mock_resp, start_time=None, end_time=None + ) + + payload = json.loads(logger.async_http_handler.post.call_args.kwargs["data"]) + assert payload["statusCode"] == "500" + assert json.loads(payload["responsePayload"])["partial"] == "data" + + +@pytest.mark.asyncio +async def test_async_log_failure_with_status_code(logger, sample_kwargs): + logger.async_http_handler.post = AsyncMock(return_value=MagicMock(status_code=200)) + exc = MagicMock() + exc.status_code = 403 + sample_kwargs["exception"] = exc + + await logger.async_log_failure_event( + kwargs=sample_kwargs, response_obj=None, start_time=None, end_time=None + ) + + payload = json.loads(logger.async_http_handler.post.call_args.kwargs["data"]) + assert payload["statusCode"] == "403" + + +# ── Client IP extraction ── + + +def test_extract_client_ip_forwarded(): + assert AktoLogger.extract_client_ip( + {"proxy_server_request": {"headers": {"x-forwarded-for": "1.2.3.4, 5.6.7.8"}}} + ) == "1.2.3.4" + + +def test_extract_client_ip_real_ip(): + assert AktoLogger.extract_client_ip( + {"proxy_server_request": {"headers": {"x-real-ip": "10.0.0.1"}}} + ) == "10.0.0.1" + + +def test_extract_client_ip_fallback(): + assert AktoLogger.extract_client_ip({}) == "0.0.0.0" + + +# ── Request path extraction ── + + +def test_extract_request_path_from_metadata(): + assert AktoLogger.extract_request_path( + {"metadata": {"user_api_key_request_route": "/v1/embeddings"}} + ) == "/v1/embeddings" + + +def test_extract_request_path_fallback(): + assert AktoLogger.extract_request_path({}) == "/v1/chat/completions" + + +# ── Default host fallback ── + + +def test_build_request_headers_default_host(): + headers = AktoLogger.build_request_headers({}) + assert headers["host"] == "litellm.ai" + assert headers["content-type"] == "application/json" + + # ── Error handling ── @pytest.mark.asyncio -async def test_async_log_swallows_errors(logger): +async def test_async_log_success_swallows_errors(logger): logger.async_http_handler.post = AsyncMock( side_effect=httpx.ConnectError("refused") ) @@ -233,3 +307,17 @@ async def test_async_log_swallows_errors(logger): start_time=None, end_time=None, ) + + +@pytest.mark.asyncio +async def test_async_log_failure_swallows_errors(logger): + logger.async_http_handler.post = AsyncMock( + side_effect=httpx.ConnectError("refused") + ) + # Should not raise + await logger.async_log_failure_event( + kwargs={"messages": [], "model": "m", "litellm_params": {}}, + response_obj=None, + start_time=None, + end_time=None, + )