mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 10:21:52 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user