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
This commit is contained in:
Cole McIntosh
2025-06-18 15:47:55 -07:00
committed by GitHub
parent 0e1ff45065
commit e1fbdde289
4 changed files with 200 additions and 1 deletions
@@ -577,6 +577,35 @@ curl -X GET 'http://localhost:4000/global/spend/report?start_date=2024-04-01&end
</Tabs>
## 📊 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
+2 -1
View File
@@ -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
@@ -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"],
@@ -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]