Try it free

Send Claude Platform traces to Dynatrace

  • Latest Dynatrace
  • How-to guide
  • 7-min read

The Claude Platform can export OpenTelemetry (OTel) traces for Messages API requests, tool uses, and Claude Managed Agents sessions. The spans follow the GenAI semantic conventions, which AI Observability AI Observability natively understands.

You can collect traces from two sources, and combine them:

  • API tracing: The Claude API sends server-side spans directly to Dynatrace. You configure it once in the Claude Console, no code changes required.
  • SDK tracing: The Claude SDK emits client-side spans through the OTel tracer provider of your application, so Claude calls appear inside your own distributed traces.

To see billed cost and organization-wide token usage, you can also import usage and cost reports from the Claude Usage and Cost Analytics API.

OpenTelemetry export on the Claude Platform is in beta. Span and attribute names may change.

Who is this for?

This guide is for:

  • AI engineering teams building applications and agents on the Claude API or Claude Managed Agents.
  • Site Reliability Engineers responsible for monitoring AI workloads.
  • Platform engineers integrating OTel data from AI providers into Dynatrace.

What will you learn?

By following this guide, you'll learn:

  • How to add Dynatrace as an OTel destination in the Claude Console.
  • How to export Claude SDK spans to Dynatrace from your application.
  • How to import Claude usage and cost reports into Dynatrace as business events.
  • How to view Claude requests, prompts, token usage, and latency in AI Observability AI Observability.

Before you begin

Prerequisites

To get started, you need to have:

  • The Admin role in your Claude Console organization (for API tracing).
  • Dynatrace SaaS with a Dynatrace Platform Subscription (DPS) license that has Traces powered by Grail enabled.
  • OTLP ingestion enabled, see OpenTelemetry and Dynatrace.
  • A platform token with the openpipeline:traces:ingest scope.
  • For querying usage and cost data, including billed costs:
    • An Admin API key (Claude Console) or an Analytics API key with the read:analytics scope (Claude Enterprise).
    • A Dynatrace platform token with the openpipeline:bizevents:ingest scope.

Prior knowledge

It's helpful to have some basic knowledge of:

  • OTel concepts like spans, exporters, and OTLP.
  • Dynatrace permissions and data ingestion.

Send API traces to Dynatrace

1. Add Dynatrace as a destination

  1. In the Claude Console, go to Settings > OpenTelemetry and select Add destination.

  2. For Provider, select Custom.

  3. For OTLP endpoint URL, enter https://<your-environment-id>.live.dynatrace.com/api/v2/otlp. Don't append /v1/traces; the Claude API adds it.

  4. Under Request headers, add the Authorization header with the value Bearer <your-platform-token>.

  5. Choose a Content mode:

    • Metadata only (default): identifiers, timing, status, and token counts.
    • Full content: also includes prompts and responses, which populate the Prompts view in AI Observability AI Observability.
  6. Select the Workspaces to export, then save the destination.

The Claude Console Edit destination dialog with the Dynatrace OTLP endpoint URL, an Authorization request header, and Full content mode selected
The Claude Console Edit destination dialog with the Dynatrace OTLP endpoint URL, an Authorization request header, and Full content mode selected

With Full content, the default Maximum content size in MB of 0.75 MB per span works with Dynatrace. Dynatrace accepts OTLP trace requests of up to 8 MB. For details, see OTLP trace ingest limits. If you raise the limit under Advanced settings, keep it well below 8 MB, because a single request can contain several spans.

2. Test the destination

In the destination's More actions menu, select Test. The Claude API sends a single anthropic.test span to Dynatrace and reports whether delivery succeeded. After real traffic flows, the Delivery column shows Healthy.

The Claude Console OpenTelemetry destinations list with a notification that Dynatrace received the test span
The Claude Console OpenTelemetry destinations list with a notification that Dynatrace received the test span

Send SDK traces to Dynatrace

The Claude SDK emits spans through the tracer provider registered in your application. Configure an OTLP exporter that points to Dynatrace, then pass the provider to the client. The following example uses Python; the SDKs for TypeScript, Java, Go, C#, PHP, and Ruby support the same options.

1. Install dependencies

pip install anthropic opentelemetry-sdk opentelemetry-exporter-otlp-proto-http

2. Configure the exporter

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://<your-environment-id>.live.dynatrace.com/api/v2/otlp/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer <your-platform-token>"
export OTEL_SERVICE_NAME="my-claude-app"
from anthropic import Anthropic
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
client = Anthropic(
open_telemetry={
"tracer_provider": provider,
"traces": {"content_mode": "content"}, # omit for metadata only
}
)
message = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=256,
messages=[{"role": "user", "content": "Write a haiku about observability."}],
)
print(message.content[0].text)
provider.shutdown()

Use API tracing and SDK tracing together, both sending to Dynatrace. The SDK propagates the W3C traceparent header, so the API's server spans join your application's trace.

Send usage and cost data to Dynatrace

Traces show individual requests, but they don't show what Anthropic bills you. You can use the Usage and Cost Analytics API to get aggregated token usage and cost for your whole organization, including traffic that you don't trace, such as Claude Code and Claude Console usage.

Using this API requires an Admin API key (Claude Console) or an Analytics API key (Claude Enterprise). If you don't have one of these API keys, you can still query token usage from your traces, but not billed cost.

For more information, see Get token usage without an API key.

To send this data to Dynatrace, run a small job that polls the API and sends the results as business events. The job reads two endpoints:

EndpointData

/v1/organizations/analytics/usage

Hourly token counts: uncached_input_tokens, cache_read_input_tokens, cache_creation (5-minute and 1-hour), output_tokens, total_tokens, requests, and server_tool_use.web_search_requests.

/v1/organizations/analytics/cost

Daily cost in USD: amount (after discounts, before credits) and list_amount (at list prices), split by cost_type (tokens, web_search, code_execution, or session_usage) and token_type.

The script groups both by model, workspace_id, service_tier, and product (for example, messages_api or claude_code). The API supports more dimensions, such as api_key_id and actor (the user or service account behind each request). For the full list, see the usage and cost API reference.

The API reports aggregates only. It doesn't include prompts, responses, or per-request identifiers.

  • The API is in beta and requires the anthropic-beta: analytics-usage-cost-2026-08-24 header.
  • Data typically refreshes within 4 hours and can take up to 24 hours. Each response reports data_refreshed_at; the following script imports a day only after the data covers it completely.
  • Values can still be revised until about 7 days after the end of the calendar month. For final, invoice-grade totals, use the Claude Console.
  • The API reports amounts in cents as decimal strings. The following script converts them to USD.
  • Requests served under a Priority Tier commitment show zero token cost, because the commitment fee is billed separately.
  • Claude Enterprise organizations use the same endpoints with an Analytics API key. On Claude Platform on AWS, call each region's endpoint with AWS authentication. For details, see Authentication.

1. Run the import job

The following Python script imports the previous UTC day of hourly usage and daily cost and sends one business event per result row. Run it once a day, for example with cron or a scheduled CI job. If the data for that day isn't complete yet, the script exits without sending anything, so a later run can pick it up.

Re-running the job for the same day sends the same buckets again. Run it once per day, or deduplicate in your queries by bucket.start and the grouping fields. Query a timeframe that covers when the job ran, because event timestamps are the ingest time, not the bucket time.

Dynatrace rejects business events with a timestamp more than 24 hours in the past, so each event carries its bucket time in bucket.start and bucket.end and gets the ingest time as its timestamp.

export ANTHROPIC_ADMIN_KEY="<your-admin-or-analytics-api-key>"
export DT_ENV_URL="https://<your-environment-id>.live.dynatrace.com"
export DT_TOKEN="<your-platform-token>"
pip install requests

Save the following script as import_usage_cost.py and run it with python import_usage_cost.py.

Show the import script
import os
import sys
from datetime import datetime, timedelta, timezone
import requests
ANTHROPIC = "https://api.anthropic.com/v1/organizations/analytics"
HEADERS = {
"x-api-key": os.environ["ANTHROPIC_ADMIN_KEY"],
"anthropic-version": "2023-06-01",
"anthropic-beta": "analytics-usage-cost-2026-08-24",
"User-Agent": "Dynatrace/1.0.0 (https://www.dynatrace.com)",
}
DT_INGEST = os.environ["DT_ENV_URL"] + "/api/v2/bizevents/ingest"
DT_HEADERS = {"Authorization": "Bearer " + os.environ["DT_TOKEN"]}
GROUP_BY = ["model", "workspace_id", "service_tier", "product"]
def fetch(path, params):
"""Return all rows for a query, restarting if a page cursor expires."""
for _ in range(3): # restart if a page cursor expires
rows, query = [], dict(params, limit=1000)
while True:
resp = requests.get(f"{ANTHROPIC}/{path}", headers=HEADERS, params=query, timeout=60)
if resp.status_code == 410: # cursor expired after a data refresh
break
resp.raise_for_status()
body = resp.json()
refreshed = body["data_refreshed_at"]
if refreshed is None or datetime.fromisoformat(refreshed.replace("Z", "+00:00")) < end:
sys.exit(f"{path}: data not complete yet (refreshed at {refreshed}), try later")
rows += body["data"]
if not body["has_more"]:
return rows
query["page"] = body["next_page"]
sys.exit(f"{path}: page cursor kept expiring, try again later")
day = os.environ.get("IMPORT_DAY") # YYYY-MM-DD (UTC); defaults to the previous UTC day
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=timezone.utc) if day else today - timedelta(days=1)
end = start + timedelta(days=1)
window = {
"starting_at": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
"ending_at": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
events = []
for row in fetch("usage", {**window, "time_bucket_width": "1h", "group_by[]": GROUP_BY}):
g, u = row["group"], row["usage"]
events.append({
"event.type": "anthropic.usage",
"event.provider": "claude-platform",
"bucket.start": row["starting_at"],
"bucket.end": row["ending_at"],
"gen_ai.provider.name": "anthropic",
"gen_ai.request.model": g["model"],
"workspace_id": g["workspace_id"],
"service_tier": g["service_tier"],
"product": g["product"],
"requests": u["requests"],
"uncached_input_tokens": u["uncached_input_tokens"],
"cache_read_input_tokens": u["cache_read_input_tokens"],
"cache_creation_5m_input_tokens": u["cache_creation"]["ephemeral_5m_input_tokens"],
"cache_creation_1h_input_tokens": u["cache_creation"]["ephemeral_1h_input_tokens"],
"output_tokens": u["output_tokens"],
"total_tokens": u["total_tokens"],
"web_search_requests": u["server_tool_use"]["web_search_requests"],
})
for row in fetch("cost", {**window, "time_bucket_width": "1d",
"group_by[]": GROUP_BY + ["cost_type", "token_type"]}):
g, c = row["group"], row["cost"]
events.append({
"event.type": "anthropic.cost",
"event.provider": "claude-platform",
"bucket.start": row["starting_at"],
"bucket.end": row["ending_at"],
"gen_ai.provider.name": "anthropic",
"gen_ai.request.model": g["model"],
"workspace_id": g["workspace_id"],
"service_tier": g["service_tier"],
"product": g["product"],
"cost_type": g["cost_type"],
"token_type": g["token_type"],
"cost.usd": float(c["amount"]) / 100,
"cost.list_usd": float(c["list_amount"]) / 100,
})
for i in range(0, len(events), 500):
requests.post(DT_INGEST, headers=DT_HEADERS, json=events[i:i + 500], timeout=30).raise_for_status()
print(f"Sent {len(events)} events")

An Admin API key has organization-wide admin access to your Claude Console organization.

  • Give this job its own API key, and set an expiration.
  • Store the API key and the platform token in a secret store.
  • Do not store your API key and platform token directly in the script.

2. Query usage and cost

Once you've imported the data, use DQL to query your usage and costs.

  • Daily cost by model and product:

    fetch bizevents
    | filter event.type == "anthropic.cost"
    | summarize {
    cost_usd = sum(cost.usd),
    list_usd = sum(cost.list_usd)
    }, by: {day = toTimestamp(bucket.start), gen_ai.request.model, product}
    | sort day desc
  • Hourly tokens and requests by model:

    fetch bizevents
    | filter event.type == "anthropic.usage"
    | summarize {
    requests = sum(requests),
    input = sum(uncached_input_tokens + cache_read_input_tokens + cache_creation_5m_input_tokens + cache_creation_1h_input_tokens),
    output = sum(output_tokens)
    }, by: {hour = bin(toTimestamp(bucket.start), 1h), gen_ai.request.model}
    | sort hour

Get token usage without an API key

To get billed cost, you need an Admin API key (Claude Console) or an Analytics API key (Claude Enterprise). There's no workspace-level alternative.

If you don't have one of these API keys, you can get token usage from your traces, but not billed cost. Use DQL to query gen_ai.usage.input_tokens and gen_ai.usage.output_tokens:

fetch spans
| filter gen_ai.provider.name == "anthropic" and isNotNull(gen_ai.usage.input_tokens)
| summarize {
input_tokens = sum(gen_ai.usage.input_tokens),
output_tokens = sum(gen_ai.usage.output_tokens)
}, by: {day = bin(start_time, 1d), gen_ai.request.model}
| sort day desc

If you use API tracing and SDK tracing together, add and service.name == "claude-platform" to the filter to count each request once:

fetch spans
| filter gen_ai.provider.name == "anthropic" and service.name == "claude-platform" and isNotNull(gen_ai.usage.input_tokens)
| summarize {
input_tokens = sum(gen_ai.usage.input_tokens),
output_tokens = sum(gen_ai.usage.output_tokens)
}, by: {day = bin(start_time, 1d), gen_ai.request.model}
| sort day desc

Observe in Dynatrace

  1. In Dynatrace, go to AI Observability AI Observability.
  2. Go to the Prompts tab and filter by provider anthropic or by your model to find Claude requests.
  3. Open a trace to inspect anthropic.messages.create, anthropic.model_request, and tool spans and their gen_ai.* attributes.

The Agents topology view shows how your applications, the claude-platform service, the anthropic model provider, and the Claude models connect.

The AI Observability Agents topology view showing two AI apps calling the anthropic model provider and the claude-sonnet-5-5 model
The AI Observability Agents topology view showing two AI apps calling the anthropic model provider and the claude-sonnet-5-5 model

API spans arrive with service.name set to claude-platform. You can also query them in Distributed Tracing Distributed Tracing or with DQL:

fetch spans
| filter service.name == "claude-platform"
| fields start_time, span.name, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, duration
A Dynatrace notebook listing anthropic.test spans from the claude-platform service with gen_ai.provider.name set to anthropic
A Dynatrace notebook listing anthropic.test spans from the claude-platform service with gen_ai.provider.name set to anthropic

When a request has both an SDK span (CLIENT) and an API span (SERVER), both carry token usage. To count tokens once, sum only the API spans. For details, see Counting tokens.

Related topics

  • OpenTelemetry for the Claude API
  • Usage and Cost Analytics API
  • Ingest business events via API
  • AI Observability AI Observability
  • OpenTelemetry and Dynatrace
Related tags
AI Observability