From e1fbdde289a5f0ce281776045fb69c2e9ba2bb7d Mon Sep 17 00:00:00 2001 From: Cole McIntosh <82463175+colesmcintosh@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:47:55 -0600 Subject: [PATCH] Fix boto3 tracer wrapping for observability (#11869) * feat: add summarize parameter to /spend/logs endpoint for individual transaction logs - Introduced a new `summarize` parameter to control data format when querying spend logs. - `summarize=true` (default) returns aggregated data, while `summarize=false` provides individual transaction logs. - Updated documentation and added tests to validate the new functionality. * fix: wrap boto3.Session() with tracer for observability - Add tracer.trace wrapper around boto3.Session() call in _get_aws_region_name method - Ensures all boto3 initializations in base_aws_llm.py are properly instrumented - Fixes test_boto3_init_tracer_wrapping test failure - Maintains consistency with other boto3 calls in the same file --- docs/my-website/docs/proxy/cost_tracking.md | 29 ++++ litellm/llms/bedrock/base_aws_llm.py | 3 +- .../spend_management_endpoints.py | 26 ++++ .../test_spend_management_endpoints.py | 143 ++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) diff --git a/docs/my-website/docs/proxy/cost_tracking.md b/docs/my-website/docs/proxy/cost_tracking.md index 5b17e565a5..58a6b1f27e 100644 --- a/docs/my-website/docs/proxy/cost_tracking.md +++ b/docs/my-website/docs/proxy/cost_tracking.md @@ -577,6 +577,35 @@ curl -X GET 'http://localhost:4000/global/spend/report?start_date=2024-04-01&end +## 📊 Spend Logs API - Individual Transaction Logs + +The `/spend/logs` endpoint now supports a `summarize` parameter to control data format when using date filters. + +### Key Parameters + +| Parameter | Description | +|-----------|-------------| +| `summarize` | **New parameter**: `true` (default) = aggregated data, `false` = individual transaction logs | + +### Examples + +**Get individual transaction logs:** +```bash +curl -X GET "http://localhost:4000/spend/logs?start_date=2024-01-01&end_date=2024-01-02&summarize=false" \ +-H "Authorization: Bearer sk-1234" +``` + +**Get summarized data (default):** +```bash +curl -X GET "http://localhost:4000/spend/logs?start_date=2024-01-01&end_date=2024-01-02" \ +-H "Authorization: Bearer sk-1234" +``` + +**Use Cases:** +- `summarize=false`: Analytics dashboards, ETL processes, detailed audit trails +- `summarize=true`: Daily spending reports, high-level cost tracking (legacy behavior) + + ## ✨ Custom Spend Log metadata Log specific key,value pairs as part of the metadata for a spend log diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 1275ea1595..20587080e4 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -334,7 +334,8 @@ class BaseAWSLLM: try: import boto3 - session = boto3.Session() + with tracer.trace("boto3.Session()"): + session = boto3.Session() configured_region = session.region_name if configured_region: aws_region_name = configured_region diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 49e2ae300e..952d680cfc 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1854,11 +1854,19 @@ async def view_spend_logs( # noqa: PLR0915 default=None, description="Time till which to view key spend", ), + summarize: bool = fastapi.Query( + default=True, + description="When start_date and end_date are provided, summarize=true returns aggregated data by date (legacy behavior), summarize=false returns filtered individual logs", + ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ View all spend logs, if request_id is provided, only logs for that request_id will be returned + When start_date and end_date are provided: + - summarize=true (default): Returns aggregated spend data grouped by date (maintains backward compatibility) + - summarize=false: Returns filtered individual log entries within the date range + Example Request for all logs ``` curl -X GET "http://0.0.0.0:8000/spend/logs" \ @@ -1882,6 +1890,12 @@ async def view_spend_logs( # noqa: PLR0915 curl -X GET "http://0.0.0.0:8000/spend/logs?user_id=ishaan@berri.ai" \ -H "Authorization: Bearer sk-1234" ``` + + Example Request for date range with individual logs (unsummarized) + ``` + curl -X GET "http://0.0.0.0:8000/spend/logs?start_date=2024-01-01&end_date=2024-01-02&summarize=false" \ +-H "Authorization: Bearer sk-1234" + ``` """ from litellm.proxy.proxy_server import prisma_client @@ -1922,6 +1936,18 @@ async def view_spend_logs( # noqa: PLR0915 elif user_id is not None and isinstance(user_id, str): filter_query["user"] = user_id # type: ignore + # Check if user wants unsummarized data + if not summarize: + # Return filtered individual log entries (similar to UI endpoint) + data = await prisma_client.db.litellm_spendlogs.find_many( + where=filter_query, # type: ignore + order={ + "startTime": "desc", + }, + ) + return data + + # Legacy behavior: return summarized data (when summarize=true) # SQL query response = await prisma_client.db.litellm_spendlogs.group_by( by=["api_key", "user", "model", "startTime"], diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index e86a6d000e..2af9588337 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -1079,3 +1079,146 @@ async def test_global_spend_keys_endpoint_limit_validation(client, monkeypatch): assert response.status_code == 422 mock_query_raw.assert_not_called() mock_query_raw.reset_mock() + + +@pytest.mark.asyncio +async def test_view_spend_logs_summarize_parameter(client, monkeypatch): + """Test the new summarize parameter in the /spend/logs endpoint""" + import datetime + from datetime import timezone, timedelta + + # Mock spend logs data + mock_spend_logs = [ + { + "id": "log1", + "request_id": "req1", + "api_key": "sk-test-key", + "user": "test_user_1", + "team_id": "team1", + "spend": 0.05, + "startTime": (datetime.datetime.now(timezone.utc) - timedelta(days=1)).isoformat(), + "model": "gpt-3.5-turbo", + "prompt_tokens": 100, + "completion_tokens": 50, + "total_tokens": 150, + }, + { + "id": "log2", + "request_id": "req2", + "api_key": "sk-test-key", + "user": "test_user_1", + "team_id": "team1", + "spend": 0.10, + "startTime": (datetime.datetime.now(timezone.utc) - timedelta(days=1)).isoformat(), + "model": "gpt-4", + "prompt_tokens": 200, + "completion_tokens": 100, + "total_tokens": 300, + }, + ] + + # Mock for unsummarized data (summarize=false) + class MockDB: + def __init__(self): + self.litellm_spendlogs = self + + async def find_many(self, *args, **kwargs): + # Return individual log entries when summarize=false + return mock_spend_logs + + async def group_by(self, *args, **kwargs): + # Return grouped data when summarize=true + # Simplified mock response for grouped data + yesterday = datetime.datetime.now(timezone.utc) - timedelta(days=1) + return [ + { + "api_key": "sk-test-key", + "user": "test_user_1", + "model": "gpt-3.5-turbo", + "startTime": yesterday.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "_sum": {"spend": 0.05} + }, + { + "api_key": "sk-test-key", + "user": "test_user_1", + "model": "gpt-4", + "startTime": yesterday.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "_sum": {"spend": 0.10} + } + ] + + class MockPrismaClient: + def __init__(self): + self.db = MockDB() + + # Apply the monkeypatch + mock_prisma_client = MockPrismaClient() + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Set up test dates + start_date = (datetime.datetime.now(timezone.utc) - timedelta(days=2)).strftime("%Y-%m-%d") + end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d") + + # Test 1: summarize=false should return individual log entries + response = client.get( + "/spend/logs", + params={ + "start_date": start_date, + "end_date": end_date, + "summarize": "false", + }, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + data = response.json() + + # Should return the raw log entries + assert isinstance(data, list) + assert len(data) == 2 + assert data[0]["id"] == "log1" + assert data[1]["id"] == "log2" + assert data[0]["request_id"] == "req1" + assert data[1]["request_id"] == "req2" + + # Test 2: summarize=true should return grouped data + response = client.get( + "/spend/logs", + params={ + "start_date": start_date, + "end_date": end_date, + "summarize": "true", + }, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + data = response.json() + + # Should return grouped/summarized data + assert isinstance(data, list) + # The structure should be different - grouped by date with aggregated spend + assert "startTime" in data[0] + assert "spend" in data[0] + assert "users" in data[0] + assert "models" in data[0] + + # Test 3: default behavior (no summarize parameter) should maintain backward compatibility + response = client.get( + "/spend/logs", + params={ + "start_date": start_date, + "end_date": end_date, + }, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + data = response.json() + + # Should return grouped/summarized data (same as summarize=true) + assert isinstance(data, list) + assert "startTime" in data[0] + assert "spend" in data[0] + assert "users" in data[0] + assert "models" in data[0]