DEV Community

Shell QA
Shell QA

Posted on

Complete End-to-End OpenTelemetry Setup Guide for AI Agents & Power BI Observability

OpenTelemetry E2E Setup Guide for AI Agents

This guide shows how to set up end-to-end OpenTelemetry observability for AI agents, from local tracing to production export. It covers the OpenTelemetry Collector, Python and Node.js instrumentation, agent-specific spans, LLM and tool-call tracing, validation, and production recommendations.

1. What You Are Instrumenting

For an AI agent, the most useful trace structure is a top-level agent run with child spans for every meaningful operation.


agent.run
|-- agent.plan
|-- llm.chat
|-- tool.call: knowledge_search
|-- retrieval.query
|-- memory.read
|-- llm.chat
|-- memory.write
`-- agent.finalize

Enter fullscreen mode Exit fullscreen mode

At minimum, trace these operations:

  • agent.run : one full user request or autonomous task

  • llm.chat : each model call

  • tool.call : each tool invocation

  • retrieval.query : vector search, database lookup, or document retrieval

  • memory.read : agent memory lookup

  • memory.write : agent memory update

  • agent.handoff : transfer to another agent or human

  • agent.finalize : final response construction

2. Recommended Architecture


Agent Application
|
v
OTLP traces, metrics, logs
|
v
OpenTelemetry Collector
|
v Exporter
v
Observability Backend

Enter fullscreen mode Exit fullscreen mode

Common observability backends include:

  • Jaeger

  • Grafana Tempo

  • Honeycomb

  • Datadog

  • New Relic

  • Azure Monitor

  • AWS X-Ray

  • Google Cloud Trace

  • Elastic Observability

For local development, you can start with the OpenTelemetry Collector plus a logging exporter. For production, send data from the Collector to your preferred backend.

3. Local OpenTelemetry Collector Setup

Create a file named otel-collector-config.yaml:


receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch:

exporters:
  logging:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [logging]

Enter fullscreen mode Exit fullscreen mode

Run the Collector with Docker on Windows PowerShell:


docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "${PWD}/otel-collector-config.yaml:/etc/otelcol/config.yaml" \
  otel/opentelemetry-collector:latest
Enter fullscreen mode Exit fullscreen mode

Run the Collector on macOS or Linux:

docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "$PWD/otel-collector-config.yaml:/etc/otelcol/config.yaml" \
  otel/opentelemetry-collector:latest

Enter fullscreen mode Exit fullscreen mode

The local OTLP endpoints are:

4. Python Agent Setup

Install dependencies:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
Enter fullscreen mode Exit fullscreen mode

Create telemetry.py:


from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

def configure_telemetry() -> None:
    resource = Resource.create({
        "service.name": "agent-service",
        "service.version": "1.0.0",
        "deployment.environment": "local",
    })

    provider = TracerProvider(resource=resource)
    exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)
Enter fullscreen mode Exit fullscreen mode

Instrument the agent:


from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
from telemetry import configure_telemetry

configure_telemetry()
tracer = trace.get_tracer("agent-service")

def run_agent(user_input: str) -> str:
    with tracer.start_as_current_span("agent.run") as span:
        span.set_attribute("agent.name", "support-agent")
        span.set_attribute("agent.version", "1.0.0")
        span.set_attribute("agent.input.length", len(user_input))

        try:
            with tracer.start_as_current_span("agent.plan") as plan_span:
                plan = create_plan(user_input)
                plan_span.set_attribute("agent.plan.steps", len(plan))

            with tracer.start_as_current_span("llm.chat") as llm_span:
                llm_span.set_attribute("gen_ai.system", "openai")
                llm_span.set_attribute("gen_ai.operation.name", "chat")
                llm_span.set_attribute("gen_ai.request.model", "gpt-4.1")

                llm_response = call_llm(user_input)

                llm_span.set_attribute("gen_ai.response.model", "gpt-4.1")
                llm_span.set_attribute("gen_ai.usage.input_tokens", llm_response.input_tokens)
                llm_span.set_attribute("gen_ai.usage.output_tokens", llm_response.output_tokens)

            with tracer.start_as_current_span("tool.call") as tool_span:
                tool_span.set_attribute("tool.name", "knowledge_search")
                tool_span.set_attribute("tool.call_id", llm_response.tool_call_id)

                documents = search_documents(llm_response.tool_query)

                tool_span.set_attribute("tool.success", True)
                tool_span.set_attribute("retrieval.document_count", len(documents))

            with tracer.start_as_current_span("agent.finalize") as final_span:
                final_answer = produce_answer(llm_response, documents)
                final_span.set_attribute("agent.output.length", len(final_answer))

            return final_answer

        except Exception as exc:
            span.record_exception(exc)
            span.set_status(Status(StatusCode.ERROR, str(exc)))
            raise
Enter fullscreen mode Exit fullscreen mode

5. Node.js or TypeScript Agent Setup

Install dependencies:


npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http
Enter fullscreen mode Exit fullscreen mode

Create telemetry.ts:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

export const telemetrySdk = new NodeSDK({
  serviceName: "agent-service",
  traceExporter: new OTLPTraceExporter({
    url: "http://localhost:4318/v1/traces",
  }),
});

export async function startTelemetry() {
  await telemetrySdk.start();
}

export async function stopTelemetry() {
  await telemetrySdk.shutdown();
}
Enter fullscreen mode Exit fullscreen mode

Instrument the agent:

import { SpanStatusCode, trace } from "@opentelemetry/api";
import { startTelemetry, stopTelemetry } from "./telemetry";

const tracer = trace.getTracer("agent-service");

export async function runAgent(userInput: string): Promise<string> {
  return tracer.startActiveSpan("agent.run", async span => {
    span.setAttribute("agent.name", "support-agent");
    span.setAttribute("agent.version", "1.0.0");
    span.setAttribute("agent.input.length", userInput.length);

    try {
      const plan = await tracer.startActiveSpan("agent.plan", async planSpan => {
        const result = await createPlan(userInput);
        planSpan.setAttribute("agent.plan.steps", result.length);
        planSpan.end();
        return result;
      });

      const llmResponse = await tracer.startActiveSpan("llm.chat", async llmSpan => {
        llmSpan.setAttribute("gen_ai.system", "openai");
        llmSpan.setAttribute("gen_ai.operation.name", "chat");
        llmSpan.setAttribute("gen_ai.request.model", "gpt-4.1");

        const response = await callLlm(userInput, plan);

        llmSpan.setAttribute("gen_ai.response.model", "gpt-4.1");
        llmSpan.setAttribute("gen_ai.usage.input_tokens", response.inputTokens);
        llmSpan.setAttribute("gen_ai.usage.output_tokens", response.outputTokens);
        llmSpan.end();

        return response;
      });

      const documents = await tracer.startActiveSpan("tool.call", async toolSpan => {
        toolSpan.setAttribute("tool.name", "knowledge_search");
        toolSpan.setAttribute("tool.call_id", llmResponse.toolCallId);

        const result = await searchDocuments(llmResponse.toolQuery);

        toolSpan.setAttribute("tool.success", true);
        toolSpan.setAttribute("retrieval.document_count", result.length);
        toolSpan.end();

        return result;
      });

      const finalAnswer = await tracer.startActiveSpan("agent.finalize", async finalSpan => {
        const answer = await produceAnswer(llmResponse, documents);
        finalSpan.setAttribute("agent.output.length", answer.length);
        finalSpan.end();
        return answer;
      });

      span.end();
      return finalAnswer;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: String(error),
      });
      span.end();
      throw error;
    }
  });
}

async function main() {
  await startTelemetry();

  try {
    await runAgent("How do I reset my password?");
  } finally {
    await stopTelemetry();
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Recommended Span Attributes

Use OpenTelemetry semantic conventions where possible.

GenAI Attributes

  • gen_ai.system

  • gen_ai.operation.name

  • gen_ai.request.model

  • gen_ai.response.model

  • gen_ai.request.temperature

  • gen_ai.request.max_tokens

  • gen_ai.usage.input_tokens

  • gen_ai.usage.output_tokens

Agent Attributes

  • agent.name

  • agent.version

  • agent.run_id

  • agent.session_id

  • agent.step.name

  • agent.step.index

  • agent.output.length

  • agent.input.length

Tool Attributes

  • tool.name

  • tool.call_id

  • tool.success

  • tool.error.type

  • tool.retry_count

Retrieval Attributes

  • retrieval.system

  • retrieval.index.name

  • retrieval.query_count

  • retrieval.document_count

  • retrieval.top_k

Memory Attributes

  • memory.operation

  • memory.scope

  • memory.result_count

7. What Not To Capture

Do not capture sensitive or high-risk data as span attributes unless your organization has explicit approval, redaction, access control, and retention policies.

Avoid storing:

  • Full prompts

  • Full model responses

  • User secrets

  • API keys or tokens

  • Raw documents

  • Email addresses

  • Payment information

  • Health data

  • Authentication headers

  • Full tool outputs

Prefer safe metadata:

  • prompt length

  • response length

  • token counts

  • model name

  • tool name

  • status

  • latency

  • retry count

  • document count

8. Metrics To Add

Traces show what happened for one request. Metrics show aggregate behavior.

Recommended metrics:

  • agent.run.count

  • agent.run.duration

  • agent.run.error.count

  • llm.request.count

  • llm.request.duration

  • llm.token.input.count

  • llm.token.output.count

  • tool.call.count

  • tool.call.duration

  • tool.call.error.count

  • retrieval.query.count

  • retrieval.document.count

  • agent.handoff.count

Start with traces first, then add metrics once your span structure is stable.

9. Logging Setup

Use logs for discrete application events, but correlate them with traces.

Recommended log fields:

  • trace_id

  • span_id

  • agent_run_id

  • agent.name

  • event.name

  • status

  • error.type

  • error.message

Keep logs redacted. Avoid logging raw prompts, completions, credentials, or document bodies.

10. Validation Checklist

Use this checklist to verify the setup end to end.

  • Start the OpenTelemetry Collector.

  • Run one local agent request.

  • Confirm the Collector receives spans.

  • Confirm the root span is named agent.run.

  • Confirm LLM calls appear as llm.chat child spans.

  • Confirm tool calls appear as tool.call child spans.

  • Confirm errors are recorded on failed spans.

  • Confirm token usage attributes appear when available.

  • Confirm sensitive content is not exported.

  • Confirm traces reach your observability backend.

11. Production Collector Example

For production, route data through the Collector and export to your backend.


otlp:
  protocols:
    http:
    grpc:

processors:
  batch:
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128

exporters:
  otlphttp:
    endpoint: https://your-observability-backend.example.com
    headers:
      api-key: ${OBSERVABILITY_API_KEY}

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp]
Enter fullscreen mode Exit fullscreen mode

Set environment variables securely through your deployment platform, secret manager, or CI/CD system.

12. Production Hardening

Before production rollout:

  • Add sampling if trace volume is high.

  • Redact sensitive inputs and outputs.

  • Add service names and versions.

  • Add deployment environment attributes.

  • Use secure OTLP endpoints.

  • Store backend API keys in a secret manager.

  • Configure retention policies.

  • Add alerts for agent error rate and high latency.

  • Track model cost through token metrics.

  • Validate compliance requirements before exporting AI data.

13. Sampling Guidance

For development, sample everything.

For production, consider:

  • 100% sampling for errors

  • 100% sampling for low-volume critical workflows

  • Lower probabilistic sampling for high-volume successful requests

  • Tail sampling when supported by your backend or Collector distribution

Example parent-based trace ID ratio sampling in Python:


from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased

provider = TracerProvider(
    sampler=ParentBased(TraceIdRatioBased(0.10))
)
Enter fullscreen mode Exit fullscreen mode

This samples roughly 10% of new traces while preserving parent-child trace consistency.

14. Framework-Specific Notes

LangChain

Instrument the chain or agent executor as agent.run, then wrap model calls, retrievers, and tools as child spans. If you use callbacks, create spans inside callback handlers.

Semantic Kernel

Trace kernel invocation as agent.run, function calls as tool.call, planner execution as agent.plan, and AI service calls as llm.chat.
AutoGen or Multi-Agent Systems
Use one trace for the full multi-agent workflow. Each agent turn can be represented as agent.run or agent.step, with agent.name distinguishing participants. Handoffs should be explicit spans named agent.handoff.

Custom Agents

Create spans directly around your orchestration code. This usually gives the best signal because you know where planning, memory, retrieval, tool execution, and finalization happen.

15. Minimal Rollout Plan

  • Add the OpenTelemetry SDK to the agent service.

  • Send traces to a local Collector.

  • Add a root agent.run span.

  • Add child spans for LLM calls and tool calls.

  • Validate trace shape locally.

  • Add retrieval and memory spans.

  • Add token and latency attributes.

  • Add redaction rules.

  • Export from the Collector to your observability backend.

  • Add dashboards and alerts.

16. Example Dashboard Panels

Useful panels for agent operations:

  • Agent run count by agent name

  • Agent error rate

  • Agent p50, p95, and p99 latency

  • LLM latency by model

  • Input and output tokens by model

  • Tool call failure rate

  • Retrieval document count

  • Handoff count

  • Estimated model cost

  • Top failing tools

17. Power BI Dashboard For Usage And Tokens

Power BI should usually read from a queryable store, not directly from raw OpenTelemetry traces. Use OpenTelemetry for observability, then write a normalized usage table for reporting.

Recommended flow:


Agent Application
|
v
OpenTelemetry traces and metrics
|
v
OpenTelemetry Collector
|
v Export
v
Azure Monitor, Log Analytics, SQL, Fabric Lakehouse, or Data Warehouse
|
v Power BI semantic model
v
Power BI dashboard
Enter fullscreen mode Exit fullscreen mode

Recommended Data Source Options

Use one of these patterns:

  • Azure Monitor or Log Analytics if your traces already go to Azure.

  • Application Insights if your application telemetry is already centralized there.

  • Azure SQL Database if you want simple relational reporting.

  • Microsoft Fabric Lakehouse or Warehouse if you want scalable analytics.

  • Databricks, Snowflake, or BigQuery if your organization already uses one of them.

For most teams, the easiest production setup is to store one row per LLM request in a table named agent_llm_usage.

Usage Table Schema

Create a reporting table with stable, low-cardinality columns.


CREATE TABLE agent_llm_usage (
    usage_id VARCHAR(100) NOT NULL,
    trace_id VARCHAR(100) NULL,
    span_id VARCHAR(100) NULL,
    timestamp_utc DATETIME2 NOT NULL,
    environment VARCHAR(50) NOT NULL,
    service_name VARCHAR(100) NOT NULL,
    agent_name VARCHAR(100) NOT NULL,
    agent_version VARCHAR(50) NULL,
    session_id VARCHAR(100) NULL,
    user_id_hash VARCHAR(100) NULL,
    model_provider VARCHAR(100) NULL,
    model_name VARCHAR(100) NOT NULL,
    operation_name VARCHAR(50) NOT NULL,
    input_tokens INT NOT NULL DEFAULT 0,
    output_tokens INT NOT NULL DEFAULT 0,
    total_tokens AS (input_tokens + output_tokens),
    estimated_cost_usd DECIMAL(18, 6) NULL,
    duration_ms INT NULL,
    status VARCHAR(30) NOT NULL,
    error_type VARCHAR(200) NULL,
    tool_name VARCHAR(100) NULL,
    retrieval_document_count INT NULL
);
Enter fullscreen mode Exit fullscreen mode

Do not store raw prompts, raw completions, secrets, full document text, or emails in this table. Use hashed user identifiers if user-level reporting is required.

Writing Usage Records

OpenTelemetry spans should still include token attributes such as:


gen_ai.usage.input_tokens
gen_ai.usage.output_tokens
gen_ai.request.model
gen_ai.system
agent.name
agent.session_id
Enter fullscreen mode Exit fullscreen mode

For Power BI, also write a compact business reporting event when each LLM call finishes. In Python, the event can be inserted into SQL, sent to Event Hubs, or written to your analytics pipeline.


def record_llm_usage(response, context):
    usage_record = {
        "usage_id": context.usage_id,
        "trace_id": context.trace_id,
        "span_id": context.span_id,
        "timestamp_utc": context.timestamp_utc,
        "environment": context.environment,
        "service_name": "agent-service",
        "agent_name": context.agent_name,
        "agent_version": context.agent_version,
        "session_id": context.session_id,
        "user_id_hash": context.user_id_hash,
        "model_provider": response.provider,
        "model_name": response.model,
        "operation_name": "chat",
        "input_tokens": response.input_tokens,
        "output_tokens": response.output_tokens,
        "estimated_cost_usd": response.estimated_cost_usd,
        "duration_ms": response.duration_ms,
        "status": "success",
    }

    insert_usage_record(usage_record)
Enter fullscreen mode Exit fullscreen mode

Cost Calculation

Keep model pricing in a separate table so costs can be updated without changing historical usage records.


CREATE TABLE model_pricing (
    model_provider VARCHAR(100) NOT NULL,
    model_name VARCHAR(100) NOT NULL,
    effective_from_utc DATETIME2 NOT NULL,
    input_cost_per_1k_tokens DECIMAL(18, 8) NOT NULL,
    output_cost_per_1k_tokens DECIMAL(18, 8) NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

The estimated cost formula is:


estimated_cost = (input_tokens / 1000 * input_price) + (output_tokens / 1000 * output_price)
Enter fullscreen mode Exit fullscreen mode

You can calculate this in the ingestion pipeline, SQL view, Fabric notebook, or Power BI semantic model. Pipeline or SQL calculation is usually better because all reports use the same cost logic.

Power BI Data Model

Use a simple star schema.

Fact table:

  • agent_llm_usage

Dimension tables:

  • dim_date

  • dim_agent

  • dim_model

  • dim_environment

  • dim_tool

Relationships:

  • dim_date[date] -> agent_llm_usage[date]

  • dim_agent[agent_name] -> agent_llm_usage[agent_name]

  • dim_model[model_name] -> agent_llm_usage[model_name]

  • dim_environment[environment] -> agent_llm_usage[environment]

  • dim_tool[tool_name] -> agent_llm_usage[tool_name]

Core DAX Measures

Create these measures in Power BI:


Total Requests = 
COUNTROWS(agent_llm_usage)

Successful Requests = 
CALCULATE(
    COUNTROWS(agent_llm_usage),
    agent_llm_usage[status] = "success"
)

Failed Requests = 
CALCULATE(
    COUNTROWS(agent_llm_usage),
    agent_llm_usage[status] <> "success"
)

Failure Rate = 
DIVIDE([Failed Requests], [Total Requests])

Input Tokens = 
SUM(agent_llm_usage[input_tokens])

Output Tokens = 
SUM(agent_llm_usage[output_tokens])

Total Tokens = 
[Input Tokens] + [Output Tokens]

Estimated Cost USD = 
SUM(agent_llm_usage[estimated_cost_usd])

Average Duration MS = 
AVERAGE(agent_llm_usage[duration_ms])

Average Tokens Per Request = 
DIVIDE([Total Tokens], [Total Requests])

P95 Duration MS = 
PERCENTILEX.INC(
    agent_llm_usage,
    agent_llm_usage[duration_ms],
    0.95
)
Enter fullscreen mode Exit fullscreen mode

Recommended Power BI Pages

Create these report pages:

  • Executive Overview

  • Token Usage

  • Cost Analysis

  • Agent Performance

  • Model Performance

  • Tool And Retrieval Usage

  • Errors And Reliability

Recommended visuals for the Executive Overview page:

  • Card: total requests

  • Card: total tokens

  • Card: estimated cost

  • Card: failure rate

  • Line chart: requests by day

  • Line chart: tokens by day

  • Bar chart: cost by agent

  • Bar chart: tokens by model

  • Table: top agents by cost, tokens, and failures

Recommended visuals for the Token Usage page:

  • Line chart: input tokens and output tokens over time

  • Stacked column chart: tokens by model

  • Matrix: agent name by model name with total tokens

  • Slicer: date range

Recommended visuals for the Cost Analysis page:

  • Slicer: environment

  • Slicer: agent name

  • Slicer: model name

  • Line chart: estimated cost by day

  • Bar chart: estimated cost by model

  • Bar chart: estimated cost by agent

  • Table: session or hashed user groups by cost

  • KPI: cost per request

Recommended visuals for the Errors And Reliability page:

  • Card: failed requests

  • Card: failure rate

  • Line chart: failures by day

  • Bar chart: errors by model

  • Bar chart: errors by tool

  • Table: error type, agent name, model name, and count

Refresh And Governance

Recommended refresh setup:

  • Development: manual refresh

  • Small production workload: scheduled refresh every 1 to 4 hours

  • High-volume production workload: Direct Lake, DirectQuery, or incremental refresh

Governance recommendations:

  • Use row-level security if teams should only see their own agents.

  • Store only hashed user IDs.

  • Keep prompt and completion content out of the reporting model.

  • Certify the semantic model once measures are validated.

  • Document model pricing assumptions and update them when provider pricing changes.

18. Troubleshooting

No Spans In Collector

Check:

  • Collector is running.

  • App exports to http://localhost:4318/v1/traces for OTLP HTTP.

  • Port 4318 is reachable.

  • The SDK is initialized before the agent runs.

  • The app shuts down cleanly so batch spans are flushed.

Root Span Exists But Child Spans Are Missing

Check:

  • Child spans are created inside the active context.

  • Async operations preserve context.

  • Spans are ended after work completes.

  • Exceptions do not skip span.end() in TypeScript.

Too Much Sensitive Data

Check:

  • No full prompts are added as attributes.

  • No raw tool outputs are added as attributes.

  • Logs do not contain secrets.

  • Collector processors or backend rules redact known sensitive fields.

High Trace Volume

Check:

  • Add sampling.

  • Reduce span count for noisy internal steps.

  • Keep attributes low-cardinality.

  • Avoid unique values such as full user IDs, emails, or raw queries in indexed attributes.

19. Final Recommended Baseline

A practical first production baseline is:


Root span:
agent.run

Child spans:
agent.plan
llm.chat
tool.call
retrieval.query
memory.read
memory.write
agent.finalize

Required attributes:
service.name
deployment.environment
agent.name
agent.version
gen_ai.system
gen_ai.request.model
gen_ai.usage.input_tokens
gen_ai.usage.output_tokens
tool.name
tool.success
Enter fullscreen mode Exit fullscreen mode

This gives enough visibility to answer the most important operational questions: what happened, where time was spent, which model and tools were used, whether the run failed, and how much token usage it consumed.

Top comments (0)