NAME

Punk::OpenTelemetry::Config - the OTEL_* environment surface and precedence

SYNOPSIS

my $env = Punk::OpenTelemetry::Config::from_env();

my $cfg = Punk::OpenTelemetry::Config::resolve(
    \%keyword, $punk_yml_block, $env);      # first wins

return if Punk::OpenTelemetry::Config::disabled($cfg);

$log->info(Punk::OpenTelemetry::Config::diagnostic($cfg));

DESCRIPTION

Nobody configures a telemetry SDK by calling its constructor. They set environment variables, because that is what their deployment tool sets, and because the OTEL_* names are the one part of OpenTelemetry that is the same in every language. An SDK that reads only its own options is one every operator has to learn separately, which is exactly what the spec's environment surface exists to prevent.

So: roughly thirty variables, parsed here, once, into a plain hash - and then merged under whatever the application declared.

PRECEDENCE

keyword  >  punk.yml  >  environment  >  default

The spec defines three configuration interfaces (programmatic, environment variable, declarative file) and says programmatic configuration is the foundation the others should be built on. It states no precedence between programmatic and environment configuration. The one precedence rule it gives concerns the declarative config file: when OTEL_EXPERIMENTAL_CONFIG_FILE is set, that file takes precedence over the SDK configuration environment variables.

So this order matches the spec where it speaks and follows its stated principle where it does not. It is also Punk's own convention.

punk.yml's otel: block is not the spec's declarative configuration format, and the two should not be conflated.

READ ONCE

The environment is read at boot, not per request. Re-reading %ENV in the request path 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.

TWO THINGS THAT MUST NOT GO WRONG

OTEL_SDK_DISABLED

It has to make the whole thing inert. It is the switch an operator reaches for at three in the morning, so it is checked before anything is built and before a single hook is registered.

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

OTEL_EXPORTER_OTLP_HEADERS

It carries credentials. Every value is percent-decoded (the spec carries these in the W3C Baggage encoding, so a token containing a comma or an equals sign arrives encoded, and one that survives the split but not the decode authenticates against nothing) - and then never printed. diagnostic emits the header count, never a value, not even truncated: a token with its first eight characters shown is a token in the log.

WHAT IS PARSED

Every name in full, so that looking one up finds it.

Identity

OTEL_SERVICE_NAME                     unknown_service, and a warning
OTEL_RESOURCE_ATTRIBUTES              -

An unnamed service is warned about at boot rather than left to be discovered on a dashboard, where it is indistinguishable from every other unnamed service.

Propagation and sampling

OTEL_PROPAGATORS                      tracecontext,baggage
OTEL_TRACES_SAMPLER                   parentbased_always_on
OTEL_TRACES_SAMPLER_ARG               -

Both propagators by default, because a deployment that drops baggage loses it silently.

Transport

OTEL_EXPORTER_OTLP_ENDPOINT           -   NOTHING IS EXPORTED WITHOUT ONE
OTEL_EXPORTER_OTLP_PROTOCOL           http/protobuf
OTEL_EXPORTER_OTLP_COMPRESSION        none
OTEL_EXPORTER_OTLP_TIMEOUT            10000     ms
OTEL_EXPORTER_OTLP_HEADERS            -

and the per-signal forms, each overriding the general one for that signal alone:

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT       _METRICS_        _LOGS_
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL       _METRICS_        _LOGS_
OTEL_EXPORTER_OTLP_TRACES_COMPRESSION    _METRICS_        _LOGS_
OTEL_EXPORTER_OTLP_TRACES_HEADERS        _METRICS_        _LOGS_

There is no default endpoint, and this SDK does not fall back to http://localhost:4318 the way some others do. With none set the SDK still builds and still records, and the spans are dropped for want of anywhere to go. It is the first thing to check when everything looks configured and nothing arrives.

The general endpoint has the signal path appended; a per-signal endpoint is used exactly as given. That asymmetry is the spec's, and it is the most common thing to get wrong - see "Endpoints" in Punk::OpenTelemetry::Exporter.

Batching

OTEL_BSP_SCHEDULE_DELAY               5000      ms
OTEL_BSP_EXPORT_TIMEOUT              30000      ms
OTEL_BSP_MAX_QUEUE_SIZE               2048
OTEL_BSP_MAX_EXPORT_BATCH_SIZE         512

OTEL_BLRP_SCHEDULE_DELAY              1000      ms
OTEL_BLRP_EXPORT_TIMEOUT             30000      ms
OTEL_BLRP_MAX_QUEUE_SIZE              2048
OTEL_BLRP_MAX_EXPORT_BATCH_SIZE        512

BSP is spans, BLRP is log records. The schedule delays differ on purpose - 5s and 1s, the spec's numbers - because a log somebody is watching for is worth sending sooner than a span they will look at afterwards.

Metrics

OTEL_METRIC_EXPORT_INTERVAL          60000      ms
OTEL_METRIC_EXPORT_TIMEOUT           30000      ms
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE   cumulative
OTEL_METRICS_EXEMPLAR_FILTER         trace_based

Limits

OTEL_ATTRIBUTE_COUNT_LIMIT             128
OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT        -      unlimited
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT        128
OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT   -      unlimited
OTEL_SPAN_EVENT_COUNT_LIMIT            128
OTEL_SPAN_LINK_COUNT_LIMIT             128
OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT       128
OTEL_LINK_ATTRIBUTE_COUNT_LIMIT        128

A length limit of 0 means unlimited, which is why the two length limits are absent by default rather than set to something enormous.

The switch

OTEL_SDK_DISABLED                     unset

See "OTEL_SDK_DISABLED" above for why only the string true counts.

OTEL_EXPERIMENTAL_CONFIG_FILE is not read. Supporting the spec's declarative configuration format is separate work; see "PRECEDENCE" for where it would sit if it were added.

Two rules for every value above

A value that is meant to be a number and is not is ignored rather than taken as zero. OTEL_BSP_MAX_QUEUE_SIZE=lots must not silently become a queue of nothing, which is indistinguishable from a broken exporter.

An empty value is absent, not empty. OTEL_SERVICE_NAME= in a compose file means the operator did not set it, and honouring it as a service named "" produces telemetry nothing can find again.

FUNCTIONS

from_env

The whole environment surface, as a hashref.

resolve(@layers)

Merges layers, first wins, so they are written in the order they take precedence. Undef layers are skipped, so a caller need not test each source.

Merging is one level deep for a hash value, because the sub-hashes here are namespaces rather than values: an app that sets one per-signal endpoint means that one, not "and forget what the environment said about the other two". An array replaces wholesale, because a propagator list is a single decision and merging two gives an order nobody chose.

disabled($config)

Whether the SDK is off. Separate from "from_env" because it is asked before anything is built, and the answer must not depend on any of it having been built.

diagnostic($config)

The boot line: enabled or disabled, service, protocol, endpoint, sampler and its argument, propagators, and the header count. Almost every OpenTelemetry support question is answered by those facts, and almost no SDK prints them.

SEE ALSO

Punk::Plugin::OpenTelemetry, which is what reads this.

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)