Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions content/integrations/frameworks/ag2.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
---
title: Observability for AG2 with Langfuse Integration
sidebarTitle: AG2
logo: /images/integrations/ag2_icon.svg
description: Integrate Langfuse with AG2 via native OpenTelemetry tracing for full observability into multi-agent conversations, LLM calls, tool executions, and costs.
category: Integrations
---

# Integrate Langfuse with AG2

This guide shows how to integrate **Langfuse** with **AG2** using AG2's built-in OpenTelemetry tracing for full observability into your multi-agent workflows.

> **What is AG2?** [AG2](https://ag2.ai/) ([GitHub](https://github.com/ag2ai/ag2)) is an open-source Python framework for building multi-agent AI systems. AG2 provides tools for orchestrating collaborative agents, tool use, group chats, and distributed agent-to-agent (A2A) deployments. AG2 v0.11+ includes native [OpenTelemetry tracing](https://docs.ag2.ai/docs/user-guide/tracing/opentelemetry) that captures every conversation, agent turn, LLM call, tool execution, and speaker selection as structured spans.

> **What is Langfuse?** [Langfuse](https://langfuse.com) is an open-source LLM engineering platform. It offers tracing and monitoring capabilities for AI applications. Langfuse helps developers debug, analyze, and optimize their AI systems by providing detailed insights and integrating with a wide array of tools and frameworks through native integrations, OpenTelemetry, and dedicated SDKs.

<Callout type="info">
AG2 is the community-driven continuation of AutoGen. If you are using
Microsoft AutoGen instead, see the [AutoGen
integration](/integrations/frameworks/autogen), which traces via OpenLIT
rather than AG2's native OpenTelemetry support.
</Callout>

## Get started

AG2's native OpenTelemetry tracing exports rich, hierarchical spans that Langfuse ingests directly — no intermediary libraries needed.

<Steps>
### Step 1: Install dependencies

```bash
pip install "ag2[openai,tracing]" opentelemetry-exporter-otlp langfuse -U
```

### Step 2: Configure Langfuse SDK

Set up your Langfuse API keys. You can get these keys by signing up for a free [Langfuse Cloud](https://cloud.langfuse.com/) account or by [self-hosting Langfuse](https://langfuse.com/self-hosting).

```python
import os

# Get keys for your project from the project settings page: https://cloud.langfuse.com
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
os.environ["LANGFUSE_BASE_URL"] = "https://cloud.langfuse.com" # 🇪🇺 EU region
# os.environ["LANGFUSE_BASE_URL"] = "https://us.cloud.langfuse.com" # 🇺🇸 US region

# Your OpenAI key
os.environ["OPENAI_API_KEY"] = "sk-proj-..."
```

Initialize the Langfuse client and verify the connection:

```python
from langfuse import get_client

langfuse = get_client()

if langfuse.auth_check():
print("Langfuse client is authenticated and ready!")
```

### Step 3: Configure OpenTelemetry to export to Langfuse

Set up an OpenTelemetry `TracerProvider` that exports spans directly to Langfuse's OTel endpoint:

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

LANGFUSE_PUBLIC_KEY = os.environ["LANGFUSE_PUBLIC_KEY"]
LANGFUSE_SECRET_KEY = os.environ["LANGFUSE_SECRET_KEY"]
LANGFUSE_BASE_URL = os.environ["LANGFUSE_BASE_URL"]

auth = base64.b64encode(
f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode()
).decode()

resource = Resource.create({"service.name": "ag2-langfuse-demo"})
tracer_provider = TracerProvider(resource=resource)

exporter = OTLPSpanExporter(
endpoint=f"{LANGFUSE_BASE_URL}/api/public/otel/v1/traces",
headers={"Authorization": f"Basic {auth}"},
)
tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)
```

### Step 4: Instrument agents and run

Create AG2 agents and instrument them with AG2's built-in tracing:

```python
from autogen import ConversableAgent, LLMConfig
from autogen.opentelemetry import instrument_agent, instrument_llm_wrapper

llm_config = LLMConfig({"model": "gpt-4o-mini"})

assistant = ConversableAgent(
name="assistant",
system_message="You are a helpful assistant.",
llm_config=llm_config,
human_input_mode="NEVER",
)

user_proxy = ConversableAgent(
name="user_proxy",
human_input_mode="NEVER",
max_consecutive_auto_reply=1,
)

# Instrument agents and LLM calls
instrument_llm_wrapper(tracer_provider=tracer_provider)
instrument_agent(assistant, tracer_provider=tracer_provider)
instrument_agent(user_proxy, tracer_provider=tracer_provider)

# Run a chat — traces are sent to Langfuse automatically
result = user_proxy.run(
assistant,
message="What is the capital of France?",
max_turns=2,
)
result.process()

# Flush spans before exit
tracer_provider.shutdown()
```

### Step 5: View traces in Langfuse

After executing the application, navigate to your Langfuse Trace Table. You will see hierarchical traces showing the full conversation flow — each agent turn, LLM call (with model, tokens, and cost), and tool execution nested in a tree that mirrors your agent workflow.

AG2's tracing emits 7 span types that map to Langfuse observations:

| AG2 span type | What it captures |
| ------------------- | ------------------------------------------------- |
| `conversation` | Overall chat with total token usage and cost |
| `agent` | Individual agent turn with input/output messages |
| `llm` | LLM API call with model, tokens, cost, parameters |
| `tool` | Tool execution with arguments and return value |
| `code_execution` | Code execution with output |
| `human_input` | Human input prompt and response |
| `speaker_selection` | Group chat speaker selection with candidates |

</Steps>

## Tool use example

AG2 traces tool executions with full argument and return value capture. This example reuses the `tracer_provider` configured in Step 3 above:

```python
from typing import Annotated
from autogen import ConversableAgent, LLMConfig
from autogen.opentelemetry import instrument_agent, instrument_llm_wrapper
from autogen.tools import tool

@tool(description="Get weather information for a city")
def get_weather(city: Annotated[str, "The city name"]) -> str:
"""Get weather information for a city."""
weather_data = {
"new york": "Sunny, 72F",
"london": "Cloudy, 15C",
"tokyo": "Rainy, 18C",
}
return weather_data.get(city.lower(), f"Weather data not available for {city}")

llm_config = LLMConfig({"model": "gpt-4o-mini"})

weather_agent = ConversableAgent(
name="weather",
system_message="Use the get_weather tool to answer weather questions.",
functions=[get_weather],
llm_config=llm_config,
human_input_mode="NEVER",
)

instrument_llm_wrapper(tracer_provider=tracer_provider)
instrument_agent(weather_agent, tracer_provider=tracer_provider)

result = weather_agent.run(message="What is the weather in Tokyo?", max_turns=2)
result.process()
```

In Langfuse, `execute_tool get_weather` spans appear nested under the agent span, with tool arguments and return values visible in the observation input/output.

## Group chat example

For group chats, use `instrument_pattern` to instrument all agents in a single call (again reusing the `tracer_provider` from Step 3):

```python
from autogen import ConversableAgent, LLMConfig
from autogen.agentchat import run_group_chat
from autogen.agentchat.group.patterns import AutoPattern
from autogen.opentelemetry import instrument_llm_wrapper, instrument_pattern

llm_config = LLMConfig({"model": "gpt-4o-mini"})

researcher = ConversableAgent(
name="researcher",
system_message="You research topics and provide factual information.",
llm_config=llm_config,
human_input_mode="NEVER",
)

writer = ConversableAgent(
name="writer",
system_message="You write clear summaries. Say TERMINATE when done.",
llm_config=llm_config,
human_input_mode="NEVER",
)

user = ConversableAgent(name="user", human_input_mode="NEVER", llm_config=False)

pattern = AutoPattern(
initial_agent=researcher,
agents=[researcher, writer],
user_agent=user,
group_manager_args={"llm_config": llm_config},
)

instrument_llm_wrapper(tracer_provider=tracer_provider)
instrument_pattern(pattern, tracer_provider=tracer_provider)

result = run_group_chat(
pattern=pattern,
messages="Explain quantum computing in simple terms.",
max_rounds=5,
)
result.process()
```

This produces hierarchical traces in Langfuse including `speaker_selection` spans that show which agent was chosen and why.

## Distributed tracing with A2A

AG2 supports the [A2A (Agent-to-Agent) protocol](https://a2a-protocol.org/) for distributed multi-service deployments. When agents run as separate services, AG2 propagates W3C Trace Context headers across HTTP calls, and Langfuse stitches the spans into a unified trace.

```python
from autogen import ConversableAgent, LLMConfig
from autogen.a2a import A2aAgentServer
from autogen.opentelemetry import instrument_a2a_server

agent = ConversableAgent(
name="assistant",
llm_config=LLMConfig({"model": "gpt-4o-mini"}),
)

server = A2aAgentServer(agent, url="http://localhost:18123/")
instrument_a2a_server(server, tracer_provider=tracer_provider)
```

## Environment variables (alternative setup)

Instead of configuring the exporter in code, you can use environment variables:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n 'pk-lf-...:sk-lf-...' | base64)"
export OTEL_SERVICE_NAME="ag2-app"
```

Check warning on line 266 in content/integrations/frameworks/ag2.mdx

View check run for this annotation

Claude / Claude Code Review

base64 without -w0 corrupts Authorization header on Linux with real keys

Two new bash snippets pipe the `pk:sk` pair through `base64` without `-w0`: `ag2.mdx:264` (`OTEL_EXPORTER_OTLP_HEADERS`) and `agentgateway.mdx:63` (`LANGFUSE_AUTH`). GNU coreutils `base64` wraps at 76 chars, and real Langfuse keys (`pk-lf-<UUID>:sk-lf-<UUID>`, ~85 chars → ~116 chars encoded) cross that threshold — the embedded newline survives `$(...)` (which only strips *trailing* newlines) and corrupts the Authorization header, causing silent 401s on Linux. Suggest adopting the pattern already
Comment on lines +262 to +266

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Two new bash snippets pipe the pk:sk pair through base64 without -w0: ag2.mdx:264 (OTEL_EXPORTER_OTLP_HEADERS) and agentgateway.mdx:63 (LANGFUSE_AUTH). GNU coreutils base64 wraps at 76 chars, and real Langfuse keys (pk-lf-<UUID>:sk-lf-<UUID>, ~85 chars → ~116 chars encoded) cross that threshold — the embedded newline survives $(...) (which only strips trailing newlines) and corrupts the Authorization header, causing silent 401s on Linux. Suggest adopting the pattern already documented in content/integrations/frameworks/spring-ai.mdx:213-214 (base64 -w0 on Linux with a macOS-BSD fallback comment).

Extended reasoning...

What is wrong

This PR adds two new bash snippets that base64-encode a Langfuse public_key:secret_key pair without disabling GNU base64's default line-wrapping:

  • content/integrations/frameworks/ag2.mdx:264 — sets OTEL_EXPORTER_OTLP_HEADERS inline
  • content/integrations/gateways/agentgateway.mdx:63 — sets LANGFUSE_AUTH for later use in a YAML manifest

Both use the form $(echo -n "…" | base64) with no -w0.

Step-by-step proof (Linux, real Langfuse cloud keys)

  1. Cloud keys are UUID-suffixed: pk-lf-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (42 chars) and sk-lf-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (42 chars).
  2. Joined with :85 characters of input.
  3. Base64 of 85 bytes → ceil(85/3)*4 = 116 characters of output.
  4. GNU coreutils base64 defaults to --wrap=76, so it inserts a literal LF after column 76.
  5. Bash $(...) command substitution strips only trailing newlines from the captured stdout — an LF at position 77 stays inside the string.
  6. For agentgateway.mdx: LANGFUSE_AUTH now contains <76 chars>\n<40 chars>, which is pasted into the Collector YAML as Authorization: "Basic <broken>" → OTLP server sees a malformed credential → 401.
  7. For ag2.mdx: OTEL_EXPORTER_OTLP_HEADERS gets an embedded LF. HTTP header values cannot contain raw LF; the OTLP HTTP client either rejects the header or transmits a truncated one → 401.
  8. The placeholder pk-lf-...:sk-lf-... shown in the docs is only 19 chars (28 encoded), so testing with the literal placeholder does NOT trigger the wrap — the bug is invisible until a reader plugs in real keys.

Why existing code does not prevent it

There is no runtime guard — the docs are copy-pasted verbatim by readers. The failure mode is silent (auth error, no hint that the credential is malformed) and specific to Linux (BSD base64 on macOS does not wrap), which is the majority CI/dev environment.

The repo already documents the fix

content/integrations/frameworks/spring-ai.mdx:213-214 spells out the correct cross-platform pattern:

export AUTH_STRING=$(echo -n "pk-lf-...:sk-lf-..." | base64 -w0) # Linux
# macOS (BSD base64 has no -w0): export AUTH_STRING=$(echo -n "pk-lf-...:sk-lf-..." | base64)

The explicit # Linux / # macOS (BSD base64 has no -w0) comments are a direct acknowledgment that the maintainers have already been bitten by this. github-copilot.mdx and other/openclaw.mdx use the same pattern.

Suggested fix

Adopt the spring-ai pattern in both new files: append -w0 to the base64 call and add a one-line macOS fallback comment. This is a one-character diff per file.

Severity note

Marking as nit because several pre-existing docs (amazon-agentcore.mdx, pipecat.mdx, vscode.mdx, existing-otel-setup.mdx, and agentgateway.mdx's own troubleshooting row at line 425) use the same non-wrapping-safe pattern, so this PR extends a pre-existing inconsistency rather than introducing a new one. It is still worth fixing on the way in — the correct pattern is one directory over in spring-ai.mdx, and following the wrong pattern here causes a real silent-auth failure for readers who paste the snippet with real UUID keys.


## Learn more

- [AG2 OpenTelemetry documentation](https://docs.ag2.ai/docs/user-guide/tracing/opentelemetry)
- [AG2 GitHub](https://github.com/ag2ai/ag2)
- [Langfuse OpenTelemetry integration](/integrations/native/opentelemetry)
- [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/)

import LearnMore from "@/components-mdx/integration-learn-more.mdx";

<LearnMore />
120 changes: 120 additions & 0 deletions content/integrations/frameworks/genkit.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Trace Genkit with Langfuse via OpenTelemetry
sidebarTitle: Genkit
logo: /images/integrations/genkit_icon.svg
description: Learn how to send Genkit traces to Langfuse from Go applications using OpenTelemetry.
category: Integrations
---

# Trace Genkit with Langfuse

Langfuse can ingest Genkit traces via OpenTelemetry. Once your Genkit application exports OTLP spans, Langfuse maps Genkit spans into traces and observations with model input, output, and usage data when available.

> **What is Genkit?** [Genkit](https://genkit.dev/) is an open-source framework for building AI features and agentic workflows with structured flows, tool calling, and model integrations.

> **What is Langfuse?** [Langfuse](https://langfuse.com/) is an open-source LLM engineering platform for observability, prompt management, and evals.

<Callout type="info" emoji="ℹ️">
This Langfuse integration is OpenTelemetry-based, not Go-specific. If your Genkit setup already exports OTLP traces, you can point that exporter at Langfuse. For Genkit Go, the common setup is the [Genkit OpenTelemetry plugin](https://github.com/genkit-ai/opentelemetry-go-plugin?tab=readme-ov-file#langfuse).
</Callout>

<Steps>
## Step 1: Install the Genkit OpenTelemetry plugin

```bash
go get github.com/xavidop/genkit-opentelemetry-go
```

The package is documented in the [`genkit-ai/opentelemetry-go-plugin` repository](https://github.com/genkit-ai/opentelemetry-go-plugin).

Check failure on line 28 in content/integrations/frameworks/genkit.mdx

View check run for this annotation

Claude / Claude Code Review

Genkit plugin module path contradicts documented repository

The Callout (line 18) and prose (line 28) both link to `github.com/genkit-ai/opentelemetry-go-plugin` as the documented plugin, but the `go get` command (line 25) and Go import (line 54) use `github.com/xavidop/genkit-opentelemetry-go` — a different owner and repository name. A reader clicking the linked repo will land on a package whose install/API instructions don't match the code snippets. Please reconcile by either updating the code to use the linked module, or updating both prose links to p
Comment on lines +22 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The Callout (line 18) and prose (line 28) both link to github.com/genkit-ai/opentelemetry-go-plugin as the documented plugin, but the go get command (line 25) and Go import (line 54) use github.com/xavidop/genkit-opentelemetry-go — a different owner and repository name. A reader clicking the linked repo will land on a package whose install/API instructions don't match the code snippets. Please reconcile by either updating the code to use the linked module, or updating both prose links to point at github.com/xavidop/genkit-opentelemetry-go.

Extended reasoning...

The bug. content/integrations/frameworks/genkit.mdx contains an internal inconsistency between the documentation prose and the code snippets. Two prose references point readers at one Go module while the code snippets install and import a completely different Go module:

Why this is a real problem. In Go, the module path IS the package identity — github.com/genkit-ai/opentelemetry-go-plugin and github.com/xavidop/genkit-opentelemetry-go are two distinct packages with different owners and different repository names (not just an org rename — the repo name itself changes from opentelemetry-go-plugin to genkit-opentelemetry-go). They cannot both refer to the same package.

Impact on the reader. This is the very first step of the integration (Step 1: Install the Genkit OpenTelemetry plugin). A reader who follows the prose link lands on genkit-ai/opentelemetry-go-plugin's README and gets install/config instructions that reference a different import path than the code snippets in this same guide. A reader who follows the code snippet installs xavidop/genkit-opentelemetry-go, then clicks the doc link and finds themselves reading docs for a package they never installed. Either path is broken.

Step-by-step proof.

  1. Reader opens the Genkit integration page.
  2. Reader reads the Callout on line 18 and clicks "Genkit OpenTelemetry plugin" → lands on https://github.com/genkit-ai/opentelemetry-go-plugin.
  3. Reader returns to the guide and runs go get github.com/xavidop/genkit-opentelemetry-go from line 25 — this pulls a different module (different owner, different repo).
  4. Reader clicks the prose link on line 28 ("documented in the genkit-ai/opentelemetry-go-plugin repository") → lands again on genkit-ai/opentelemetry-go-plugin, whose README describes a different import path and possibly a different API surface than the xavidop/... module the reader just installed.
  5. Reader copies the Go code from Step 3 (line 54: opentelemetry "github.com/xavidop/genkit-opentelemetry-go") — the import matches what was installed, but the reader has no docs for it because both prose links point elsewhere.

Why this wasn't caught. The PR description itself flags Genkit content as "as written by the original authors — worth a quick spot-check," indicating the maintainer already suspected this area needed verification.

Fix. Pick one authoritative source and align both sides. If xavidop/genkit-opentelemetry-go is the correct package (matching what the code imports), update the Callout on line 18 and the prose on line 28 to link there instead. If genkit-ai/opentelemetry-go-plugin is the correct package (matching the prose links), update the go get on line 25 and the import alias on line 54 to that module path.


## Step 2: Configure Langfuse credentials

```bash
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU region
# export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 US region

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
```

## Step 3: Initialize the OpenTelemetry plugin

Configure the plugin to send OTLP/HTTP traces to your Langfuse instance. Langfuse currently accepts OTLP over HTTP, so set `OTLPUseHTTP: true`. The example below disables metrics export because this setup is only sending traces to Langfuse.

```go
package main

import (
"context"
"encoding/base64"
"log"
"os"

"github.com/firebase/genkit/go/genkit"
opentelemetry "github.com/xavidop/genkit-opentelemetry-go"
)

func main() {
baseURL := os.Getenv("LANGFUSE_BASE_URL")
if baseURL == "" {
baseURL = "https://cloud.langfuse.com"
}

publicKey := os.Getenv("LANGFUSE_PUBLIC_KEY")
secretKey := os.Getenv("LANGFUSE_SECRET_KEY")
if publicKey == "" || secretKey == "" {
log.Fatal("LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set")
}

auth := base64.StdEncoding.EncodeToString([]byte(publicKey + ":" + secretKey))

plugin := opentelemetry.New(opentelemetry.Config{
ServiceName: "my-genkit-app",
ForceExport: true,
DisableMetricsExporter: true,
OTLPEndpoint: baseURL + "/api/public/otel/v1/traces",
OTLPUseHTTP: true,
OTLPHeaders: map[string]string{
"Authorization": "Basic " + auth,
"X-Langfuse-Ingestion-Version": "4",
},
})

genkit.Init(
context.Background(),
genkit.WithPlugins(plugin),
)
}
```

## Step 4: Run your Genkit flows

After initializing the plugin, Genkit flows and model calls emit OpenTelemetry spans. Langfuse ingests those spans and displays them as traces with nested observations, including inputs, outputs, tool calls, and token usage when Genkit provides that metadata.

If you already use an OpenTelemetry Collector, you can route Genkit spans through the Collector and export them to Langfuse from there. For endpoint and authentication details, see the [Langfuse OpenTelemetry guide](/integrations/native/opentelemetry).

## Step 5: View traces in Langfuse

Open **Traces** in Langfuse to inspect flow executions, model calls, latency, cost, and errors. If traces do not show up in local development, double-check `ForceExport: true`, the OTLP endpoint, and the Basic Auth credentials.

![Genkit trace in Langfuse](/images/cookbook/integration-genkit/genkit-example-trace.png)

</Steps>

## Troubleshooting

- **No traces appear in Langfuse**
- The Genkit OpenTelemetry plugin is initialized before your flows run.
- `OTLPUseHTTP` is set to `true`.
- The OTLP endpoint points to `https://<your-langfuse-host>/api/public/otel/v1/traces`.
- The `Authorization` header contains the Base64-encoded `public_key:secret_key` pair.
- If the `X-Langfuse-Ingestion-Version: 4` header is not set, traces may still ingest successfully but can take up to 10 minutes to appear in Langfuse.
- `ForceExport` is enabled in short-lived local processes so spans are flushed before the program exits.

## Next steps

Once traces are flowing, you can use Langfuse to debug prompt inputs and outputs, add scores, and build dashboards on top of your Genkit workflows.

import NextSteps from "@/components-mdx/get-started/next-steps.mdx";

<NextSteps />
Loading
Loading