NAME
Langertha::Knarr::Tracing - Automatic Langfuse tracing per proxy request
VERSION
version 1.101
SYNOPSIS
use Langertha::Knarr::Tracing;
my $tracing = Langertha::Knarr::Tracing->new(config => $config);
my $trace_id = $tracing->start_trace(
model => 'gpt-5.6-terra',
engine => 'Langertha::Engine::OpenAI',
messages => \@messages,
params => \%params,
format => 'openai',
);
# ... handle request ...
$tracing->end_trace($trace_id,
output => $response_text,
model => 'gpt-5.6-terra',
usage => { input => 100, output => 50, total => 150 },
);
DESCRIPTION
Records every proxy request as a Langfuse trace with a nested generation. When tracing is not configured (no public and secret key), all methods are no-ops.
Langfuse credentials are read from the config file's langfuse: section or from the LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, and LANGFUSE_URL environment variables. The module strips surrounding quotes from environment variable values, which Docker --env-file sometimes adds literally.
Timing sources
Knarr has two request paths and they do not measure latency the same way. The generation's startTime always marks the moment "start_trace" ran; what differs is where endTime and completionStartTime come from.
Routed, non-streaming — a Langertha engine produced a Langertha::Response, so Langertha::Knarr::Handler::Tracing hands the engine-measured
timinghash to "end_trace".endTimebecomesstartTime + total_secondsandcompletionStartTimebecomesstartTime + ttft_seconds, both anchored to the high-resolution timestamp "start_trace" recorded. This is the only path with a real time-to-first-token, and the durations exclude the proxy's own formatting overhead.Routed, streaming — the decorator accumulates deltas and never sees a response object, so there is no
timing.endTimeis the wall-clock moment the stream was exhausted and nocompletionStartTimeis emitted.Raw passthrough — bytes are piped 1:1 and never parsed, so no Langertha::Response exists at all.
endTimeis again the proxy's own wall clock at "end_trace", which includes network time to the upstream provider.
Callers that pass no timing therefore keep exactly the previous behaviour: proxy-measured endTime, no completionStartTime.
config
The Langertha::Knarr::Config object. Required. Provides Langfuse credentials and trace_name.
trace_name
The Langfuse trace name applied to all traces. Resolved in priority order from: langfuse.trace_name in config, LANGFUSE_TRACE_NAME env var, KNARR_TRACE_NAME env var, or the default knarr-proxy.
start_trace
my $trace_info = $tracing->start_trace(
model => $model_name,
engine => $engine_class,
messages => \@messages,
params => \%params,
format => 'openai',
);
Creates a new Langfuse trace and generation. Returns a $trace_info hashref that must be passed to "end_trace". Returns undef when tracing is disabled.
The returned hashref carries start_hires, the gettimeofday pair behind start_time. "end_trace" anchors engine-measured durations to it; see "Timing sources".
end_trace
$tracing->end_trace($trace_info,
output => $response_text,
model => $model,
usage => { input => 100, output => 50, total => 150 },
timing => { ttft_seconds => 0.25, total_seconds => 1.5 },
response_id => 'chatcmpl-123',
);
# On error:
$tracing->end_trace($trace_info, error => "Something went wrong");
Closes the generation and trace started by "start_trace", then flushes the batch to Langfuse. Pass error to record a failed generation at level ERROR. Does nothing when $trace_info is undef (tracing was disabled at start).
Optional metadata carried off a Langertha::Knarr::Response, all skipped when absent:
timing— HashRef withttft_seconds/total_seconds. Drives the generation'sendTimeandcompletionStartTime(the Langfuse field for time-to-first-token) and is recorded verbatim in the metadata, so provider-native stage durations survive too. See "Timing sources".response_id— the provider's own response id, for correlating a Langfuse generation with the provider's logs.thinking— reasoning text the engine split offcontent. It is model output thatoutputno longer contains, so the trace is the only place it survives.rate_limit— a Langertha::RateLimit (or equivalent hashref). Flattened to its quota scalars; the raw header hash is not recorded.usage— a Langertha::Usage (the shape every routed response carries) or a plain hashref. Objects are flattened withto_hashtoinput_tokens/output_tokens/total_tokens; hashrefs are recorded verbatim.
flush
$tracing->flush;
Sends all pending trace events to the Langfuse ingestion API as a batch and clears the internal buffer. Called automatically by "end_trace". Does nothing when tracing is disabled or the batch is empty.
Never throws: a batch that cannot be JSON-encoded is logged at error level and dropped, the same way an ingestion HTTP failure is. "end_trace" runs on the request's response path, so a tracing problem must not take the client's response down with it.
SEE ALSO
Langertha::Knarr — Tracing is wired in automatically for all routes
Langertha::Knarr::Config — Provides Langfuse credentials
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-knarr/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 <torsten@raudssus.de> https://raudssus.de/
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.