Langfuse

Langfuse ingests OpenTelemetry traces and has SDKs for Python and JavaScript; for every other language it points at its OTLP endpoint. An agent built on this SDK needs nothing Langfuse-specific to use that endpoint — its spans already carry the conventions Langfuse maps — only configuration, which Langfuse::from_env() reads from the three variables Langfuse's own SDKs read and Telemetry::builder().with_langfuse(..) applies. A Rust agent is configured exactly like the Python agents next to it.

The preset defaults to a Langfuse you run yourself, at http://localhost:3000. That is deliberate: Langfuse's own SDKs default to its cloud service, and an agent configured from this page sends nothing off the machine unless LANGFUSE_BASE_URL names somewhere else.

Run Langfuse

Langfuse is open source and its repository ships a docker-compose.yml. From a clone of langfuse/langfuse, these variables make it create an organisation, a project with known keys and a user on first start; TELEMETRY_ENABLED=false turns off Langfuse's own usage reporting:

cat > langfuse.env <<'ENV'
TELEMETRY_ENABLED=false
LANGFUSE_INIT_ORG_ID=local
LANGFUSE_INIT_PROJECT_ID=local
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-local
LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-local
[email protected]
LANGFUSE_INIT_USER_PASSWORD=change-me-please
ENV
docker compose --env-file langfuse.env up -d

The UI is then at http://localhost:3000. Before running it anywhere but a laptop, follow Langfuse's self-hosting documentation (https://langfuse.com/self-hosting): the compose file's own comments mark the secrets to change.

Setup

a2a-protocol-sdk   = "0.15"   # `otlp`, which Langfuse needs, is a default feature
tokio              = { version = "1", features = ["full"] }
tracing-subscriber = "0.3"
export LANGFUSE_PUBLIC_KEY=pk-lf-local     # the project's keys
export LANGFUSE_SECRET_KEY=sk-lf-local
export LANGFUSE_BASE_URL=http://localhost:3000   # the default; set it for any other instance
use a2a_protocol_sdk::server::otel::{Langfuse, Telemetry};
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let telemetry = Telemetry::builder()
    .with_default_service_name("my-agent")
    .with_langfuse(Langfuse::from_env()?)
    .build()?;
tracing_subscriber::registry().with(telemetry.layer()).init();

// ... build the handler, serve ...

telemetry.shutdown()?;
Ok(())
}

examples/langfuse-agent is a runnable version: an orchestrator agent that delegates to a worker agent, both traced into one Langfuse trace. From a checkout of this repository, with the Langfuse above running:

LANGFUSE_PUBLIC_KEY=pk-lf-local LANGFUSE_SECRET_KEY=sk-lf-local \
cargo run -p langfuse-agent -- "summarise the quarterly report"

Without LANGFUSE_PUBLIC_KEY it exports to whatever OTEL_EXPORTER_OTLP_* names.

What the preset sets

Every item below was read from Langfuse rather than assumed: the endpoint and transports from its OpenTelemetry integration page, the rest from the langfuse/langfuse source at 1a21a42 (2026-10-06).

  • Traces go to {LANGFUSE_BASE_URL}/api/public/otel/v1/traces over OTLP/HTTP with protobuf bodies — Langfuse's documentation says "gRPC is not supported yet" — with Authorization: Basic base64(public:secret) and x-langfuse-ingestion-version: 4, the header Langfuse's documentation says keeps directly ingested spans from being delayed.
  • Metrics and logs are off. Langfuse accepts OTLP metrics and discards them (web/src/pages/api/public/otel/v1/metrics/index.ts is a handler that does nothing), and has no OTLP logs route. To keep them, set OTEL_METRICS_EXPORTER=otlp / OTEL_LOGS_EXPORTER=otlp and point OTEL_EXPORTER_OTLP_ENDPOINT at a collector: traces still go to Langfuse, the other two to the collector.
  • The preset's endpoint is set in code, so for traces it wins over OTEL_EXPORTER_OTLP_ENDPOINT. Its two headers are added to those in OTEL_EXPORTER_OTLP_HEADERS; a header of the same name in the variable (say Authorization) replaces the preset's, so leave those out.

What you see

Measured against a self-hosted Langfuse 4.53.0 (docker compose up from the langfuse/langfuse repository), with the SDK's own spans and nothing added:

SpanLangfuse showsBecause
invoke_agent {name} (the executor)an AGENT observationgen_ai.operation.name = invoke_agent (ObservationTypeMapper.ts, priority 3)
lf.a2a.v1.A2AService/SendMessage, server and clienta SPANno GenAI operation; the A2A attributes land under metadata.attributes
every span of a contextone session, the A2A contextIdgen_ai.conversation.id (extractSessionId)
gen_ai.input.messages, gen_ai.output.messagesthe observation's input and outputthe same JSON-string shape as Langfuse's own OpenTelemetry GenAI fixture

The last row needs with_span_content_capture(true) on the handler builder — it copies what users and agents say into Langfuse, so it is off by default.

A delegation chain is one trace. Measured: a caller's root span, the call to an orchestrator agent, the orchestrator's server span and invoke_agent, its call to a worker agent, the worker's server span and invoke_agent — seven observations, each the child of the one before, in a single trace, with no propagation code in either agent: the client sends its own span as the traceparent (see Observability).

What was not verified

  • Langfuse Cloud. Only the self-hosted 4.53.0 above; the preset's cloud URL and region names come from Langfuse's documentation.
  • A chain that crosses languages. traceparent is W3C Trace Context, so a Python or JavaScript agent traced into the same Langfuse project should join the same trace; that has not been run. Note that Langfuse's Python SDK v4 exports only spans it recognises as LLM-related by default (should_export_span), which filters what that process sends, not what a Rust agent sends.
  • Langfuse's UI. The table above is what Langfuse's public API returned (/api/public/v2/observations), not screenshots of its interface.

Langfuse Cloud

The same preset works with Langfuse's hosted service: set LANGFUSE_BASE_URL to the region's address (https://cloud.langfuse.com, https://us.cloud.langfuse.com) and use that project's keys. Nothing else changes. As noted above, this SDK has not been run against it.

When you already run a collector

Leave the preset off, point OTEL_EXPORTER_OTLP_ENDPOINT at the collector, and let the collector forward traces to Langfuse with its otlphttp exporter and the same two headers. Metrics and logs then go wherever the collector sends them.