Tutorial: Claude + LiteLLM on Azure — Logging, Log Extraction, and Evaluation Data
Audience: QA / AI-evaluation engineers who need to understand a deployment where a Claude model is served through a LiteLLM proxy hosted in Azure, and who want to extract log data from Azure for analysis and model evaluation.
Companion page: Claude Telemetry Reference — request/response parameters element by element
Last updated: 2026-08-05
Table of Contents
- The Big Picture — Architecture
- What is Claude, and What is "Claude 3P" (Third-Party)?
- What is LiteLLM and How It Works
- What LiteLLM Itself Can Log
- Azure Logging — The Landscape
- What Gets Logged Where (End-to-End Trace)
- How to Retrieve Log Data from Azure
- KQL Query Cookbook for LLM Logs
- What Else You Can (and Should) Log
- From Logs to Evaluations
- Privacy, Cost, and Retention Considerations
- Quick Reference Card
1. The Big Picture — Architecture
A typical "Claude behind LiteLLM on Azure" deployment looks like this:
┌─────────────┐ ┌──────────────────────────────────┐ ┌───────────────────┐
│ Client App │────▶│ LiteLLM Proxy (in Azure) │────▶│ Claude Provider │
│ (your code, │ │ - App Service / AKS / │ │ - Anthropic API │
│ tests, │ │ Container Apps │ │ - AWS Bedrock │
│ eval jobs) │ │ - OpenAI-compatible endpoint │ │ - MS Foundry │
└─────────────┘ └──────────────┬───────────────────┘ └───────────────────┘
│
┌──────────────┴───────────────────┐
│ Logging / Telemetry │
│ - LiteLLM spend logs (DB) │
│ - stdout/stderr container logs │
│ - Azure Monitor / App Insights │
│ - Log Analytics workspace (KQL) │
└───────────────────────────────────┘
Three layers, each with its own logs:
| Layer | What it is | Where its logs live |
|---|---|---|
| Application | Your client code calling the proxy | Your own app logging (and App Insights if instrumented) |
| LiteLLM proxy | Gateway that translates OpenAI-format requests into Claude API calls | LiteLLM DB (spend logs), stdout → Azure container/App Service logs, callbacks (Langfuse, OTel, etc.) |
| Azure platform | The compute + networking hosting the proxy | Azure Monitor → Log Analytics workspace, Diagnostic Settings, Metrics |
The key insight for evaluation work: the LiteLLM layer is where the richest LLM-specific data lives (prompts, completions, tokens, latency, cost per request). Azure platform logs add infrastructure context (HTTP status codes, container restarts, scaling events).
2. What is Claude, and What is "Claude 3P" (Third-Party)?
Claude is Anthropic's family of large language models, accessed via the Messages API (POST /v1/messages). You send a list of messages plus parameters (model, max_tokens, etc.) and receive a response with content blocks and a usage object (input/output token counts).
"Claude 3P" = Claude via a Third-Party platform
Claude 3P ("third party") means consuming Claude not through Anthropic's own API (1P — first party), but through a cloud provider's platform that hosts/resells Claude. This distinction matters enormously for logging and debugging, because auth, model IDs, error formats, quotas, and feature availability all change with the platform:
| Aspect | 1P — Anthropic API | 3P — Amazon Bedrock | 3P — Google Vertex AI | 3P — Microsoft Foundry |
|---|---|---|---|---|
| Endpoint | api.anthropic.com/v1/messages |
bedrock-runtime.<region>.amazonaws.com |
<region>-aiplatform.googleapis.com |
<resource>.services.ai.azure.com/anthropic/v1 |
| Auth | x-api-key (Anthropic key) |
AWS SigV4 (IAM) | GCP ADC / OAuth | Azure API key / Entra ID |
| Model ID style | claude-sonnet-5 |
anthropic.claude-sonnet-5 (or inference-profile ARN us.anthropic....) |
bare ID, or dated snapshots with @ (claude-opus-4-5@20251101) |
bare first-party ID |
| Version pin | anthropic-version header |
anthropic_version: "bedrock-2023-05-31" in the body |
in body | header |
| Rate limits / quotas | Anthropic account tier | AWS account + region quotas | GCP quotas | Azure quotas |
| Billing | Anthropic | AWS (partner pricing) | GCP (partner pricing) | Microsoft Marketplace (standard API rates) |
| Error format | Anthropic {type:"error", error:{...}} |
AWS exceptions (ThrottlingException, ValidationException, …) |
Google RPC errors | Anthropic-style |
| Feature availability | Everything first | Subset, release lag varies | Subset (e.g. basic web search only) | Near-parity, many features beta |
| Operated by | Anthropic | AWS | Anthropic models, MS billing |
Your scenario is the Bedrock column: LiteLLM (in Azure) → Claude 3P via Amazon Bedrock. So when you investigate failures, remember that many errors are AWS-layer errors (IAM, region quotas, model-access grants) that never touched the Claude model at all — the telemetry reference breaks these out layer by layer.
Why teams choose 3P over 1P
- Existing cloud contract/credits and procurement (billing goes through AWS/GCP/Azure)
- Data-residency and compliance boundaries (traffic stays inside the cloud provider)
- IAM-based auth instead of managing Anthropic API keys
- Co-location with the rest of the stack (VPC networking, private endpoints)
The trade-off: feature lag and platform-specific quirks — some Anthropic features arrive later (or never) on 3P platforms, model IDs differ, and each platform imposes its own quota model. Logging must record which platform and region served each request.
One retirement caveat still worth knowing
Model generations also age out: older Claude model snapshots get deprecated per-platform on their own schedules (Anthropic's for 1P, AWS's for Bedrock). A deployment pinned to an old snapshot will one day start returning ValidationException/404 for every request — a classic "everyone broke overnight" root cause, so log the exact model ID served (see section 9).
How a Claude API call works (the thing LiteLLM wraps)
POST https://api.anthropic.com/v1/messages
Headers: x-api-key, anthropic-version: 2023-06-01
Body: {
"model": "claude-sonnet-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "..."}]
}
Response: {
"id": "msg_...",
"content": [{"type": "text", "text": "..."}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 12, "output_tokens": 84, ...}
}
Every response carries a usage block — this is the ground truth for token accounting, and LiteLLM propagates it into its logs. The stop_reason field (end_turn, max_tokens, tool_use, refusal, …) is also worth logging: it tells you why generation stopped, which matters for failure analysis in evals.
Claude can be reached via several providers, and LiteLLM abstracts all of them:
- Anthropic API (first-party,
api.anthropic.com) - AWS Bedrock (model IDs get an
anthropic.prefix) - Google Vertex AI
- Microsoft Foundry (the Azure-native way to consume Claude — relevant if your whole stack is Azure)
3. What is LiteLLM and How It Works
LiteLLM is an open-source LLM gateway (proxy server) that exposes a single OpenAI-compatible API (/chat/completions, /completions, /embeddings) and translates requests to 100+ backend providers — including Anthropic.
Why teams deploy it
| Capability | What it means |
|---|---|
| Unified API | Clients call OpenAI-format endpoints; LiteLLM converts to the Anthropic Messages API format behind the scenes. Swap models without changing client code. |
| Virtual keys | LiteLLM issues its own API keys to teams/users; the real Anthropic key stays on the server. |
| Budgets & rate limits | Per-key, per-team, per-model spend caps and TPM/RPM limits. |
| Routing/fallbacks | Load-balance across deployments; automatic fallback to another model on errors. |
| Observability | Built-in cost tracking, spend logs, and callback integrations — this is the part you care about. |
The request lifecycle through LiteLLM
- Client sends an OpenAI-format request to the proxy (
/v1/chat/completions) with a virtual key. - LiteLLM authenticates the key, checks budgets/rate limits.
- LiteLLM maps the requested model alias (e.g.,
claude-sonnet) to the configured backend model + real credentials (from itsconfig.yaml). - Request is translated to Anthropic's Messages API format and forwarded.
- Response comes back; LiteLLM computes cost (tokens × per-model pricing), records a spend log entry, fires any configured callbacks, and returns the OpenAI-format response to the client.
Typical config (config.yaml)
model_list:
- model_name: claude-sonnet # alias your clients use
litellm_params:
model: anthropic/claude-sonnet-5
api_key: os.environ/ANTHROPIC_API_KEY
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL # Postgres → enables spend logs
litellm_settings:
success_callback: ["langfuse"] # or otel, s3, azure_storage, ...
failure_callback: ["langfuse"]
How it's typically hosted in Azure
| Azure service | Notes for logging |
|---|---|
| Azure Container Apps | stdout/stderr → ContainerAppConsoleLogs_CL table in Log Analytics |
| AKS (Kubernetes) | Container Insights → ContainerLogV2 table |
| App Service (container) | App Service logs → AppServiceConsoleLogs / AppServiceHTTPLogs |
| Azure VM + Docker | Requires Azure Monitor Agent to ship logs |
Plus, usually: Azure Database for PostgreSQL (LiteLLM's spend-log store) and Azure Key Vault (secrets).
4. What LiteLLM Itself Can Log
This is the richest source of evaluation data. LiteLLM has four distinct logging surfaces:
4.1 Spend logs (Postgres database)
When database_url is configured, every request writes a row to the LiteLLM_SpendLogs table:
| Column (representative) | What you get |
|---|---|
request_id |
Unique ID per request — join key across systems |
call_type |
completion, acompletion, embedding, … |
api_key (hashed) |
Which virtual key made the call |
spend |
Computed cost in USD |
total_tokens, prompt_tokens, completion_tokens |
Token accounting |
model, model_group, api_base |
Which model/deployment served it |
startTime, endTime |
Latency computable as endTime - startTime |
user, team_id, end_user |
Attribution |
metadata |
JSON — includes tags you pass per-request |
cache_hit |
Whether LiteLLM's cache served the response |
Retrieval: direct SQL against Postgres, or LiteLLM's REST endpoints:
# Spend per key
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
"https://<your-proxy>/spend/logs?api_key=sk-..."
# Global spend report grouped by team
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
"https://<your-proxy>/global/spend/report?group_by=team"
Note: by default spend logs store metadata, not the message payloads. To store prompts/responses you must enable it (see 4.4) — with the privacy caveats in section 11.
4.2 Callbacks (success/failure hooks)
LiteLLM can push a structured log object to external systems on every request completion or failure. The callback payload includes the full request (messages), response, token usage, cost, latency, and any exception. Commonly used targets:
| Callback | Use case |
|---|---|
langfuse |
LLM-native tracing UI — best for prompt/response inspection & eval datasets |
otel (OpenTelemetry) |
Ship traces to Azure Application Insights via the OTLP exporter — keeps everything in Azure |
s3 / azure_storage |
Dump raw request/response JSON to blob storage for batch analysis |
prometheus |
Metrics endpoint for scraping (request counts, latency histograms, spend) |
| Custom Python callback | Your own CustomLogger class — do anything (e.g., write JSONL for eval pipelines) |
A minimal custom callback that writes eval-ready JSONL:
# custom_logger.py — referenced from config.yaml: callbacks: ["custom_logger.eval_logger"]
from litellm.integrations.custom_logger import CustomLogger
import json, datetime
class EvalLogger(CustomLogger):
def log_success_event(self, kwargs, response_obj, start_time, end_time):
record = {
"ts": start_time.isoformat(),
"model": kwargs.get("model"),
"messages": kwargs.get("messages"),
"response": response_obj.choices[0].message.content,
"prompt_tokens": response_obj.usage.prompt_tokens,
"completion_tokens": response_obj.usage.completion_tokens,
"latency_s": (end_time - start_time).total_seconds(),
"cost_usd": kwargs.get("response_cost"),
}
with open("logs/requests.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
eval_logger = EvalLogger()
4.3 Proxy server logs (stdout)
The LiteLLM process logs request handling, routing decisions, retries, and errors to stdout — with --detailed_debug it includes the raw request sent to Anthropic and the raw response. In Azure, stdout is captured by the container platform and lands in Log Analytics (section 5). Useful for debugging why a request failed (auth, rate limit, timeout, malformed request) rather than for per-request analytics.
4.4 Payload logging (prompts & responses)
Off by default. Options to capture actual message content:
general_settings: store_prompts_in_spend_logs: true— puts messages/responses into the spend-log metadata.- A callback target that stores payloads (Langfuse, blob storage, custom logger).
For evaluation work you almost always want payloads captured somewhere — otherwise you have metrics but no way to re-judge quality.
5. Azure Logging — The Landscape
Azure's observability stack has a few named pieces that are easy to confuse:
| Component | What it is | Role in this setup |
|---|---|---|
| Azure Monitor | The umbrella platform for all metrics + logs | Everything below is part of it |
| Log Analytics workspace | The queryable log database (KQL) | Where container/App Service logs and diagnostics land |
| Application Insights | APM: request traces, dependencies, exceptions | Receives OTel traces from LiteLLM or your client app |
| Diagnostic Settings | Per-resource routing rules for platform logs | You must enable these — many logs aren't collected by default |
| Azure Metrics | Time-series numeric data (CPU, requests, latency) | Infrastructure health, autoscale signals |
| Activity Log | Control-plane audit (who changed what resource) | Compliance/audit, not per-request LLM data |
The one thing people miss
Platform logs are not collected until you create a Diagnostic Setting routing them to a Log Analytics workspace (or Storage Account / Event Hub). If you look in Log Analytics and find nothing, check:
Azure Portal → your resource (Container App / App Service / AKS) → Diagnostic settings → Add diagnostic setting → select log categories → Send to Log Analytics workspace
Which tables to query (by hosting choice)
| Hosting | Log Analytics table | Contains |
|---|---|---|
| Container Apps | ContainerAppConsoleLogs_CL |
LiteLLM stdout/stderr |
| Container Apps | ContainerAppSystemLogs_CL |
Scaling, restarts, probes |
| AKS | ContainerLogV2 |
Pod stdout/stderr |
| AKS | KubeEvents, KubePodInventory |
Cluster events, pod state |
| App Service | AppServiceConsoleLogs |
stdout/stderr |
| App Service | AppServiceHTTPLogs |
Per-request HTTP method/path/status/latency |
| App Insights | requests, dependencies, exceptions, traces |
APM telemetry (OTel) |
| Any resource | AzureDiagnostics / AzureMetrics |
Legacy-format diagnostics & metrics |
6. What Gets Logged Where (End-to-End Trace)
Follow one request through the system and note every log artifact it produces:
| Step | Event | Log artifact | Location |
|---|---|---|---|
| 1 | Client sends request | Your app's log line (+ App Insights dependencies if instrumented) |
App Insights / your logs |
| 2 | Request hits Azure ingress | HTTP access log (status, latency, bytes) | AppServiceHTTPLogs / ingress logs |
| 3 | LiteLLM authenticates & routes | Proxy stdout (debug level) | ContainerAppConsoleLogs_CL etc. |
| 4 | LiteLLM calls Anthropic | Outbound dependency; retry/failure lines on error | Proxy stdout; App Insights dependencies if OTel enabled |
| 5 | Response returns | Spend log row (tokens, cost, latency, model) | LiteLLM Postgres |
| 6 | Callbacks fire | Full trace (prompt + response + metrics) | Langfuse / App Insights / blob / JSONL |
| 7 | Container-level effects | CPU/memory metrics, restarts, scale events | Azure Metrics, system-log tables |
Correlation strategy: pass a request ID end-to-end. LiteLLM accepts arbitrary metadata / extra_headers per request — put your trace/run ID there so you can join client logs ↔ spend logs ↔ Azure HTTP logs. With OpenTelemetry enabled on both the client and LiteLLM, W3C traceparent propagation does this automatically in App Insights.
7. How to Retrieve Log Data from Azure
Four practical extraction paths, from interactive to fully automated:
7.1 Azure Portal (interactive)
Portal → Log Analytics workspace → Logs → write KQL → Export → CSV (or "Export to Power BI"). Good for exploration; limited for bulk export (query result caps ~30k rows / 64 MB in the portal).
7.2 Azure CLI
az login
az extension add --name log-analytics # first time only
# Query a workspace with KQL
az monitor log-analytics query \
--workspace <WORKSPACE_GUID> \
--analytics-query "ContainerAppConsoleLogs_CL | where TimeGenerated > ago(1d) | take 100" \
--output json > logs.json
Timespan flag: --timespan P1D (ISO-8601 duration) instead of embedding ago() in the query if you prefer.
7.3 Python SDK (azure-monitor-query) — best for eval pipelines
from datetime import timedelta
from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
import pandas as pd
credential = DefaultAzureCredential() # az login / managed identity / env vars
client = LogsQueryClient(credential)
WORKSPACE_ID = "<workspace-guid>"
KQL = """
AppServiceHTTPLogs
| where CsUriStem == "/v1/chat/completions"
| project TimeGenerated, ScStatus, TimeTaken, CsBytes, ScBytes
"""
response = client.query_workspace(
workspace_id=WORKSPACE_ID,
query=KQL,
timespan=timedelta(days=7),
)
if response.status == LogsQueryStatus.SUCCESS:
table = response.tables[0]
df = pd.DataFrame(data=table.rows, columns=table.columns)
df.to_parquet("results/http_logs.parquet")
Install: pip install azure-monitor-query azure-identity pandas
7.4 Continuous export (for large volumes / archival)
- Diagnostic Settings → Storage Account: cheap archival of raw JSON logs in blob storage (hourly folders). Read back with
azure-storage-blob. - Diagnostic Settings → Event Hub: real-time streaming to downstream consumers (e.g., a Python consumer writing JSONL for your eval harness).
- Log Analytics Data Export rules: export whole tables to Storage/Event Hub continuously.
Don't forget the non-Azure sources
For LLM-specific fields, extracting from LiteLLM's Postgres is usually simpler and richer than KQL:
import pandas as pd
import sqlalchemy
engine = sqlalchemy.create_engine(DATABASE_URL) # Azure Database for PostgreSQL
df = pd.read_sql("""
SELECT request_id, model, spend, prompt_tokens, completion_tokens,
"startTime", "endTime",
EXTRACT(EPOCH FROM ("endTime" - "startTime")) AS latency_s
FROM "LiteLLM_SpendLogs"
WHERE "startTime" > NOW() - INTERVAL '7 days'
""", engine)
8. KQL Query Cookbook for LLM Logs
KQL (Kusto Query Language) is Azure's log query language — pipe-based, like a SQL/pandas hybrid. Queries to keep handy:
Errors in the LiteLLM container (Container Apps)
ContainerAppConsoleLogs_CL
| where TimeGenerated > ago(24h)
| where Log_s has_any ("ERROR", "Traceback", "RateLimitError", "AuthenticationError")
| project TimeGenerated, Log_s
| order by TimeGenerated desc
Request volume + error rate over time (App Service HTTP logs)
AppServiceHTTPLogs
| where TimeGenerated > ago(7d)
| summarize
total = count(),
errors = countif(ScStatus >= 500),
p50_ms = percentile(TimeTaken, 50),
p95_ms = percentile(TimeTaken, 95)
by bin(TimeGenerated, 1h)
| extend error_rate = round(100.0 * errors / total, 2)
| render timechart
Latency distribution for the completions endpoint
AppServiceHTTPLogs
| where CsUriStem == "/v1/chat/completions"
| summarize p50=percentile(TimeTaken,50), p90=percentile(TimeTaken,90),
p99=percentile(TimeTaken,99), max=max(TimeTaken)
by bin(TimeGenerated, 1d)
429s (rate limiting) — upstream vs proxy
AppServiceHTTPLogs
| where ScStatus == 429
| summarize count() by bin(TimeGenerated, 15m)
| render timechart
App Insights: LLM dependency calls (if OTel enabled)
dependencies
| where target has "anthropic" or name has "chat"
| summarize avg(duration), percentile(duration, 95), count() by name, resultCode
| order by count_ desc
Container restarts / OOM kills (a hidden cause of "missing" logs)
ContainerAppSystemLogs_CL
| where Log_s has_any ("restart", "Killing", "OOM", "probe failed")
| project TimeGenerated, Log_s
KQL survival kit: where (filter) → project (select columns) → summarize ... by bin(TimeGenerated, 1h) (group/aggregate) → render timechart (visualize) → take N (limit) → parse_json() (extract fields from JSON-in-string columns).
9. What Else You Can (and Should) Log
Beyond the defaults, from an evaluation-engineering perspective:
High value — add these if missing
| What | How | Why it matters for evals |
|---|---|---|
| Full prompts + completions | LiteLLM callback → Langfuse / blob / JSONL | Without payloads you can't re-judge quality or build regression datasets |
| Per-request tags/metadata | metadata: {"run_id": ..., "eval_name": ..., "variant": "A"} in the request body |
Lets you slice logs by experiment/variant — the backbone of A/B comparisons |
stop_reason / finish_reason |
Present in responses; ensure your logger keeps it | Distinguishes truncation (max_tokens/length) from clean completion — truncated outputs contaminate quality metrics |
| Cache hits | LiteLLM logs cache_hit; Anthropic responses carry cache_read_input_tokens |
Cached responses have different latency/cost profiles — separate them in baselines |
| Streaming time-to-first-token | OTel spans or custom callback timing | User-perceived latency ≠ total latency |
| Retries & fallbacks | LiteLLM router logs; enable otel for spans per attempt |
A "successful" request that took 3 retries is a reliability signal |
| Client-side request ID propagation | extra_headers / metadata with your own UUID |
Joins across LiteLLM DB, Azure logs, and your test-run records |
| Model version actually served | Log the response model field, not the alias you requested |
Aliases can be re-pointed; evals must record the true model snapshot |
Sometimes valuable
- Guardrail/moderation outcomes (if you add LiteLLM guardrails) — refusal and block rates over time.
- User feedback signals (thumbs up/down) — joinable to request IDs → labeled eval data for free.
- Prompt-template version — tag every request with the template hash; behavioral drift often traces to prompt edits, not model changes.
- Azure resource metrics during load tests — CPU/memory of the proxy under load; the proxy itself can be the latency bottleneck, not the model.
10. From Logs to Evaluations
The point of extracting all this: production logs are an evaluation goldmine. A workflow that matches the baseline → experiment → comparison → report pattern:
10.1 Build an eval dataset from logs
- Extract a representative sample of real prompts (LiteLLM payload logs → JSONL).
- Deduplicate & stratify — sample across intents, lengths, and time windows, not just the latest day.
- Label — human labels, user-feedback joins, or LLM-as-judge on the logged responses.
- Freeze the dataset (
datasets/prod-sample-YYYY-MM-DD.jsonl) — it's now a regression suite.
10.2 Metrics you can compute directly from logs (no re-running needed)
| Metric | Source |
|---|---|
| Cost per request / per user / per feature | LiteLLM spend |
| Token distributions (prompt vs completion) | prompt_tokens, completion_tokens |
| Latency p50/p95/p99, TTFT | startTime/endTime, OTel spans |
| Error rate, rate-limit rate, retry rate | HTTP logs + proxy logs |
| Truncation rate | finish_reason == "length" share |
| Refusal rate | stop_reason == "refusal" share (Claude 4.5+) |
| Cache efficiency | cache_hit / cache_read_input_tokens |
These are your observational baselines. Quality metrics (accuracy, faithfulness, helpfulness) require judging the logged payloads — offline, with pytest + an eval framework.
10.3 Replay-based comparison (model or prompt changes)
logs → frozen dataset → replay through LiteLLM against:
baseline: current model (e.g., claude-sonnet-5)
experiment: candidate (new model / new prompt / new params)
→ judge both outputs (LLM-as-judge or rules)
→ compare: quality delta, cost delta, latency delta
→ report (never metrics in isolation)
Because LiteLLM gives every variant the same OpenAI-format interface, replaying the same dataset against different model_name aliases is trivial — and tagging replays with metadata: {"eval_name": ..., "variant": ...} means the comparison data itself lands back in your logs, queryable by the same KQL/SQL.
Suggested tooling (Python): pytest for the harness, deepeval or promptfoo for judged metrics, pandas for log analysis, results stored as JSONL under results/runs/<date>/<model>/.
11. Privacy, Cost, and Retention Considerations
- Payload logging is opt-in for a reason. Prompts may contain PII/PHI. If you enable prompt storage, know your data classification, restrict access (Azure RBAC on the workspace/storage), and consider LiteLLM's redaction hooks or masking in a custom callback.
- Log Analytics ingestion costs money (~per-GB ingested, plus retention beyond the included period). Verbose LiteLLM debug logging on a high-traffic proxy adds up — use
Basic Logstier or blob export for bulk payloads, keep Analytics tier for operational queries. - Retention: default Log Analytics retention is 30–90 days depending on config. Eval datasets should be exported and frozen — don't rely on the workspace as your dataset store.
- Secrets hygiene: never log API keys; LiteLLM hashes virtual keys in spend logs — keep it that way in custom callbacks.
- Anthropic-side retention is separate from your logging: your Azure logs are yours; the API provider's retention policy is governed by your agreement with them.
12. Quick Reference Card
| I want to… | Go to… |
|---|---|
| See per-request cost/tokens/latency | LiteLLM Postgres LiteLLM_SpendLogs, or /spend/logs endpoint |
| See actual prompts & responses | LiteLLM callback target (Langfuse / blob / custom JSONL) — must be enabled |
| Debug a failing request | Container stdout logs → ContainerAppConsoleLogs_CL (KQL) with --detailed_debug on |
| See HTTP status/latency per request | AppServiceHTTPLogs (or ingress logs) via KQL |
| Trace one request end-to-end | App Insights (OTel on client + LiteLLM), or your own request-ID in metadata |
| Bulk-export logs for analysis | Python azure-monitor-query → DataFrame → parquet; or Diagnostic Settings → Storage |
| Query logs ad hoc | Portal → Log Analytics → KQL (see cookbook, section 8) |
| Check infra health during a load test | Azure Metrics (CPU/mem/replicas) + ContainerAppSystemLogs_CL |
| Build an eval dataset | Extract payload logs → dedupe/stratify → label → freeze JSONL |
Minimal setup checklist for a well-instrumented deployment
- Diagnostic Settings enabled on the proxy's host resource → Log Analytics
- LiteLLM
database_urlset (spend logs on) - At least one payload-capturing callback (Langfuse / blob / custom) — with PII review
- OTel → Application Insights for distributed tracing (optional but recommended)
- Per-request
metadatatags flowing from clients (run IDs, variants) - Model IDs pinned & logged from the response, not the request alias
- Export/retention plan for eval datasets (don't rely on workspace retention)
Next steps you might want: (a) a KQL workbook/dashboard spec for this stack, (b) a Python extraction script wired to your actual workspace, or (c) an eval-methodology doc (baseline/experiment design, judge prompts, metrics definitions) built on top of this logging foundation.