NAME

Langertha::Role::Langfuse - Langfuse observability integration

VERSION

version 0.503

SYNOPSIS

Langfuse is built into every Langertha engine. Just set the env vars:

export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_URL=http://localhost:3000   # optional, defaults to cloud

Then use any engine as normal — simple_chat is auto-traced:

use Langertha::Engine::OpenAI;

my $engine = Langertha::Engine::OpenAI->new(
    api_key => $ENV{OPENAI_API_KEY},
    model   => 'gpt-4o-mini',
);

my $response = $engine->simple_chat('Hello!');
$engine->langfuse_flush;  # send events to Langfuse

# inside an event loop, without blocking it:
await $engine->langfuse_flush_f;

Or pass keys explicitly:

my $engine = Langertha::Engine::Anthropic->new(
    api_key             => $ENV{ANTHROPIC_API_KEY},
    langfuse_public_key => 'pk-lf-...',
    langfuse_secret_key => 'sk-lf-...',
    langfuse_url        => 'http://localhost:3000',
);

Manual traces for custom workflows:

my $trace_id = $engine->langfuse_trace(
    name  => 'my-workflow',
    input => { query => 'custom input' },
);

$engine->langfuse_generation(
    trace_id => $trace_id,
    name     => 'step-1',
    model    => 'gpt-4o',
    input    => 'prompt text',
    output   => 'response text',
    usage    => { input => 10, output => 5, total => 15 },
);

$engine->langfuse_flush;

DESCRIPTION

This role integrates Langertha engines with Langfuse, an open-source observability platform for LLM applications. It is composed into Langertha::Role::Chat, so every engine has Langfuse support built in.

Features:

  • Zero-config via environment variables

  • Auto-instrumentation of simple_chat calls

  • Manual trace and generation event creation

  • Batched event ingestion via Langfuse REST API

  • Basic Auth using public/secret key pair

  • Disabled by default — only active when both keys are set

Langfuse concepts:

  • Trace — Top-level unit of work (a request, a conversation turn)

  • Span — A grouping of work within a trace (an iteration, a tool call)

  • Generation — A single LLM call within a trace (with model, usage, timing)

Hierarchy: Traces contain spans and generations. Spans can nest via parent_observation_id. All observations can be updated after creation.

langfuse_public_key

Your Langfuse project public key. Auto-populated from LANGFUSE_PUBLIC_KEY environment variable if not passed.

langfuse_secret_key

Your Langfuse project secret key. Auto-populated from LANGFUSE_SECRET_KEY environment variable if not passed.

langfuse_url

Langfuse API URL. Defaults to LANGFUSE_URL env var, or https://cloud.langfuse.com if not set. Set this to your self-hosted instance URL (e.g. http://localhost:3000).

langfuse_enabled

Bool indicating whether Langfuse integration is active. Lazy — defaults to true when both public and secret keys are available (from constructor or environment variables).

langfuse_max_batch

The most events kept in memory between two "langfuse_flush" calls. Default 1000 (500 traced simple_chat calls, each a trace and a generation). The events are only sent when someone flushes, and Langfuse turns on by itself as soon as LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are in the environment, so a long-running process that never flushes would otherwise keep every prompt and answer it ever sent. When the batch is full the oldest event is dropped for each new one, with a single warning per engine object. 0 removes the cap.

Nothing is flushed automatically: a flush is an HTTP request, and simple_chat should not pay for one at an unpredictable moment. Call "langfuse_flush" (or "langfuse_flush_f") yourself, for example after each request in a server.

langfuse_timestamp

my $t0 = $engine->langfuse_timestamp;   # 2026-09-25T12:34:56.789Z
...
$engine->langfuse_span(
  trace_id   => $trace_id,
  name       => 'tool: search',
  start_time => $t0,
  end_time   => $engine->langfuse_timestamp,
);

Returns the current time as an ISO-8601 UTC string with millisecond precision (YYYY-MM-DDTHH:MM:SS.mmmZ) — the format this role stamps on every Langfuse event. Use it for start_time / end_time when you create spans or generations yourself. The older private name _langfuse_timestamp still works and returns the same.

langfuse_trace

my $trace_id = $engine->langfuse_trace(
    name        => 'my-trace',
    input       => { ... },
    output      => '...',
    metadata    => { ... },
    tags        => ['tag1', 'tag2'],
    user_id     => 'user-123',
    session_id  => 'session-abc',
    release     => '1.0.0',
    version     => '1',
    public      => 1,
    environment => 'production',
);

Creates a trace event. Returns the trace ID for linking generations and spans. Accepts optional tags, user_id, session_id, release, version, public, and environment fields. Calling with the same id upserts (updates) the trace.

langfuse_generation

$engine->langfuse_generation(
    trace_id              => $trace_id,
    name                  => 'chat',
    model                 => 'gpt-4o',
    input                 => '...',
    output                => '...',
    usage                 => { input => 10, output => 5, total => 15 },
    start_time            => $iso_timestamp,
    end_time              => $iso_timestamp,
    parent_observation_id => $span_id,
    model_parameters      => { temperature => 0.7, max_tokens => 1000 },
    level                 => 'DEFAULT',
    status_message        => 'OK',
    version               => '1',
);

Creates a generation event linked to a trace. trace_id is required. Accepts optional parent_observation_id for nesting under a span, model_parameters, level (DEBUG/DEFAULT/WARNING/ERROR), status_message, and version.

langfuse_span

my $span_id = $engine->langfuse_span(
    trace_id              => $trace_id,
    name                  => 'my-span',
    input                 => { ... },
    output                => '...',
    start_time            => $iso_timestamp,
    end_time              => $iso_timestamp,
    parent_observation_id => $parent_span_id,
    metadata              => { ... },
    level                 => 'DEFAULT',
    status_message        => 'OK',
    version               => '1',
);

Creates a span event for grouping work within a trace. trace_id is required. Returns the span ID. Spans can be nested via parent_observation_id.

langfuse_update_trace

$engine->langfuse_update_trace(
    id       => $trace_id,
    output   => 'final result',
    metadata => { ... },
);

Updates a trace by upserting with the same id. Uses trace-create event type (Langfuse upserts on matching body ID). id is required.

langfuse_update_span

$engine->langfuse_update_span(
    id       => $span_id,
    end_time => $iso_timestamp,
    output   => { ... },
);

Updates an existing span. id is required. Use this to set end_time and output after the span's work completes.

langfuse_update_generation

$engine->langfuse_update_generation(
    id     => $gen_id,
    output => 'final response text',
    usage  => { input => 100, output => 50, total => 150 },
);

Updates an existing generation. id is required. Use this to add output, usage, and end_time after the LLM call completes.

langfuse_timeout

Seconds a flush may wait for Langfuse per request. Default 10, deliberately short: Langfuse is observability, and an ingestion endpoint that accepts the connection and never answers must not hold up the application (LWP's own default would be 180 seconds). The engine's "user_agent_timeout" in Langertha::Role::HTTP does not apply here; it is meant for the LLM provider. On the Net::Async::HTTP backend it is the total time of the request, on the synchronous LWP path the time without activity on the connection.

langfuse_flush_batch_size

The most events sent in one ingestion request. Default 100. A flush with more events sends several requests one after another, so a large backlog does not become one body that Langfuse rejects for its size.

langfuse_flush

$engine->langfuse_flush;

Sends all batched events to the Langfuse ingestion API over a dedicated LWP::UserAgent with "langfuse_timeout", and clears the batch. Blocks until the requests are done, so do not call it from inside an event loop; use "langfuse_flush_f" there. More than "langfuse_flush_batch_size" events go out as several requests. Returns the HTTP::Response of the last request, or nothing when there was nothing to send.

It never dies. It warns when a request fails (the events of that request are lost, and after a timeout or refused connection the rest of the flush is dropped too, instead of waiting out the timeout once per request), and when Langfuse accepts the request but rejects single events (207 Multi-Status with an errors list): the warning gives the number rejected and the first error.

langfuse_flush_f

await $engine->langfuse_flush_f;

Async "langfuse_flush": sends the batched events through the engine's own async backend (Langertha::Role::AsyncHTTP) with "langfuse_timeout" as the request's total timeout, so a slow or silent Langfuse never blocks the event loop. The batch is taken when the call starts; events recorded while it runs wait for the next flush. The future resolves to the HTTP::Response of each request and never fails; problems are warned about as in "langfuse_flush". On the synchronous fallback (no Net::Async::HTTP) it runs like "langfuse_flush".

ENVIRONMENT VARIABLES

LANGFUSE_PUBLIC_KEY — Auto-populates langfuse_public_key
LANGFUSE_SECRET_KEY — Auto-populates langfuse_secret_key
LANGFUSE_URL — Auto-populates langfuse_url (default: https://cloud.langfuse.com)

With both keys in the environment every engine records simple_chat calls without being asked to, but sends nothing until "langfuse_flush" is called. Events wait in memory up to "langfuse_max_batch"; past that the oldest are dropped with one warning. A process that has the variables set but never flushes therefore holds a bounded amount of trace data, not every prompt it ever sent.

SELF-HOSTING LANGFUSE

A ready-to-use Kubernetes manifest is included in the distribution:

kubectl apply -f ex/langfuse-k8s.yaml
kubectl -n langfuse port-forward svc/langfuse-web 3000:3000 &

export LANGFUSE_PUBLIC_KEY=pk-lf-langertha
export LANGFUSE_SECRET_KEY=sk-lf-langertha
export LANGFUSE_URL=http://localhost:3000

The manifest pre-creates a project with known API keys so you can send data immediately without going through the web UI.

Dashboard: http://localhost:3000 (login: langertha@test.invalid / langertha)

GETTING LANGFUSE KEYS

For Langfuse Cloud, sign up at https://langfuse.com/ and generate API keys in your project settings.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.