NAME
Langertha::Plugin::Langfuse - Langfuse observability plugin for any PluginHost
VERSION
version 0.503
SYNOPSIS
use Langertha::Chat;
use Langertha::Plugin::Langfuse;
my $langfuse = Langertha::Plugin::Langfuse->new(
public_key => 'pk-lf-...',
secret_key => 'sk-lf-...',
);
my $chat = Langertha::Chat->new(
engine => $engine,
plugins => [$langfuse],
);
$chat->simple_chat('Hello!');
$langfuse->flush;
Or with sugar:
my $chat = Langertha::Chat->new(
engine => $engine,
plugins => [Langfuse => {
trace_name => 'my-chat',
auto_flush => 1,
}],
);
Environment variables LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_URL are auto-populated when not explicitly set.
DESCRIPTION
This plugin integrates any Langertha::Role::PluginHost (Langertha::Chat, Langertha::Embedder, Langertha::Raider) with Langfuse observability. It hooks into the standard plugin events to automatically create traces, generations, and spans.
Unlike Langertha::Role::Langfuse (which lives on the engine), this plugin works on any PluginHost and does not require engine-level configuration.
Each LLM call becomes a generation carrying the model that answered (the engine's chat_model when the answer names none), the token usage (input, output, total; plus inputCost / outputCost / totalCost when "pricing" is set), the conversation sent, the answer (its text, and its tool calls as Langertha::ToolCall hashes), and completionStartTime when the response reports a time to first token. The conversation is a JSON-safe snapshot taken before the call: a Langertha::Content::Image appears as its compact description, and inline image data (a data: URL or a long bare base64 string) is replaced by its size, so traces never carry image bytes.
Embeddings (Langertha::Embedder) and image generations (Langertha::ImageGen) become generations too. Called through simple_embedding_result / simple_image_result (or their _f), they also carry the model that answered, the token usage (with cost when "pricing" has a rule for that model) and the call's total_seconds in the generation's metadata, all from the Langertha::CallResult the host passes to the after-hook. The bare simple_embedding / simple_image record only name, input and timestamps.
public_key
Langfuse project public key. Defaults to LANGFUSE_PUBLIC_KEY env var.
secret_key
Langfuse project secret key. Defaults to LANGFUSE_SECRET_KEY env var.
url
Langfuse API URL. Defaults to LANGFUSE_URL env var or https://cloud.langfuse.com.
enabled
Whether Langfuse integration is active. Defaults to true when both public_key and secret_key are non-empty.
trace_name
Name for the Langfuse trace created per chat session. Defaults to 'llm-call'.
user_id
Optional user ID passed to the Langfuse trace.
session_id
Optional session ID passed to the Langfuse trace.
tags
Optional tags passed to the Langfuse trace.
metadata
Optional metadata HashRef merged into the Langfuse trace.
auto_flush
When true, the batch is sent after each plugin_after_llm_response, plugin_after_image_gen and plugin_after_embedding. Defaults to false.
The hook does not wait for Langfuse. With the Net::Async::HTTP backend the flush runs in the background on the host engine's event loop, bounded by "flush_timeout", and the call returns at once; a slow or unreachable Langfuse costs the chat nothing. The request only makes progress while that loop runs, so a synchronous program should call "flush" before it exits: that waits for flushes still in flight and sends what is left. Without Net::Async::HTTP (or without an engine on the host) everything is synchronous anyway and the hook sends right away, again bounded by "flush_timeout".
flush_timeout
Seconds a flush may wait for Langfuse per request. Default 10: an ingestion endpoint that accepts the connection and never answers must not hold up the application. The engine's user_agent_timeout does not apply to flushes. On the Net::Async::HTTP backend it is the total time of the request, on the LWP path the time without activity on the connection.
flush_batch_size
The most events sent in one ingestion request. Default 100; a larger batch goes out as several requests, one after another.
max_batch
The most events kept in memory between two flushes. Default 1000. When the batch is full the oldest event is dropped for each new one, with a single warning per plugin object; 0 removes the cap. Without "auto_flush" the events are only sent by "flush", so this bounds what a process that never flushes holds.
pricing
Optional Langertha::Pricing. When set and it has a rule for the model that answered, each LLM generation's usage also carries inputCost, outputCost and totalCost (USD) computed from the reported tokens.
create_trace
my $trace_id = $plugin->create_trace(name => 'my-trace', input => {...});
Creates a trace event. Returns the trace ID. Called automatically by the plugin hooks, but can also be used manually.
create_generation
$plugin->create_generation(trace_id => $id, model => 'gpt-4o', ...);
Creates a generation event linked to a trace.
create_span
$plugin->create_span(trace_id => $id, name => 'tool-call', ...);
Creates a span event within a trace.
update_trace
$plugin->update_trace(id => $trace_id, output => 'result');
Updates a trace by upserting with the same ID.
flush
$plugin->flush;
Sends all batched events to the Langfuse ingestion API over a dedicated LWP::UserAgent with "flush_timeout", and clears the batch. Blocks until done; first it waits for any "auto_flush" request still in flight. Do not call it from inside an event loop; use "flush_f" there. More than "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 (its events are lost, and after a timeout or refused connection the rest of the flush is dropped too) and when Langfuse answers 207 Multi-Status with per-event errors (the number rejected and the first error).
flush_f
await $plugin->flush_f;
Async "flush": waits for "auto_flush" requests still in flight, then sends the batch through the host engine's async backend (Langertha::Role::AsyncHTTP) with "flush_timeout" as each request's total timeout, so a slow or silent Langfuse never blocks the event loop. Resolves to the HTTP::Response of each request and never fails; problems are warned about as in "flush". Without an engine on the host, or on the synchronous fallback, it runs like "flush".
reset_trace
$plugin->reset_trace;
Resets the current trace state. Call this between independent chat sessions to start a new trace.
SEE ALSO
Langertha::Plugin - Base class with all hook method signatures
Langertha::Role::PluginHost - Plugin system consumed by hosts
Langertha::Chat - Chat host this plugin attaches to
Langertha::Embedder - Embedder host this plugin attaches to
Langertha::ImageGen - Image generation host this plugin attaches to
Langertha::Raider - Autonomous agent host this plugin attaches to
https://langfuse.com/ - Langfuse observability platform
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.