Skip to main content

rustlavel_otel/
protobuf.rs

1//! Just enough protobuf to write OTLP.
2//!
3//! Rule one applies: protobuf is a serialisation format against a schema this
4//! crate already knows at compile time, so it is written here rather than
5//! pulled in with a code generator and a runtime library. Only the *encoding*
6//! half exists — nothing in OTLP export ever has to read a protobuf message,
7//! because the collector's reply is a status code and, at worst, a partial
8//! success body nobody needs to decode field by field.
9//!
10//! The failure this module is guarding against is silent. A collector that
11//! cannot parse a payload answers 400 and drops it; a collector that parses a
12//! *wrong* field number accepts the payload and quietly loses whatever was in
13//! it. Byte-level tests are the only way to know which one happened, so the
14//! tests below check exact sequences taken from the protobuf encoding
15//! specification rather than round-tripping through this same code.
16
17/// Wire types, as protobuf numbers them. The tag byte of a field is its number
18/// shifted left three, with the wire type in the low bits.
19const VARINT: u32 = 0;
20const FIXED64: u32 = 1;
21const FIXED32: u32 = 5;
22const LENGTH_DELIMITED: u32 = 2;
23
24/// A protobuf message under construction.
25///
26/// Sub-messages are built as their own `Encoder` and then handed to
27/// [`Encoder::message`], because a length-delimited field has to know its
28/// length before it writes its body and the only honest way to know that is to
29/// have encoded the body already.
30#[derive(Debug, Default, Clone)]
31pub struct Encoder {
32    bytes: Vec<u8>,
33}
34
35impl Encoder {
36    pub fn new() -> Self {
37        Encoder::default()
38    }
39
40    pub fn into_bytes(self) -> Vec<u8> {
41        self.bytes
42    }
43
44    pub fn as_bytes(&self) -> &[u8] {
45        &self.bytes
46    }
47
48    pub fn len(&self) -> usize {
49        self.bytes.len()
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.bytes.is_empty()
54    }
55
56    /// A field key: the field number and the wire type, as one varint.
57    fn tag(&mut self, field: u32, wire: u32) {
58        self.raw_varint(u64::from(field) << 3 | u64::from(wire));
59    }
60
61    /// Base 128, little-endian, high bit set on every byte but the last.
62    fn raw_varint(&mut self, mut value: u64) {
63        while value >= 0x80 {
64            self.bytes.push((value as u8) | 0x80);
65            value >>= 7;
66        }
67        self.bytes.push(value as u8);
68    }
69
70    /// A `uint32`/`uint64` field. Zero is proto3's default for these, and a
71    /// field left out decodes back to the default, so writing it would only
72    /// make the payload bigger.
73    pub fn uint64(&mut self, field: u32, value: u64) {
74        if value != 0 {
75            self.tag(field, VARINT);
76            self.raw_varint(value);
77        }
78    }
79
80    pub fn uint32(&mut self, field: u32, value: u32) {
81        self.uint64(field, u64::from(value));
82    }
83
84    /// An `int64` field. Negative values sign-extend to a full 64 bits, which
85    /// is why protobuf spends ten bytes on `-1` — `sint64` is the zig-zag
86    /// alternative, and OTLP does not use it anywhere this crate encodes.
87    pub fn int64(&mut self, field: u32, value: i64) {
88        if value != 0 {
89            self.tag(field, VARINT);
90            self.raw_varint(value as u64);
91        }
92    }
93
94    pub fn bool(&mut self, field: u32, value: bool) {
95        if value {
96            self.bool_present(field, value);
97        }
98    }
99
100    /// Fields inside a `oneof` have explicit presence, so their default value
101    /// still has to be written.
102    ///
103    /// This is not a micro-optimisation in reverse — it is the difference
104    /// between an attribute arriving as `false` and arriving as nothing at all.
105    /// A decoder tracks which arm of a `oneof` is set by which tag it saw, so
106    /// omitting `bool_value = false` sets no arm and the collector prints
107    /// `Empty()`. OTLP's `AnyValue` and `NumberDataPoint.value` are both
108    /// `oneof`s, and a real collector is where this was caught.
109    pub fn bool_present(&mut self, field: u32, value: bool) {
110        self.tag(field, VARINT);
111        self.raw_varint(u64::from(value));
112    }
113
114    /// See [`Encoder::bool_present`] — `AnyValue.int_value`.
115    pub fn int64_present(&mut self, field: u32, value: i64) {
116        self.tag(field, VARINT);
117        self.raw_varint(value as u64);
118    }
119
120    /// See [`Encoder::bool_present`] — `AnyValue.string_value`.
121    pub fn string_present(&mut self, field: u32, value: &str) {
122        self.tag(field, LENGTH_DELIMITED);
123        self.raw_varint(value.len() as u64);
124        self.bytes.extend_from_slice(value.as_bytes());
125    }
126
127    /// See [`Encoder::bool_present`] — `NumberDataPoint.as_int`.
128    pub fn sfixed64_present(&mut self, field: u32, value: i64) {
129        self.tag(field, FIXED64);
130        self.bytes.extend_from_slice(&value.to_le_bytes());
131    }
132
133    /// An enum field. Enums travel as varints, and every OTLP enum reserves
134    /// zero for "unspecified", so omitting zero is right here too.
135    pub fn enumeration(&mut self, field: u32, value: i32) {
136        if value != 0 {
137            self.tag(field, VARINT);
138            self.raw_varint(value as u32 as u64);
139        }
140    }
141
142    /// A `fixed64` field: eight little-endian bytes, no varint compression.
143    /// OTLP uses it for timestamps, where the values are large enough that a
144    /// varint would cost more than eight bytes anyway.
145    pub fn fixed64(&mut self, field: u32, value: u64) {
146        if value != 0 {
147            self.tag(field, FIXED64);
148            self.bytes.extend_from_slice(&value.to_le_bytes());
149        }
150    }
151
152    /// A `fixed32` field: four little-endian bytes. `LogRecord.flags` is the one
153    /// user here, carrying the W3C trace flags.
154    pub fn fixed32(&mut self, field: u32, value: u32) {
155        if value != 0 {
156            self.tag(field, FIXED32);
157            self.bytes.extend_from_slice(&value.to_le_bytes());
158        }
159    }
160
161    /// A `sfixed64` field — `NumberDataPoint.as_int`, and nothing else here.
162    pub fn sfixed64(&mut self, field: u32, value: i64) {
163        if value != 0 {
164            self.tag(field, FIXED64);
165            self.bytes.extend_from_slice(&value.to_le_bytes());
166        }
167    }
168
169    /// A `double` field, IEEE 754 little-endian.
170    pub fn double(&mut self, field: u32, value: f64) {
171        if value != 0.0 {
172            self.present_double(field, value);
173        }
174    }
175
176    /// A `double` field that is written even when it holds the default.
177    ///
178    /// Proto3 `optional double` has explicit presence, and OTLP uses it for
179    /// `HistogramDataPoint.sum`, `min` and `max`. There, absent means "this
180    /// histogram does not report a sum" while zero means "the sum is zero" —
181    /// two different facts that the default-skipping [`Encoder::double`] would
182    /// collapse into one.
183    pub fn present_double(&mut self, field: u32, value: f64) {
184        self.tag(field, FIXED64);
185        self.bytes.extend_from_slice(&value.to_le_bytes());
186    }
187
188    /// A `string` field: length-delimited UTF-8.
189    pub fn string(&mut self, field: u32, value: &str) {
190        if !value.is_empty() {
191            self.bytes_field(field, value.as_bytes());
192        }
193    }
194
195    /// A `bytes` field. Named to leave `bytes` free for the buffer itself.
196    pub fn bytes_field(&mut self, field: u32, value: &[u8]) {
197        if value.is_empty() {
198            return;
199        }
200        self.tag(field, LENGTH_DELIMITED);
201        self.raw_varint(value.len() as u64);
202        self.bytes.extend_from_slice(value);
203    }
204
205    /// A nested message.
206    ///
207    /// Written even when the sub-message encoded to nothing: message fields
208    /// have explicit presence in proto3, so an empty `Status` still says "this
209    /// span carries a status" rather than "no status was set". Callers that
210    /// mean the second one simply do not call this.
211    pub fn message(&mut self, field: u32, message: &Encoder) {
212        self.tag(field, LENGTH_DELIMITED);
213        self.raw_varint(message.bytes.len() as u64);
214        self.bytes.extend_from_slice(&message.bytes);
215    }
216
217    /// A packed `repeated fixed64` — `HistogramDataPoint.bucket_counts`.
218    ///
219    /// Packed means one length-delimited field holding every element back to
220    /// back, rather than one tag per element. Proto3 packs repeated scalars by
221    /// default, so a collector expects exactly this.
222    pub fn packed_fixed64(&mut self, field: u32, values: &[u64]) {
223        if values.is_empty() {
224            return;
225        }
226        self.tag(field, LENGTH_DELIMITED);
227        self.raw_varint((values.len() * 8) as u64);
228        for value in values {
229            self.bytes.extend_from_slice(&value.to_le_bytes());
230        }
231    }
232
233    /// A packed `repeated double` — `HistogramDataPoint.explicit_bounds`.
234    pub fn packed_double(&mut self, field: u32, values: &[f64]) {
235        if values.is_empty() {
236            return;
237        }
238        self.tag(field, LENGTH_DELIMITED);
239        self.raw_varint((values.len() * 8) as u64);
240        for value in values {
241            self.bytes.extend_from_slice(&value.to_le_bytes());
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn varint_of(value: u64) -> Vec<u8> {
251        let mut encoder = Encoder::new();
252        encoder.raw_varint(value);
253        encoder.into_bytes()
254    }
255
256    /// The worked example in the protobuf encoding specification: 300 is
257    /// `1010 1100 0000 0010`, seven bits at a time, low group first, with the
258    /// continuation bit set on every byte except the last.
259    #[test]
260    fn varints_match_the_specifications_worked_example() {
261        assert_eq!(varint_of(0), [0x00]);
262        assert_eq!(varint_of(1), [0x01]);
263        assert_eq!(varint_of(127), [0x7f]);
264        assert_eq!(varint_of(128), [0x80, 0x01]);
265        assert_eq!(varint_of(300), [0xac, 0x02]);
266        assert_eq!(varint_of(u64::MAX), [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]);
267    }
268
269    /// Also from the specification: field 1, wire type 0, value 150 is the
270    /// three bytes `08 96 01`.
271    #[test]
272    fn a_tagged_varint_field_is_the_canonical_three_bytes() {
273        let mut encoder = Encoder::new();
274        encoder.uint64(1, 150);
275
276        assert_eq!(encoder.as_bytes(), [0x08, 0x96, 0x01]);
277    }
278
279    /// The tag packs the field number above the three wire-type bits, so field
280    /// 2 with a length-delimited body is `(2 << 3) | 2` = 0x12, then the length.
281    #[test]
282    fn a_string_field_is_tag_length_then_utf8() {
283        let mut encoder = Encoder::new();
284        encoder.string(2, "testing");
285
286        assert_eq!(encoder.as_bytes(), b"\x12\x07testing");
287    }
288
289    #[test]
290    fn a_nested_message_is_length_delimited_around_its_own_bytes() {
291        let mut inner = Encoder::new();
292        inner.uint64(1, 150);
293
294        let mut outer = Encoder::new();
295        outer.message(3, &inner);
296
297        // Field 3, wire type 2, three bytes of body, then the body itself.
298        assert_eq!(outer.as_bytes(), [0x1a, 0x03, 0x08, 0x96, 0x01]);
299    }
300
301    /// An empty sub-message still writes its tag and a zero length, because
302    /// "present but default" and "absent" are different to a decoder.
303    #[test]
304    fn an_empty_nested_message_still_writes_its_header() {
305        let mut outer = Encoder::new();
306        outer.message(15, &Encoder::new());
307
308        assert_eq!(outer.as_bytes(), [0x7a, 0x00]);
309    }
310
311    #[test]
312    fn fixed64_is_eight_little_endian_bytes() {
313        let mut encoder = Encoder::new();
314        // Field 7 (`Span.start_time_unix_nano`), wire type 1: (7 << 3) | 1.
315        encoder.fixed64(7, 1);
316
317        assert_eq!(encoder.as_bytes(), [0x39, 0x01, 0, 0, 0, 0, 0, 0, 0]);
318    }
319
320    #[test]
321    fn a_double_is_its_ieee_754_bits_little_endian() {
322        let mut encoder = Encoder::new();
323        // Field 4 (`NumberDataPoint.as_double`), wire type 1: (4 << 3) | 1.
324        encoder.double(4, 1.0);
325
326        assert_eq!(encoder.as_bytes(), [0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]);
327    }
328
329    /// The distinction the histogram sum depends on: a plain double vanishes at
330    /// zero, an explicitly present one does not.
331    #[test]
332    fn zero_is_skipped_by_double_and_kept_by_present_double() {
333        let mut skipped = Encoder::new();
334        skipped.double(5, 0.0);
335        assert!(skipped.is_empty());
336
337        let mut kept = Encoder::new();
338        kept.present_double(5, 0.0);
339        assert_eq!(kept.as_bytes(), [0x29, 0, 0, 0, 0, 0, 0, 0, 0]);
340    }
341
342    /// The same distinction for the other three, which is what `oneof` members
343    /// need. A real collector prints `Empty()` for an attribute whose arm was
344    /// never tagged, and that is silent data loss with a 200 in front of it.
345    #[test]
346    fn oneof_members_are_written_even_when_they_hold_the_default() {
347        let mut boolean = Encoder::new();
348        boolean.bool(2, false);
349        assert!(boolean.is_empty());
350        boolean.bool_present(2, false);
351        assert_eq!(boolean.as_bytes(), [0x10, 0x00]);
352
353        let mut integer = Encoder::new();
354        integer.int64_present(3, 0);
355        assert_eq!(integer.as_bytes(), [0x18, 0x00]);
356
357        let mut text = Encoder::new();
358        text.string_present(1, "");
359        assert_eq!(text.as_bytes(), [0x0a, 0x00]);
360
361        let mut number = Encoder::new();
362        number.sfixed64_present(6, 0);
363        assert_eq!(number.as_bytes(), [0x31, 0, 0, 0, 0, 0, 0, 0, 0]);
364    }
365
366    /// Negative `int64` sign-extends rather than zig-zagging, so -1 is nine
367    /// 0xff bytes and a 0x01. Getting this wrong would turn a small negative
368    /// number into an enormous positive one.
369    #[test]
370    fn a_negative_int64_sign_extends_to_ten_bytes() {
371        let mut encoder = Encoder::new();
372        encoder.int64(1, -1);
373
374        assert_eq!(encoder.as_bytes(), [0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]);
375    }
376
377    #[test]
378    fn packed_repeated_scalars_share_one_tag_and_one_length() {
379        let mut encoder = Encoder::new();
380        // Field 6 (`bucket_counts`), wire type 2, two elements of eight bytes.
381        encoder.packed_fixed64(6, &[1, 2]);
382
383        let mut expected = vec![0x32, 0x10];
384        expected.extend_from_slice(&1u64.to_le_bytes());
385        expected.extend_from_slice(&2u64.to_le_bytes());
386        assert_eq!(encoder.as_bytes(), expected.as_slice());
387    }
388
389    #[test]
390    fn packed_doubles_are_bounds_as_the_collector_expects_them() {
391        let mut encoder = Encoder::new();
392        encoder.packed_double(7, &[0.005, 0.01]);
393
394        let mut expected = vec![0x3a, 0x10];
395        expected.extend_from_slice(&0.005f64.to_le_bytes());
396        expected.extend_from_slice(&0.01f64.to_le_bytes());
397        assert_eq!(encoder.as_bytes(), expected.as_slice());
398    }
399
400    /// Proto3 leaves defaults out. An all-default message is zero bytes, and a
401    /// decoder rebuilds the defaults on the other side.
402    #[test]
403    fn default_values_are_omitted_entirely() {
404        let mut encoder = Encoder::new();
405        encoder.uint64(1, 0);
406        encoder.int64(2, 0);
407        encoder.bool(3, false);
408        encoder.enumeration(4, 0);
409        encoder.fixed64(5, 0);
410        encoder.string(6, "");
411        encoder.bytes_field(7, &[]);
412        encoder.packed_fixed64(8, &[]);
413
414        assert!(encoder.is_empty());
415    }
416
417    /// Field numbers above 15 need a two-byte tag, and OTLP has several — the
418    /// span status is field 15, and `Span.flags` is 16.
419    #[test]
420    fn field_numbers_past_fifteen_use_a_two_byte_tag() {
421        let mut encoder = Encoder::new();
422        encoder.uint64(16, 1);
423
424        // (16 << 3) | 0 = 128, which needs a continuation byte of its own.
425        assert_eq!(encoder.as_bytes(), [0x80, 0x01, 0x01]);
426    }
427}