How to Achieve Agent Observability Using OpenTelemetry and Langfuse

Agent observability using OpenTelemetry and Langfuse requires instrumenting your code with the OpenTelemetry GenAI Semantic Conventions and exporting spans to Langfuse via an OTLP collector endpoint.

The rohitg00/ai-engineering-from-scratch repository provides a complete, dependency-free implementation for tracing LLM agents using standardized telemetry. By following the OpenTelemetry GenAI Semantic Conventions, you can capture hierarchical spans for agent creation, LLM invocations, and tool calls, then visualize them in Langfuse for debugging and cost analysis.

Understanding the OpenTelemetry GenAI Semantic Conventions

The OpenTelemetry GenAI Semantic Conventions define a standardized attribute set using the gen_ai.* namespace. This standardization ensures that span names, attributes, and content-capture rules remain consistent across all LLM vendors and tooling ecosystems.

According to the source code in phases/14-agent-engineering/23-otel-genai-conventions/code/main.py, every logical agent operation creates a parent span with specific attributes:

  • gen_ai.agent.name identifies the specific agent instance
  • gen_ai.operation.name specifies the operation type (e.g., create_agent, invoke_agent)
  • gen_ai.provider.name indicates the LLM vendor (e.g., anthropic, openai)
  • gen_ai.request.model records the specific model identifier

Child spans represent sub-operations such as individual prompt sends or tool-call responses, creating a coherent trace tree that any OTEL-compatible backend can render.

Implementing the Tracer in Python

The repository ships a pure-standard-library Python tracer that implements these conventions without external dependencies. Located at phases/14-agent-engineering/23-otel-genai-conventions/code/main.py, this tracer generates hierarchical span trees that capture the full lifecycle of an agent execution.

The implementation includes an ExternalContentStore class (lines 28-44) that holds large payloads—full prompts and responses—offline. By setting capture_inline=False, spans store only reference IDs to respect size limits, preventing the trace payload from becoming unwieldy while maintaining access to complete conversation history.


# File: phases/14-agent-engineering/23-otel-genai-conventions/code/main.py

tracer = Tracer(capture_inline=False)

# Create an agent

tracer.start_span(
    "create_agent research_bot",
    attributes={
        "gen_ai.agent.name": "research_bot",
        "gen_ai.operation.name": "create_agent",
        "gen_ai.provider.name": "anthropic",
    },
)
tracer.end_span()

# Invoke the agent (LLM call)

invoke = tracer.start_span(
    "invoke_agent research_bot",
    attributes={
        "gen_ai.agent.name": "research_bot",
        "gen_ai.operation.name": "invoke_agent",
        "gen_ai.provider.name": "anthropic",
        "gen_ai.request.model": "claude-opus-4-6",
    },
)

# …loop over turns, emit chat and tool_call spans

tracer.end_span()

Configuring the OTLP Collector for Langfuse

Once your tracer emits spans, you must forward them to a backend using the OTLP Collector. The collector receives OpenTelemetry JSON-Lines or protobuf spans and forwards them to Langfuse's ingestion endpoint.

As documented in phases/13-tools-and-protocols/20-opentelemetry-genai/docs/en.md, you can configure the collector to push spans to http://localhost:4318/v1/traces (or your Langfuse instance URL). The following export function serializes the span tree and transmits it via socket:

import json, time, socket

def export_spans(root_span: Span, endpoint: str = "localhost:4317"):
    """Serialises the span tree as JSON-Lines and POSTs to an OTLP collector."""
    payload = []

    def walk(span: Span):
        if span.name != "__root__":
            payload.append({
                "name": span.name,
                "kind": span.kind,
                "attributes": span.attributes,
                "start_time_unix_nano": span.start_ns,
                "end_time_unix_nano": span.end_ns,
            })
        for child in span.children:
            walk(child)

    walk(root_span)
    data = "\n".join(json.dumps(p) for p in payload).encode()
    sock = socket.create_connection((endpoint.split(":")[0], int(endpoint.split(":")[1])))
    sock.sendall(data)
    sock.close()

# After the run:

export_spans(tracer.root, endpoint="localhost:4317")

Start the collector with a configuration pointing to your Langfuse instance: otelcol --config otel-collector.yaml.

Viewing and Analyzing Traces in Langfuse

Langfuse serves as the observability backend, storing spans, token counters, and session replay data. When configured to receive OTLP spans, Langfuse renders a hierarchical view of the agent execution, correlating token usage across turns and providing cost attribution dashboards.

To view traces:

  1. Navigate to the Langfuse UI and select Traces
  2. Locate your agent run by session ID or timestamp
  3. Expand the trace tree to see nested agent, LLM, and tool spans
  4. Click external content store links (when capture_inline=False) to view full prompts and responses

Langfuse offers an MIT-licensed self-hosted option or a free cloud tier supporting 50,000 events per month, making it accessible for development and production monitoring alike.

Alternative Backends and Portability

Because the implementation relies on standardized OpenTelemetry conventions rather than vendor-specific APIs, you can swap Langfuse for Arize Phoenix or Comet Opik without modifying your instrumentation code. Both alternatives expose OTLP collector endpoints and understand the gen_ai.* attribute schema.

This portability ensures your agent observability strategy remains backend-agnostic. Simply reconfigure the OTLP collector endpoint in otel-collector.yaml to point to Phoenix or Opik, and the same spans will render correctly in the new UI.

Summary

  • Standardized Instrumentation: Use the gen_ai.* attribute set from the OpenTelemetry GenAI Semantic Conventions to ensure vendor-neutral tracing.
  • Zero-Dependency Tracing: The phases/14-agent-engineering/23-otel-genai-conventions/code/main.py tracer requires only Python standard library components.
  • Efficient Payload Management: ExternalContentStore handles large prompts offline, keeping spans lightweight while preserving full conversation history.
  • Universal Export: OTLP collectors bridge your instrumented code to Langfuse, Phoenix, or Opik without code changes.
  • Complete Visibility: Langfuse provides trace navigation, token counting, cost dashboards, and session replay for debugging complex agent behaviors.

Frequently Asked Questions

What are the OpenTelemetry GenAI Semantic Conventions?

The OpenTelemetry GenAI Semantic Conventions are a standardized set of attribute names and span structures specifically designed for generative AI applications. They use the gen_ai.* namespace to standardize how agent names, operation types, model identifiers, and request parameters are recorded across different LLM providers. This standardization ensures that traces from different vendors can be visualized consistently in any compatible observability platform.

How does ExternalContentStore optimize span size?

ExternalContentStore addresses OpenTelemetry's span size limitations by storing full prompt and response content outside the trace payload. When capture_inline=False, the tracer writes large text payloads to an external store and includes only reference IDs in the span attributes. This approach prevents trace rejection due to payload limits while maintaining accessibility to complete conversation histories through the observability UI.

Can I switch from Langfuse to another backend without code changes?

Yes. Because the implementation emits standard OTLP spans using the GenAI Semantic Conventions, you can migrate to Arize Phoenix or Comet Opik by simply reconfiguring the OTLP collector endpoint. No changes to your tracer initialization or span creation code are required, as all three backends understand the gen_ai.* attribute schema and hierarchical span structure.

What dependencies are required for the tracer?

The tracer implementation in phases/14-agent-engineering/23-otel-genai-conventions/code/main.py requires only Python's standard library. It uses built-in modules such as json, time, and socket for span creation and export, eliminating the need for external OpenTelemetry SDKs or vendor-specific agents in your application code. The OTLP collector runs as a separate sidecar process (Docker container or binary) and handles all backend-specific protocol requirements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →