NAME
Punk::Observe::Decode - OTLP protobuf, read as records
SYNOPSIS
use Punk::Observe::Decode;
my $out = Punk::Observe::Decode::decode($bytes, 'traces');
for my $rec (@{ $out->{records} }) {
printf "%s %s %s\n", $rec->{t}, $rec->{duration}, $rec->{body};
}
my $wire = Punk::Observe::Decode::pb_fields($bytes);
printf "field %d, wire type %d\n", $_->{field}, $_->{wire}
for @{ $wire->{fields} };
DESCRIPTION
The reader that turns an OTLP export request into records. Two levels are exposed: the generic protobuf walk, which knows the wire format and nothing about OpenTelemetry, and the OTLP decoder above it, which knows what the field numbers mean.
Field numbers come from Punk::OpenTelemetry's otel_proto.h by dependency rather than by being copied. Two sources of truth for a field number is the one thing that must not happen here: a client writing field 9 and a server reading field 9 as something else produces plausible wrong data, silently, with no error anywhere.
Nothing here is a transport. To receive OTLP over HTTP, see Punk::Observe::Ingest.
FUNCTIONS
pb_fields
my $out = Punk::Observe::Decode::pb_fields($bytes);
Walks one protobuf message and returns a hashref describing its top-level fields, with no schema applied. Useful for looking at a payload that will not decode, where the question is what actually arrived.
{
fields => [ { field => 1, wire => 2, bytes => "..." }, ... ],
err => 0,
errstr => 'ok',
}
Every entry carries field (the field number) and wire (the wire type: 0 varint, 1 fixed64, 2 length-delimited, 5 fixed32). Depending on the wire type one more key is present:
0 varint the value, and int32 as the sign-extended 32-bit reading
1 fixed64 the raw bits, and double as the same eight bytes as a float
2 bytes the payload
5 fixed32 the value
The walk stops at the first malformed field, so fields is the prefix that parsed. err is zero on a clean read, and errstr is one of ok, truncated, varint too long, unsupported wire type or field number 0.
A 64-bit value crosses as a number where that is lossless and as a decimal string where it is not. See "TIMESTAMPS" in Punk::Observe.
decode
my $out = Punk::Observe::Decode::decode($bytes, $signal);
Decodes an OTLP export request into records. $signal is traces, metrics or logs, and naming anything else is fatal.
{
ok => 1,
records => [ ... ],
dropped_bad_trace => 0,
clamped_durations => 0,
}
ok is false when the payload did not parse, in which case records is empty. An empty message is not a failure: proto3 says every field of an empty message is absent, so a zero-length body decodes to zero records and ok is true.
dropped_bad_trace counts spans refused for an all-zero trace identifier, and clamped_durations counts spans whose end preceded their start. Both are reported rather than hidden, because either one arriving steadily means the instrumentation upstream is wrong.
Each record is a hashref:
kind 1 metric, 2 log, 3 span
t event time, unix nanoseconds
duration nanoseconds, 0 where the signal has none
severity OTLP's 24-point scale, 0 where absent
flags record flags
span_kind OTLP span kind
status OTLP status code
trace_hi the trace id, high 8 bytes
trace_lo the trace id, low 8 bytes
span_id the span id
parent_id the parent span id, 0 for a span with none
value a metric value, as a number or an integer
value_is_int whether value came off the wire as an integer
body the log body, empty string where absent
attrs a hashref of the flattened attributes
attr_order the attribute keys in canonical order
attrs and attr_order carry the same keys. The order matters and is not the hash's: it is the canonical sort that the content-derived series id is built from, which is what lets separate workers agree on an id without coordinating.