Observability
Two observability stacks, one OpenTelemetry pipeline. Whether you choose Azure Monitor or the Grafana OSS stack, the harness exports the same traces, metrics, and logs.
This page is the operator's view — how to wire each backend and what
to look at first. The full instrumentation contract (span/metric/log names, GenAI
semantic-convention mappings, resource attribute requirements, sampling policy) lives
in documentation/blueprints/agentic-harness-observability.md
(v1.3) and the single emit-point in
Domain.AI/Telemetry/Conventions/GenAiSemconvRegistry.cs. When this page
and the blueprint disagree, the blueprint wins.
The OpenTelemetry GenAI semantic conventions this harness emits are marked
Development upstream and are gated behind the
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental opt-in. Attribute
and span names under gen_ai.* may change in a future OTel release —
pin your collector mappings and re-validate on SDK upgrades. The current pinned version
is tracked in documentation/architecture/magentic-spans.md.
Two paths, same data
The harness uses OpenTelemetry (OTel) as its instrumentation standard. This means you choose where the telemetry goes — Azure-native or open-source — without changing any application code. The same spans, metrics, and log records flow regardless of the exporter.
Path 1 — Azure Native
Path 2 — OSS Stack
You can run both paths at the same time. The OTel SDK supports multiple exporters. Many teams send traces to Grafana/Tempo for developer debugging and metrics to Azure Monitor for operations/alerting.
What the harness instruments
The harness emits structured spans for every major subsystem. These spans carry semantic attributes that let you slice traces by model, tool, quality score, or token count.
Distributed traces (spans)
| Span | Source | What It Tracks |
|---|---|---|
invoke_agent |
Agent runtime | Top-level agent turn: prompt to response (parents every child span below) |
chat |
Chat client | A single LLM chat-completion call, with model, token, and finish-reason attributes |
execute_tool |
Tool executor | Individual tool execution with args and result. MCP tools surface here too (wrapped as AITools) |
embeddings |
Embedding service | Embedding generation for RAG ingestion/queries, with token count |
invoke_workflow |
Magentic orchestration | Root multi-agent workflow span (manager + participant rounds hang off it) |
invoke_a2a |
A2A client / server | Agent-to-agent dispatch, correlated across caller and callee spans |
Custom metrics (counters & histograms)
| Metric | Type | What It Measures |
|---|---|---|
agent.orchestration.turns_total |
Counter | Total agent turns processed |
agent.tokens.input / agent.tokens.output / agent.tokens.total |
Histogram | Token usage per turn (input, output, and combined) |
agent.tokens.cache_read / agent.tokens.cache_write |
Counter | Prompt-cache token accounting (cache hits vs. cache writes) |
rag.retrieval.duration |
Histogram | RAG retrieval latency in ms |
agent.tool.invocations |
Counter | Tool invocations by tool name |
agent.tool.duration |
Histogram | Tool execution latency in ms, by tool |
agent.governance.decisions / agent.governance.violations |
Counter | Governance decisions evaluated and policy violations raised |
agent.safety.blocks |
Counter | Content-safety blocks by category |
The instrument names above are exactly what the app emits (no harness. or
service-name prefix — that would double-prefix). The full authoritative list is
locked in
Tests/Presentation.AgentHub.Tests/Telemetry/Contracts/MetricNamingContract.cs.
On the Prometheus path the OTel Collector applies namespace: agentic_harness
(scripts/otel-collector/config.yaml), so agent.tokens.total
scrapes as agentic_harness_agent_tokens_total. Never add the prefix in app
code.
Azure Monitor setup
For the Azure-native path, configure the OTel exporter to send data to Application Insights via its connection string.
{
"OpenTelemetry": {
"Exporter": "AzureMonitor",
"AzureMonitor": {
"ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/"
}
}
}
Application Insights provides built-in dashboards for request rates, failure rates,
and response times. For agent-specific views, create custom
Azure Workbooks that query the customEvents and
dependencies tables in Log Analytics.
KQL query example
Agent turn analysis — average and p95 duration by span name, bucketed by hour:
dependencies
| where name in ("invoke_agent", "chat", "execute_tool", "embeddings")
| summarize avg(duration), percentile(duration, 95), count()
by name, bin(timestamp, 1h)
| render timechart
Grafana + Tempo + Prometheus setup
For the OSS path, point the OTel exporter at your OTel Collector endpoint.
{
"OpenTelemetry": {
"Exporter": "Otlp",
"Otlp": {
"Endpoint": "http://otel-collector:4317",
"Protocol": "grpc"
}
}
}
The OTel Collector receives spans, metrics, and logs, then routes them to Tempo (traces), Prometheus (metrics), and Loki (logs). Grafana provides the unified dashboard layer across all three backends. This stack runs as sidecar containers in the Container Apps environment or as a separate deployment.
The harness itself only knows how to speak two protocols: OTLP (over gRPC) and
Prometheus scrape (the /metrics endpoint, mapped in Program.cs).
The OTel Collector shown in the diagram is one common packaging choice โ it lets
you fan out to Tempo, Loki, and other backends without changing app config. You can
also point the OTLP exporter directly at a backend that accepts OTLP (Tempo, Honeycomb,
Datadog) and skip the collector entirely. The app doesn't know or care.
There are no pre-bundled Grafana dashboard JSON files in this template โ the metric
and span names listed above are the contract you build dashboards against. The shape
of every span and metric attribute lives in
Domain.AI/Telemetry/Conventions/GenAiSemconvRegistry.cs, and the metric
names are locked in by
Tests/Presentation.AgentHub.Tests/Telemetry/Contracts/MetricNamingContract.cs
(asserted by DashboardContractTests.cs), so dashboard authors can rely on
the names staying stable across releases.
Trace sampling — do it at the collector
High-traffic production traces are expensive to store. You keep the ones that matter (errors, slow requests, agent runs) and sample down the routine ones. The right place to make that decision is tail-based sampling at the OpenTelemetry Collector, not in application code.
The harness deliberately ships no in-app tail sampler. An earlier
version had one (a custom OTel BaseProcessor<Activity>), but it
could not actually drop spans: by the time a processor's OnEnd runs,
the OTLP and Azure Monitor exporters have already enqueued the span for export. It
exported 100% of spans regardless of the configured rate. Tail sampling needs to
buffer whole traces across process boundaries and decide after the trace
completes — that is exactly what the Collector's tail_sampling
processor is built for. The SDK exports every span; the collector decides what to keep.
Add a tail_sampling processor to your collector pipeline
(scripts/otel-collector/config.yaml). The policy below reproduces the intent of
the removed in-app sampler: always keep error traces, traces slower than 5s,
and agent-execution traces; probabilistically sample ~10% of everything else.
Policies are OR-combined — a trace matching any keep policy is retained.
processors:
tail_sampling:
# Buffer a trace this long after its last span before deciding.
decision_wait: 10s
# Cap on traces held in memory at once (tune for your throughput).
num_traces: 50000
expected_new_traces_per_second: 200
policies:
# 1. Always keep error traces.
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
# 2. Always keep slow traces (> 5s end-to-end).
- name: keep-slow
type: latency
latency:
threshold_ms: 5000
# 3. Always keep agent-execution traces (gen_ai.system set by the harness).
- name: keep-agent-by-genai-system
type: string_attribute
string_attribute:
key: gen_ai.system
values: [az.ai.agent, microsoft.extensions.ai, semantic_kernel]
enabled_regex_matching: false
# 3b. Also keep any trace that carries the agent.phase attribute.
- name: keep-agent-by-phase
type: string_attribute
string_attribute:
key: agent.phase
values: ['.+']
enabled_regex_matching: true
# 4. Sample ~10% of everything that matched none of the above.
- name: sample-the-rest
type: probabilistic
probabilistic:
sampling_percentage: 10
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo] # or your backend of choice
For tail sampling to work, every span for a trace must reach the collector, so keep
the app-side sampler at AlwaysOn (dev) or
ParentBased(TraceIdRatioBased(1.0)) (prod) and let the collector do the
thinning. If you run more than one collector instance, put a
loadbalancing exporter in front so all spans of a trace land on the same
collector — tail_sampling buffers per-collector and cannot see spans
routed elsewhere. The full sampling rationale lives in
documentation/blueprints/agentic-harness-observability.md
(gate G6).
Tracing an agent conversation
When you open a trace in your viewer (Jaeger, Tempo, or Application Insights Transaction search), here is what you see for a typical agent turn that involves RAG retrieval and tool use:
gen_ai.agent.name, gen_ai.conversation.id, and (on the
child chat spans) gen_ai.request.model.
gen_ai.request.model, gen_ai.usage.input_tokens, and
gen_ai.usage.output_tokens.
gen_ai.request.model and gen_ai.usage.input_tokens.
file_system tool.
Attributes include gen_ai.tool.name,
gen_ai.tool.call.arguments, and gen_ai.tool.call.result.
AITools, so they surface as ordinary execute_tool spans
carrying the same gen_ai.tool.* attributes.
Each span carries attributes like gen_ai.request.model,
gen_ai.usage.input_tokens, gen_ai.usage.output_tokens,
gen_ai.tool.name, and error.type that let you
diagnose exactly where time and tokens went.
The agent.tokens.total histogram, combined with the
agent.tokens.cache_read / agent.tokens.cache_write
prompt-cache counters and Azure OpenAI pricing, gives you a direct
cost-per-conversation metric. Set alerting thresholds based on your budget to catch
runaway agent loops before they burn through your token allocation.
Alerting
Recommended alert rules for production deployments. These apply to both Azure Monitor (metric alerts) and Grafana (Prometheus alert rules).
| Alert | Condition | Action |
|---|---|---|
| Agent error rate > 5% | invoke_agent spans with the error.type attribute set |
Page on-call |
| RAG latency > 3s p95 | rag.retrieval.duration histogram p95 |
Scale AI Search replicas |
| Governance violations spike | agent.governance.violations counter spike |
Check policy config and provider health |
| Token spend > daily budget | agent.tokens.total histogram sum |
Throttle non-critical requests |