common_query/native_histogram/
encoding.rs1use 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#[derive(Debug, Snafu)]
32#[snafu(display("{message}"))]
33pub struct NativeHistogramError {
34 message: String,
35}
36
37type Result<T> = std::result::Result<T, NativeHistogramError>;
38
39pub 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
57pub 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(¤t_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}