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

  1. The Big Picture — Architecture
  2. What is Claude, and What is "Claude 3P" (Third-Party)?
  3. What is LiteLLM and How It Works
  4. What LiteLLM Itself Can Log
  5. Azure Logging — The Landscape
  6. What Gets Logged Where (End-to-End Trace)
  7. How to Retrieve Log Data from Azure
  8. KQL Query Cookbook for LLM Logs
  9. What Else You Can (and Should) Log
  10. From Logs to Evaluations
  11. Privacy, Cost, and Retention Considerations
  12. 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 Google 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

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:


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

  1. Client sends an OpenAI-format request to the proxy (/v1/chat/completions) with a virtual key.
  2. LiteLLM authenticates the key, checks budgets/rate limits.
  3. LiteLLM maps the requested model alias (e.g., claude-sonnet) to the configured backend model + real credentials (from its config.yaml).
  4. Request is translated to Anthropic's Messages API format and forwarded.
  5. 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:

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)

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


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

  1. Extract a representative sample of real prompts (LiteLLM payload logs → JSONL).
  2. Deduplicate & stratify — sample across intents, lengths, and time windows, not just the latest day.
  3. Label — human labels, user-feedback joins, or LLM-as-judge on the logged responses.
  4. 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


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


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.