datadog_agent_metrics_v3/
types.rs

1//! V3 payload type definitions and protocol buffer field numbers.
2
3/// V3 metric type values.
4///
5/// These match the `metricType` enum in `intake_v3.proto`.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7#[repr(u8)]
8pub enum V3MetricType {
9    /// A monotonically increasing counter, submitted as per-interval deltas.
10    Count = 1,
11    /// A count normalized to a per-second rate.
12    Rate = 2,
13    /// A point-in-time value.
14    Gauge = 3,
15    /// A distribution summarized as a DDSketch.
16    Sketch = 4,
17}
18
19impl V3MetricType {
20    /// Returns the numeric value for encoding in the types column.
21    pub const fn as_u64(self) -> u64 {
22        self as u64
23    }
24}
25
26/// V3 value type values.
27///
28/// These are encoded in bits 4-7 of the types column and indicate which
29/// value array contains the metric's points.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[repr(u8)]
32pub(crate) enum V3ValueType {
33    /// Value is zero, not stored explicitly.
34    Zero = 0x00,
35
36    /// Value is stored in vals_sint64.
37    Sint64 = 0x10,
38
39    /// Value is stored in vals_float32.
40    Float32 = 0x20,
41
42    /// Value is stored in vals_float64.
43    Float64 = 0x30,
44}
45
46impl V3ValueType {
47    /// Returns the numeric value for encoding in the types column.
48    pub fn as_u64(self) -> u64 {
49        self as u64
50    }
51}
52
53/// Intermediate point classification for value type compaction.
54///
55/// This provides finer-grained classification than [`V3ValueType`] to avoid
56/// precision loss when combining different value types. In particular, it
57/// distinguishes small integers (that fit losslessly in f32) from large integers
58/// (that don't), so that mixing a large integer with a Float32 value correctly
59/// escalates to Float64 rather than silently truncating the integer.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
61#[repr(u8)]
62enum PointKind {
63    /// Value is zero.
64    Zero = 0,
65    /// Integer with |v| <= 2^24, fits losslessly in both sint64 and f32.
66    Int24 = 1,
67    /// Integer with |v| > 2^24, fits in sint64 varint but NOT losslessly in f32.
68    Int48 = 2,
69    /// Fractional value exactly representable as f32.
70    Float32 = 3,
71    /// Everything else - requires full f64 precision.
72    Float64 = 4,
73}
74
75/// Maximum integer magnitude that fits losslessly in f32 (2^24).
76const F32_INT_MAX: i64 = 1 << 24;
77
78impl PointKind {
79    /// Classifies a single f64 value.
80    fn for_value(v: f64) -> Self {
81        if v == 0.0 {
82            return Self::Zero;
83        }
84
85        // Varint range that fits in 7 bytes or less (49 bits).
86        const VARINT_WIDTH: i32 = 7 * 7 - 1;
87        const MAX_INT: i64 = 1 << VARINT_WIDTH;
88        const MIN_INT: i64 = -MAX_INT;
89
90        let i = v as i64;
91        if (MIN_INT..MAX_INT).contains(&i) && (i as f64) == v {
92            if (-F32_INT_MAX..=F32_INT_MAX).contains(&i) {
93                return Self::Int24;
94            }
95            return Self::Int48;
96        }
97
98        if (v as f32 as f64) == v {
99            return Self::Float32;
100        }
101
102        Self::Float64
103    }
104
105    /// Combines two point kinds into the smallest kind that can represent both.
106    ///
107    /// This is `max(self, other)` in all cases **except**:
108    /// - `Int48 + Float32 = Float64` (and vice versa), because large integers
109    ///   lose precision in f32, and fractional values can't be stored as sint64.
110    fn union(self, other: Self) -> Self {
111        match (self, other) {
112            (Self::Int48, Self::Float32) | (Self::Float32, Self::Int48) => Self::Float64,
113            _ => self.max(other),
114        }
115    }
116
117    /// Converts to the wire-format value type.
118    fn to_value_type(self) -> V3ValueType {
119        match self {
120            Self::Zero => V3ValueType::Zero,
121            Self::Int24 | Self::Int48 => V3ValueType::Sint64,
122            Self::Float32 => V3ValueType::Float32,
123            Self::Float64 => V3ValueType::Float64,
124        }
125    }
126}
127
128/// Determines the best [`V3ValueType`] for a set of f64 values.
129///
130/// Uses [`PointKind`] internally to avoid precision loss when mixing
131/// large integers with fractional float32 values.
132pub(crate) fn value_type_for_values(values: impl Iterator<Item = f64>) -> V3ValueType {
133    let mut kind = PointKind::Zero;
134    for v in values {
135        kind = kind.union(PointKind::for_value(v));
136    }
137    kind.to_value_type()
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_point_kind_classification() {
146        // Zero
147        assert_eq!(PointKind::for_value(0.0), PointKind::Zero);
148
149        // Small integers (fit in f32)
150        assert_eq!(PointKind::for_value(100.0), PointKind::Int24);
151        assert_eq!(PointKind::for_value(-100.0), PointKind::Int24);
152        assert_eq!(PointKind::for_value((1 << 24) as f64), PointKind::Int24);
153        assert_eq!(PointKind::for_value(-((1 << 24) as f64)), PointKind::Int24);
154
155        // Large integers (don't fit losslessly in f32)
156        assert_eq!(PointKind::for_value(((1 << 24) + 1) as f64), PointKind::Int48);
157        assert_eq!(PointKind::for_value((1i64 << 30) as f64), PointKind::Int48);
158
159        // Float32
160        assert_eq!(PointKind::for_value(1.5), PointKind::Float32);
161        assert_eq!(PointKind::for_value(2.75), PointKind::Float32);
162
163        // Float64
164        assert_eq!(PointKind::for_value(std::f64::consts::PI), PointKind::Float64);
165        let large = ((1i64 << 50) + 1) as f64;
166        assert_eq!(PointKind::for_value(large), PointKind::Float64);
167    }
168
169    #[test]
170    fn test_point_kind_union() {
171        // Standard widening (max)
172        assert_eq!(PointKind::Zero.union(PointKind::Int24), PointKind::Int24);
173        assert_eq!(PointKind::Int24.union(PointKind::Int48), PointKind::Int48);
174        assert_eq!(PointKind::Int24.union(PointKind::Float32), PointKind::Float32);
175        assert_eq!(PointKind::Float32.union(PointKind::Float64), PointKind::Float64);
176        assert_eq!(PointKind::Float64.union(PointKind::Zero), PointKind::Float64);
177
178        // The critical case: large integer + float32 must escalate to float64
179        assert_eq!(PointKind::Int48.union(PointKind::Float32), PointKind::Float64);
180        assert_eq!(PointKind::Float32.union(PointKind::Int48), PointKind::Float64);
181    }
182
183    #[test]
184    fn test_value_type_for_values() {
185        // All zeros
186        assert_eq!(value_type_for_values([0.0, 0.0].into_iter()), V3ValueType::Zero);
187
188        // Small integers
189        assert_eq!(value_type_for_values([100.0, 200.0].into_iter()), V3ValueType::Sint64);
190
191        // Large integers
192        assert_eq!(
193            value_type_for_values([(1i64 << 30) as f64, 200.0].into_iter()),
194            V3ValueType::Sint64
195        );
196
197        // Small integer + float32 → Float32 (safe, small int fits in f32)
198        assert_eq!(value_type_for_values([100.0, 1.5].into_iter()), V3ValueType::Float32);
199
200        // Large integer + float32 → Float64 (the bug fix!)
201        assert_eq!(
202            value_type_for_values([(1i64 << 30) as f64, 1.5].into_iter()),
203            V3ValueType::Float64
204        );
205
206        // Float64 value forces Float64
207        assert_eq!(
208            value_type_for_values([100.0, std::f64::consts::PI].into_iter()),
209            V3ValueType::Float64
210        );
211
212        // Empty iterator
213        assert_eq!(value_type_for_values(std::iter::empty()), V3ValueType::Zero);
214    }
215}