NAME

Punk::OpenTelemetry::Instrument - where the spans come from

SYNOPSIS

my $tracer = Punk::OpenTelemetry::Tracer->new(...);
Punk::OpenTelemetry::Instrument::install($tracer);

# later, if the database spans are drowning you
Punk::OpenTelemetry::Instrument::configure(db => 0);

DESCRIPTION

Attaches the SDK to the hooks the ecosystem grew for it in phase 1: Punk's pk_abi request, response and query observers, and Fetch's outbound request observer.

Both ABIs are optional and resolved lazily. A process with neither loaded still gets a working tracer - manual spans, encoders, exporter - and simply has nothing automatic to observe. That is the difference between a telemetry layer and a dependency: this one attaches to what is there.

TWO RULES

The sampling decision comes first

Attributes are built only after a span exists. An unsampled request must not pay to assemble strings nobody will read, which is why "start" in Punk::OpenTelemetry::Tracer returns undef rather than a null object and why every callback here begins by checking for one.

http.route is the pattern, or it is absent

Never the path. For a 404, a 405 or a mounted app, pk_abi's route_pattern_of returns nothing and the attribute is simply not set.

Falling back to url.path there is the exact substitution that turns a bounded dimension into an unbounded one - and 404 traffic is precisely where a scanner will hand you a million distinct values. An API operation has no route pattern either, and uses its operationId, which is bounded the same way.

WHAT IS INSTRUMENTED

  • Server spans, from the pk_abi request and response observers. The request side runs before routing, so the span is named for the method alone and upgraded to "GET /users/:id" on the response side, once the pattern exists. Naming it after the path at request time would be the cardinality mistake, permanently.

  • Client spans, from Fetch's observer - which also injects the traceparent. Without that injection the far side starts a new trace and the two halves of a call are never joined, which is the whole reason Fetch grew the hook.

  • Database spans, from pk_abi's on_query (the shipped Punk::Model::DBI backend). The statement text is the prepared one; the phase-1 observers do not pass bind values at all, so the literal data cannot leak even by accident.

STATUS DIFFERS BY SPAN KIND

A 4xx does not fail a SERVER span: the server worked and said no. Marking those as errors makes the error rate a measure of how many people mistyped a URL, which is a graph nobody can act on. Only 5xx is an error.

On a CLIENT span the rule inverts, deliberately - a 4xx the client received is a failure of the call this process made.

THE RECURSION GUARD

The exporter sends spans over HTTP with Fetch. Fetch is instrumented. So an export would produce a client span, which is queued, and exported, and...

The first collector outage would become an infinite loop of telemetry about failing to send telemetry. Everything the SDK does on its own behalf runs inside suppress_begin / suppress_end, which nest, and every instrumentation point checks the flag.

FUNCTIONS

install($tracer, %opt)

Wire everything up. Returns a hashref saying which points went live - server, client, db_punk - so a caller can tell the difference between "instrumented" and "nothing to instrument". Idempotent: the underlying registrations are process-global and permanent, so this is meant to be called once, at boot, in the worker that will serve.

%opt takes server, client and db, each true by default.

configure(%opt)

Turn a point on or off afterwards. The registrations stay; the callbacks return immediately. An application drowning in database spans can silence those without losing its server spans. enabled is the master switch.

config

The current state of all four switches.

suppress_begin / suppress_end / suppressed

The recursion guard, above.

method / server_status / client_status / db_operation / schema_url

The semantic conventions, exposed because they are pure functions over strings and are where the cardinality rules live - so they are asserted directly rather than inferred from a span that happens to look right.

method bounds an unknown HTTP method to _OTHER; the raw value is kept in http.request.method_original, where it cannot become a metric dimension.

schema_url is the semantic convention version this distribution emits, pinned in C as OTEL_SCHEMA_URL so it cannot drift from what the encoder puts on the wire. It is the single source of that answer: the file Punk::OpenTelemetry::Schema ships is the one this names, and Punk::OpenTelemetry::Schema->shipped_version is derived from it.

SEE ALSO

Punk::OpenTelemetry::Schema - what a schema URL means on the far end, and converting telemetry between two of them.

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)