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 timing hash to "end_trace". endTime becomes startTime + total_seconds and completionStartTime becomes startTime + 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. endTime is the wall-clock moment the stream was exhausted and no completionStartTime is emitted.

  • Raw passthrough — bytes are piped 1:1 and never parsed, so no Langertha::Response exists at all. endTime is 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 with ttft_seconds / total_seconds. Drives the generation's endTime and completionStartTime (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 off content. It is model output that output no 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 with to_hash to input_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

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.