Skip to main content

common_query/native_histogram/
encoding.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use api::greptime_proto::io::prometheus::write::v2::histogram::{Count, ZeroCount};
16use api::greptime_proto::io::prometheus::write::v2::{BucketSpan, Histogram};
17use api::helper::ColumnDataTypeWrapper;
18use api::v1::value::ValueData;
19use api::v1::{ColumnSchema, ListValue, SemanticType, Value};
20use snafu::{Snafu, ensure};
21
22use crate::native_histogram::{
23    CUSTOM_BUCKETS_SCHEMA, MAX_EXPONENTIAL_SCHEMA, NATIVE_HISTOGRAM_FIELD_NAMES,
24    exponential_overflow_bucket_index, native_histogram_value_type,
25};
26use crate::prelude::greptime_native_histogram;
27
28const MAX_REDUCIBLE_NATIVE_HISTOGRAM_SCHEMA: i32 = 52;
29
30/// Error returned while validating or encoding a native histogram.
31#[derive(Debug, Snafu)]
32#[snafu(display("{message}"))]
33pub struct NativeHistogramError {
34    message: String,
35}
36
37type Result<T> = std::result::Result<T, NativeHistogramError>;
38
39/// Returns the canonical column schema for a native histogram value.
40pub fn native_histogram_column_schema() -> Result<ColumnSchema> {
41    let (datatype, datatype_extension) =
42        ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
43            .map_err(|error| NativeHistogramError {
44                message: format!("native histogram type cannot be encoded: {error}"),
45            })?
46            .into_parts();
47
48    Ok(ColumnSchema {
49        column_name: greptime_native_histogram().to_string(),
50        datatype: datatype as i32,
51        semantic_type: SemanticType::Field as i32,
52        datatype_extension,
53        options: None,
54    })
55}
56
57/// Validates and encodes a Prometheus histogram into the canonical Struct value.
58pub fn encode_native_histogram(histogram: &Histogram) -> Result<ValueData> {
59    let uses_float_counts = native_histogram_uses_float_counts(histogram)?;
60    validate_native_histogram(histogram, uses_float_counts)?;
61
62    let mut items = Vec::with_capacity(NATIVE_HISTOGRAM_FIELD_NAMES.len());
63    let positive_span_lengths = i32_span_lengths("positive", &histogram.positive_spans)?;
64    let negative_span_lengths = i32_span_lengths("negative", &histogram.negative_spans)?;
65    items.extend([
66        pb_value(ValueData::I32Value(histogram.schema)),
67        pb_value(ValueData::F64Value(histogram.zero_threshold)),
68        pb_value(ValueData::F64Value(histogram.sum)),
69        pb_value(ValueData::I32Value(histogram.reset_hint)),
70        optional_pb_value((histogram.start_timestamp != 0).then_some(
71            ValueData::TimestampMillisecondValue(histogram.start_timestamp),
72        )),
73        f64_list_value(histogram.custom_values.iter().copied()),
74        i32_list_value(histogram.positive_spans.iter().map(|span| span.offset)),
75        i32_list_value(positive_span_lengths),
76        i32_list_value(histogram.negative_spans.iter().map(|span| span.offset)),
77        i32_list_value(negative_span_lengths),
78    ]);
79
80    if uses_float_counts {
81        validate_float_native_histogram_counts(histogram)?;
82        let count = match histogram.count.as_ref() {
83            Some(Count::CountFloat(count)) => *count,
84            _ => 0.0,
85        };
86        let zero_count = match histogram.zero_count.as_ref() {
87            Some(ZeroCount::ZeroCountFloat(zero_count)) => *zero_count,
88            _ => 0.0,
89        };
90        items.extend([
91            null_pb_value(),
92            null_pb_value(),
93            i64_list_value(std::iter::empty()),
94            i64_list_value(std::iter::empty()),
95            pb_value(ValueData::F64Value(count)),
96            pb_value(ValueData::F64Value(zero_count)),
97            f64_list_value(histogram.positive_counts.iter().copied()),
98            f64_list_value(histogram.negative_counts.iter().copied()),
99        ]);
100    } else {
101        let count = match histogram.count.as_ref() {
102            Some(Count::CountInt(count)) => *count,
103            _ => 0,
104        };
105        let zero_count = match histogram.zero_count.as_ref() {
106            Some(ZeroCount::ZeroCountInt(zero_count)) => *zero_count,
107            _ => 0,
108        };
109        let positive_buckets = bucket_counts_from_deltas(&histogram.positive_deltas)?;
110        let negative_buckets = bucket_counts_from_deltas(&histogram.negative_deltas)?;
111        validate_integer_native_histogram_counts(histogram, &positive_buckets, &negative_buckets)?;
112        let count = i64::try_from(count).map_err(|_| NativeHistogramError {
113            message: format!("native histogram integer count {count} overflows i64"),
114        })?;
115        let zero_count = i64::try_from(zero_count).map_err(|_| NativeHistogramError {
116            message: format!("native histogram integer zero_count {zero_count} overflows i64"),
117        })?;
118        items.extend([
119            pb_value(ValueData::I64Value(count)),
120            pb_value(ValueData::I64Value(zero_count)),
121            i64_list_value(positive_buckets),
122            i64_list_value(negative_buckets),
123            null_pb_value(),
124            null_pb_value(),
125            f64_list_value(std::iter::empty()),
126            f64_list_value(std::iter::empty()),
127        ]);
128    }
129
130    Ok(ValueData::StructValue(api::v1::StructValue { items }))
131}
132
133fn validate_native_histogram(histogram: &Histogram, uses_float_counts: bool) -> Result<()> {
134    let exponential_overflow_index = validate_native_histogram_schema(histogram.schema)?;
135    validate_native_histogram_custom_values(histogram)?;
136
137    if histogram.schema == CUSTOM_BUCKETS_SCHEMA {
138        ensure!(
139            histogram.zero_threshold == 0.0 && native_histogram_zero_count_is_zero(histogram),
140            NativeHistogramSnafu {
141                message: "custom native histogram must not use a zero bucket"
142            }
143        );
144        ensure!(
145            histogram.negative_spans.is_empty()
146                && histogram.negative_deltas.is_empty()
147                && histogram.negative_counts.is_empty(),
148            NativeHistogramSnafu {
149                message: "custom native histogram must not use negative buckets"
150            }
151        );
152    }
153
154    let (positive_buckets, negative_buckets) = if uses_float_counts {
155        (
156            histogram.positive_counts.len(),
157            histogram.negative_counts.len(),
158        )
159    } else {
160        (
161            histogram.positive_deltas.len(),
162            histogram.negative_deltas.len(),
163        )
164    };
165    let bucket_index_range = if let Some(overflow_index) = exponential_overflow_index {
166        (i32::MIN, overflow_index)
167    } else {
168        (
169            0,
170            i32::try_from(histogram.custom_values.len()).map_err(|_| NativeHistogramError {
171                message: "custom native histogram has too many custom_values".to_string(),
172            })?,
173        )
174    };
175    validate_native_histogram_spans(
176        "positive",
177        &histogram.positive_spans,
178        positive_buckets,
179        bucket_index_range,
180    )?;
181    validate_native_histogram_spans(
182        "negative",
183        &histogram.negative_spans,
184        negative_buckets,
185        bucket_index_range,
186    )?;
187
188    Ok(())
189}
190
191fn validate_native_histogram_schema(schema: i32) -> Result<Option<i32>> {
192    if schema == CUSTOM_BUCKETS_SCHEMA {
193        return Ok(None);
194    }
195
196    if let Some(overflow_index) = exponential_overflow_bucket_index(schema) {
197        return Ok(Some(overflow_index));
198    }
199
200    if (MAX_EXPONENTIAL_SCHEMA + 1..=MAX_REDUCIBLE_NATIVE_HISTOGRAM_SCHEMA).contains(&schema) {
201        Err(NativeHistogramError {
202            message: format!("native histogram schema {schema} must be reduced before ingestion"),
203        })
204    } else {
205        Err(NativeHistogramError {
206            message: format!("native histogram schema {schema} is unsupported"),
207        })
208    }
209}
210
211fn validate_native_histogram_custom_values(histogram: &Histogram) -> Result<()> {
212    if histogram.schema != CUSTOM_BUCKETS_SCHEMA {
213        ensure!(
214            histogram.custom_values.is_empty(),
215            NativeHistogramSnafu {
216                message: "standard native histogram must not use custom_values"
217            }
218        );
219        return Ok(());
220    }
221
222    for value in &histogram.custom_values {
223        ensure!(
224            !value.is_nan() && *value != f64::INFINITY,
225            NativeHistogramSnafu {
226                message: "custom native histogram custom_values must not contain +Inf or NaN"
227            }
228        );
229    }
230    for values in histogram.custom_values.windows(2) {
231        ensure!(
232            values[0] < values[1],
233            NativeHistogramSnafu {
234                message: "custom native histogram custom_values must be sorted"
235            }
236        );
237    }
238
239    Ok(())
240}
241
242fn validate_native_histogram_spans(
243    name: &str,
244    spans: &[BucketSpan],
245    bucket_count: usize,
246    bucket_index_range: (i32, i32),
247) -> Result<()> {
248    let span_len = spans.iter().try_fold(0usize, |sum, span| {
249        let length = usize::try_from(span.length).map_err(|_| NativeHistogramError {
250            message: format!("native histogram {name} span length exceeds usize"),
251        })?;
252        sum.checked_add(length).ok_or_else(|| NativeHistogramError {
253            message: format!("native histogram {name} spans overflow"),
254        })
255    })?;
256    ensure!(
257        span_len == bucket_count,
258        NativeHistogramSnafu {
259            message: format!(
260                "native histogram {name} spans describe {span_len} buckets, found {bucket_count}"
261            )
262        }
263    );
264
265    let mut current_index = 0i32;
266    for (span_index, span) in spans.iter().enumerate() {
267        ensure!(
268            span.offset >= 0 || (span_index == 0 && bucket_index_range.0 == i32::MIN),
269            NativeHistogramSnafu {
270                message: format!(
271                    "native histogram {name} span {} has negative offset {}",
272                    span_index + 1,
273                    span.offset
274                )
275            }
276        );
277        current_index = if span_index == 0 {
278            span.offset
279        } else {
280            current_index
281                .checked_add(span.offset)
282                .ok_or_else(|| NativeHistogramError {
283                    message: format!("native histogram {name} span index overflows i32"),
284                })?
285        };
286
287        for _ in 0..span.length {
288            ensure!(
289                (bucket_index_range.0..=bucket_index_range.1).contains(&current_index),
290                NativeHistogramSnafu {
291                    message: format!(
292                        "native histogram {name} bucket index {current_index} is out of range"
293                    )
294                }
295            );
296            current_index = current_index
297                .checked_add(1)
298                .ok_or_else(|| NativeHistogramError {
299                    message: format!("native histogram {name} span index overflows i32"),
300                })?;
301        }
302    }
303
304    Ok(())
305}
306
307fn validate_float_native_histogram_counts(histogram: &Histogram) -> Result<()> {
308    let count = match histogram.count.as_ref() {
309        Some(Count::CountFloat(count)) => *count,
310        _ => 0.0,
311    };
312    ensure!(
313        count >= 0.0 || count.is_nan(),
314        NativeHistogramSnafu {
315            message: "native histogram float count must not be negative"
316        }
317    );
318
319    let zero_count = match histogram.zero_count.as_ref() {
320        Some(ZeroCount::ZeroCountFloat(zero_count)) => *zero_count,
321        _ => 0.0,
322    };
323    ensure!(
324        zero_count >= 0.0 || zero_count.is_nan(),
325        NativeHistogramSnafu {
326            message: "native histogram float zero_count must not be negative"
327        }
328    );
329
330    for (name, counts) in [
331        ("positive", &histogram.positive_counts),
332        ("negative", &histogram.negative_counts),
333    ] {
334        for (index, count) in counts.iter().enumerate() {
335            ensure!(
336                *count >= 0.0 || count.is_nan(),
337                NativeHistogramSnafu {
338                    message: format!(
339                        "native histogram {name} bucket {} count must not be negative",
340                        index + 1
341                    )
342                }
343            );
344        }
345    }
346
347    Ok(())
348}
349
350fn validate_integer_native_histogram_counts(
351    histogram: &Histogram,
352    positive_buckets: &[i64],
353    negative_buckets: &[i64],
354) -> Result<()> {
355    let count = match histogram.count.as_ref() {
356        Some(Count::CountInt(count)) => *count,
357        _ => 0,
358    };
359    let zero_count = match histogram.zero_count.as_ref() {
360        Some(ZeroCount::ZeroCountInt(zero_count)) => *zero_count,
361        _ => 0,
362    };
363    let bucket_count =
364        positive_buckets
365            .iter()
366            .chain(negative_buckets)
367            .try_fold(zero_count, |total, bucket| {
368                let bucket = u64::try_from(*bucket).map_err(|_| NativeHistogramError {
369                    message: "native histogram bucket count is negative".to_string(),
370                })?;
371                total
372                    .checked_add(bucket)
373                    .ok_or_else(|| NativeHistogramError {
374                        message: "native histogram bucket total overflows u64".to_string(),
375                    })
376            })?;
377    ensure!(
378        if histogram.sum.is_nan() {
379            bucket_count <= count
380        } else {
381            bucket_count == count
382        },
383        NativeHistogramSnafu {
384            message: format!(
385                "native histogram has {bucket_count} observations in buckets, count is {count}"
386            )
387        }
388    );
389
390    Ok(())
391}
392
393fn native_histogram_zero_count_is_zero(histogram: &Histogram) -> bool {
394    match histogram.zero_count.as_ref() {
395        Some(ZeroCount::ZeroCountInt(zero_count)) => *zero_count == 0,
396        Some(ZeroCount::ZeroCountFloat(zero_count)) => *zero_count == 0.0,
397        None => true,
398    }
399}
400
401fn native_histogram_uses_float_counts(histogram: &Histogram) -> Result<bool> {
402    let uses_float_count = matches!(histogram.count, Some(Count::CountFloat(_)))
403        || matches!(histogram.zero_count, Some(ZeroCount::ZeroCountFloat(_)));
404    let uses_int_count = matches!(histogram.count, Some(Count::CountInt(_)))
405        || matches!(histogram.zero_count, Some(ZeroCount::ZeroCountInt(_)));
406    let uses_float_buckets =
407        !histogram.positive_counts.is_empty() || !histogram.negative_counts.is_empty();
408    let uses_int_buckets =
409        !histogram.positive_deltas.is_empty() || !histogram.negative_deltas.is_empty();
410
411    ensure!(
412        !matches!(
413            (&histogram.count, &histogram.zero_count),
414            (Some(Count::CountInt(_)), Some(ZeroCount::ZeroCountFloat(_)))
415                | (Some(Count::CountFloat(_)), Some(ZeroCount::ZeroCountInt(_)))
416        ),
417        NativeHistogramSnafu {
418            message: "native histogram count and zero_count must use the same integer or float family"
419        }
420    );
421    ensure!(
422        !(uses_float_buckets && uses_int_buckets),
423        NativeHistogramSnafu {
424            message: "native histogram bucket counts must use either integer deltas or float counts"
425        }
426    );
427    ensure!(
428        !(uses_float_count && uses_int_buckets),
429        NativeHistogramSnafu {
430            message: "float native histogram must not use integer bucket deltas"
431        }
432    );
433    ensure!(
434        !(uses_int_count && uses_float_buckets),
435        NativeHistogramSnafu {
436            message: "integer native histogram must not use float bucket counts"
437        }
438    );
439
440    Ok(uses_float_count || uses_float_buckets)
441}
442
443fn pb_value(value_data: ValueData) -> Value {
444    optional_pb_value(Some(value_data))
445}
446
447fn null_pb_value() -> Value {
448    optional_pb_value(None)
449}
450
451fn optional_pb_value(value_data: Option<ValueData>) -> Value {
452    Value { value_data }
453}
454
455fn list_value(values: impl IntoIterator<Item = ValueData>) -> Value {
456    pb_value(ValueData::ListValue(ListValue {
457        items: values.into_iter().map(pb_value).collect(),
458    }))
459}
460
461fn i32_list_value(values: impl IntoIterator<Item = i32>) -> Value {
462    list_value(values.into_iter().map(ValueData::I32Value))
463}
464
465fn i32_span_lengths(name: &str, spans: &[BucketSpan]) -> Result<Vec<i32>> {
466    spans
467        .iter()
468        .map(|span| {
469            i32::try_from(span.length).map_err(|_| NativeHistogramError {
470                message: format!(
471                    "native histogram {name} span length {} overflows i32",
472                    span.length
473                ),
474            })
475        })
476        .collect()
477}
478
479fn i64_list_value(values: impl IntoIterator<Item = i64>) -> Value {
480    list_value(values.into_iter().map(ValueData::I64Value))
481}
482
483fn f64_list_value(values: impl IntoIterator<Item = f64>) -> Value {
484    list_value(values.into_iter().map(ValueData::F64Value))
485}
486
487fn bucket_counts_from_deltas(deltas: &[i64]) -> Result<Vec<i64>> {
488    let mut count = 0_i64;
489    let mut buckets = Vec::with_capacity(deltas.len());
490
491    for delta in deltas {
492        count = count
493            .checked_add(*delta)
494            .ok_or_else(|| NativeHistogramError {
495                message: "native histogram bucket count overflows i64".to_string(),
496            })?;
497        ensure!(
498            count >= 0,
499            NativeHistogramSnafu {
500                message: "native histogram bucket count is negative"
501            }
502        );
503        buckets.push(count);
504    }
505
506    Ok(buckets)
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn shared_errors_are_protocol_neutral() {
515        let error = encode_native_histogram(&Histogram {
516            schema: 9,
517            ..Default::default()
518        })
519        .unwrap_err();
520
521        assert_eq!(
522            error.to_string(),
523            "native histogram schema 9 must be reduced before ingestion"
524        );
525        assert!(!error.to_string().contains("remote write"));
526        assert!(!error.to_string().contains("OTLP"));
527    }
528}