NAME

Punk::Plugin::OpenTelemetry - OpenTelemetry for a Punk application

SYNOPSIS

package MyApp;
use Punk;
use Punk::Plugin::OpenTelemetry;

otel service_name => 'checkout',
     endpoint     => 'http://collector:4318';

plugin 'OpenTelemetry';

get '/orders/:id' => sub {
    my ($c) = @_;
    $c->otel;                     # the tracer
    $c->json({ ok => 1 });
};

Or entirely from the environment, with no code at all:

OTEL_SERVICE_NAME=checkout \
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \
plackup -s Hyperman app.psgi

DESCRIPTION

Registering this plugin turns on server, client and database spans, the metrics the HTTP conventions ask for, and log records correlated by trace id. The instrumentation goes through C ABI observer tables in Punk, Fetch and DBIx::Loop, so an instrumented request pays no Perl frame for being instrumented, and an unsampled one allocates nothing at all.

CONFIGURATION

Three sources, in this order:

otel keyword  >  punk.yml otel: block  >  OTEL_* environment  >  default

The specification defines three configuration interfaces - programmatic, environment variable and declarative file - and says programmatic configuration is the foundation the others should be built on. It states no precedence between programmatic and environment configuration, and gives exactly one precedence rule: a declarative config file takes precedence over the SDK configuration environment variables. The order above matches the spec where it speaks and follows its stated principle where it does not. It is also Punk's own convention, which layers punk.yml under what the app class declared.

The punk.yml otel: block is not the spec's declarative configuration format, and the two should not be conflated. Supporting OTEL_EXPERIMENTAL_CONFIG_FILE is separate work; were it added, that file would take precedence over the OTEL_* variables as the spec requires, and would sit between the punk.yml block and the environment.

# punk.yml
otel:
  service_name: checkout
  endpoint: http://collector:4318
  sampler: traceidratio
  sampler_arg: 0.05

With no endpoint, nothing is exported

There is no default endpoint. Set none and the SDK builds, instruments the request path, records spans and then has nowhere to send them - so they are dropped. It does not fall back to http://localhost:4318.

This is worth stating plainly because other SDKs do default to that address, and because the failure is silent: the application works, the boot line says enabled, and no telemetry ever arrives. If you have configured everything else and are seeing nothing, check the endpoint first.

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318      # a local collector

The environment

The OTEL_* variables are read by "from_env" in Punk::OpenTelemetry::Config, once, at boot. Re-reading %ENV per request would be both slower and wrong: a worker that picked up a mid-flight change would disagree with its siblings, and telemetry that disagrees about its own configuration is worse than telemetry that is uniformly stale.

Turning it off

OTEL_SDK_DISABLED=true is checked before anything is built and before a single hook is registered. Nothing is allocated, the request path is not wrapped, and the process is not paying for telemetry it is not sending.

The value is the spec's boolean, not Perl truth: only the string true, case-insensitively, disables the SDK. OTEL_SDK_DISABLED=false does not, and it would under any looser rule.

Credentials

OTEL_EXPORTER_OTLP_HEADERS carries the token the exporter authenticates with. Header values are never printed in the boot diagnostic, never written to a span attribute and never included in a self-diagnostic. The diagnostic prints the header count, because "did my credentials arrive" is a real question and a number answers it without answering anything else.

THE BOOT DIAGNOSTIC

One line at info, stating whether it is enabled, the service name, the protocol, the endpoint, the sampler and its argument, and the propagators. Almost every OpenTelemetry support question is answered by those six facts, and almost no SDK prints them.

OpenTelemetry enabled service=checkout protocol=http/protobuf
endpoint=http://collector:4318 sampler=traceidratio:0.05
propagators=tracecontext,baggage

THE FORK TRAP

The resource is built at boot, in the parent, and every attribute on it is inherited by every worker - which is right for all of them except one. service.instance.id must differ per worker, so each takes a fresh one from Hyperman's on_worker_start.

This is the single most common way a home-grown metrics layer is broken, and it is invisible: a collector receiving several workers' cumulative series under one identity does not report a conflict, it resolves it, and the numbers come out wrong by a factor of however many workers are running.

KEYWORDS

otel %opt

Records configuration. Declaring it more than once merges, so a base class can set the service name and a subclass add the endpoint. It may be called before or after plugin 'OpenTelemetry', since the keyword is installed by use.

Called with no arguments it returns the tracer - but only once the application has been built, because that is when the configuration is resolved and the tracer constructed. Before to_app it returns undef. In a route handler it is always there; at application-body scope it is not, and $c->otel is the accessor to reach for anyway.

HELPERS

$c->otel

The tracer.

$c->otel_meter

The meter, when the metrics signal is on.

SEE ALSO

Punk::OpenTelemetry, Punk::OpenTelemetry::Config, Punk::OpenTelemetry::Instrument.

AUTHOR

LNATION <email@lnation.org>

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)