OTel GenAI Semantic Conventions
Key Points
- OpenTelemetry has GenAI semantic conventions for AI/LLM observability — standardized attribute names.
Microsoft.Extensions.AI'sUseOpenTelemetry()middleware emits these automatically.- Standard fields:
gen_ai.system,gen_ai.request.model,gen_ai.usage.input_tokens,gen_ai.usage.output_tokens,gen_ai.response.id. - Vendor-portable dashboards: queries work across OpenAI, Anthropic, Google, etc.
- Sensitive data: prompts/responses NOT included by default; opt-in with care.
Standard attributes
gen_ai.system: "openai" | "anthropic" | "google.gemini" | "azure.ai.openai" | ...
gen_ai.operation.name: "chat" | "embeddings" | "image_generation"
gen_ai.request.model: "gpt-4o-mini"
gen_ai.request.temperature: 0.7
gen_ai.request.max_tokens: 1000
gen_ai.request.top_p: 0.9
gen_ai.response.model: "gpt-4o-mini-2024-07-18"
gen_ai.response.id: "chatcmpl-..."
gen_ai.response.finish_reasons: ["stop"]
gen_ai.usage.input_tokens: 1234
gen_ai.usage.output_tokens: 567
Span events (with sensitive data enabled)
gen_ai.system.message { content: "..." }
gen_ai.user.message { content: "..." }
gen_ai.assistant.message { content: "..." }
gen_ai.tool.message { content: "...", id: "...", name: "GetWeather" }
gen_ai.choice { index: 0, finish_reason: "stop", message: {...} }
Setup
chat = chat.AsBuilder()
.UseOpenTelemetry(sourceName: "MyApp.AI", configure: o =>
{
o.EnableSensitiveData = false; // default: false
})
.Build();
Activity source name should be configured for OTel collector.
Pipeline
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddSource("MyApp.AI")
.AddOtlpExporter())
.WithMetrics(m => m
.AddMeter("MyApp.AI")
.AddOtlpExporter());
Dashboards
// App Insights — top models by cost
dependencies
| where customDimensions["gen_ai.system"] != ""
| extend model = tostring(customDimensions["gen_ai.request.model"])
| extend in_tokens = toint(customDimensions["gen_ai.usage.input_tokens"])
| extend out_tokens = toint(customDimensions["gen_ai.usage.output_tokens"])
| summarize total_in = sum(in_tokens), total_out = sum(out_tokens) by model
// Latency p99 per model
dependencies
| where name == "chat"
| summarize p99 = percentile(duration, 99) by model = tostring(customDimensions["gen_ai.request.model"])
Vendor portability
Same query works against OpenAI, Anthropic, Gemini — because attribute names are standardized.
Cost tracking
var inTokens = activity?.GetTagItem("gen_ai.usage.input_tokens") as long?;
var outTokens = activity?.GetTagItem("gen_ai.usage.output_tokens") as long?;
var model = activity?.GetTagItem("gen_ai.request.model") as string;
var cost = ComputeCost(model, inTokens, outTokens);
Pre-computed cost as custom metric:
private static readonly Histogram<double> _cost = _meter.CreateHistogram<double>("ai.cost.usd");
_cost.Record(cost, new TagList { { "model", model }, { "tenant", tenantId } });
Custom attributes
Add app-specific tags:
activity?.SetTag("app.feature", "chatbot");
activity?.SetTag("user.tier", "premium");
activity?.SetTag("tenant.id", tenantId);
Use to filter dashboards.
Distributed tracing across calls
LLM call within agent → tool calls → DB calls all linked via OTel:
[GET /chat]
└── [Agent.InvokeAsync]
├── [chat (gen_ai.system=openai)]
├── [Tool: SearchDB]
│ └── [SQL Server query]
└── [chat (gen_ai.system=openai)] (final)
Click span → see all related operations.
Spec status
GenAI conventions are stable in OpenTelemetry as of 2026. Some fields experimental (e.g., for tool calls).
Senior considerations
- Always add OTel for AI calls.
- Don't enable sensitive data in prod (PII).
- Do add custom tags for app context.
- Cost dashboards save money — visibility = control.