1use std::collections::hash_map::Entry;
16
17use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
18use api::greptime_proto::io::prometheus::write::v2::histogram::{Count, ZeroCount};
19#[cfg(test)]
20use api::greptime_proto::io::prometheus::write::v2::{Exemplar, Metadata, metadata};
21use api::greptime_proto::io::prometheus::write::v2::{Histogram, Request, Sample, TimeSeries};
22use api::helper::ColumnDataTypeWrapper;
23use api::v1::value::ValueData;
24use api::v1::{ColumnSchema, ListValue, RowInsertRequest, Rows, SemanticType, Value};
25use bytes::Bytes;
26use common_grpc::precision::Precision;
27use common_query::native_histogram::*;
28use common_query::prelude::{greptime_timestamp, greptime_value};
29use pipeline::{ContextOpt, ContextReq};
30use prost::Message;
31use snafu::{OptionExt, ResultExt, ensure};
32
33use crate::error::{self, Result};
34use crate::prom_remote_write::row_builder::PromCtx;
35use crate::prom_remote_write::try_decompress;
36use crate::prom_remote_write::validation::validate_label_name;
37#[allow(deprecated)]
38use crate::prom_store::{
39 DATABASE_LABEL, DATABASE_LABEL_ALT, METRIC_NAME_LABEL, PHYSICAL_TABLE_LABEL,
40 PHYSICAL_TABLE_LABEL_ALT, SCHEMA_LABEL,
41};
42use crate::row_writer::{self, TableData};
43
44type PromTags = Vec<(String, String)>;
45type ResolvedSeriesLabels = (PromCtx, String, PromTags);
46
47pub(crate) fn decode_remote_write_v2_request(is_zstd: bool, body: Bytes) -> Result<Request> {
48 let _timer = crate::metrics::METRIC_HTTP_PROM_STORE_DECODE_ELAPSED.start_timer();
49
50 let buf = if let Ok(buf) = try_decompress(is_zstd, &body[..]) {
53 buf
54 } else {
55 try_decompress(!is_zstd, &body[..])?
56 };
57
58 Request::decode(&buf[..]).context(error::DecodePromRemoteRequestSnafu)
59}
60
61pub(crate) struct RemoteWriteV2WriteRequests {
62 pub samples: ContextReq,
63 pub histograms: ContextReq,
64 pub sample_count: u64,
65 pub histogram_count: u64,
66}
67
68pub(crate) fn into_write_requests(request: Request) -> Result<RemoteWriteV2WriteRequests> {
73 let _timer = crate::metrics::METRIC_HTTP_PROM_STORE_CONVERT_ELAPSED.start_timer();
74 let Request {
75 symbols,
76 timeseries,
77 } = request;
78
79 ensure!(
80 symbols.first().map(|s| s.as_str()) == Some(""),
81 error::InvalidPromRemoteRequestSnafu {
82 msg: "remote write v2 symbols must start with an empty string".to_string(),
83 }
84 );
85
86 let mut sample_tables = HashMap::<PromCtx, HashMap<String, TableData>>::new();
87 let mut histogram_tables = HashMap::<PromCtx, HashMap<String, TableData>>::new();
88 let mut sample_metrics = HashSet::<(PromCtx, String)>::new();
89 let mut histogram_metrics = HashSet::<(PromCtx, String)>::new();
90 let mut sample_count_total = 0;
91 let mut histogram_count_total = 0;
92
93 for series in timeseries {
94 let sample_count = series.samples.len();
96 let histogram_count = series.histograms.len();
97 if sample_count == 0 && histogram_count == 0 {
98 continue;
99 }
100
101 let (prom_ctx, table_name, tags) = resolve_series_labels(&symbols, &series)?;
102 ensure_no_internal_histogram_labels(&tags)?;
103 let metric_key = (prom_ctx.clone(), table_name.clone());
104 if sample_count > 0 {
105 ensure!(
106 !histogram_metrics.contains(&metric_key),
107 error::InvalidPromRemoteRequestSnafu {
108 msg: format!(
109 "remote write v2 metric `{table_name}` contains both samples and native histograms"
110 ),
111 }
112 );
113 sample_metrics.insert(metric_key.clone());
114 }
115 if histogram_count > 0 {
116 ensure!(
117 !sample_metrics.contains(&metric_key),
118 error::InvalidPromRemoteRequestSnafu {
119 msg: format!(
120 "remote write v2 metric `{table_name}` contains both samples and native histograms"
121 ),
122 }
123 );
124 histogram_metrics.insert(metric_key);
125 }
126
127 if sample_count > 0 && histogram_count == 0 {
128 let table_data = get_or_create_table_data(
131 &mut sample_tables,
132 prom_ctx,
133 table_name,
134 tags.len() + 2,
135 sample_count,
136 );
137
138 write_samples(table_data, series.samples, tags)?;
139 sample_count_total += sample_count as u64;
140 continue;
142 }
143
144 if histogram_count > 0 {
145 let table_data = get_or_create_table_data(
146 &mut histogram_tables,
147 prom_ctx,
148 table_name,
149 tags.len() + 2,
150 histogram_count,
151 );
152
153 let mut histograms = series.histograms;
154 let Some(last_histogram) = histograms.pop() else {
155 continue;
156 };
157 for histogram in &histograms {
158 write_native_histogram(table_data, histogram, tags.iter().cloned())?;
159 }
160 write_native_histogram(table_data, &last_histogram, tags.into_iter())?;
161 histogram_count_total += histogram_count as u64;
162 }
163 }
164
165 Ok(RemoteWriteV2WriteRequests {
166 samples: into_context_req(sample_tables),
167 histograms: into_context_req(histogram_tables),
168 sample_count: sample_count_total,
169 histogram_count: histogram_count_total,
170 })
171}
172
173fn get_or_create_table_data(
174 tables: &mut HashMap<PromCtx, HashMap<String, TableData>>,
175 prom_ctx: PromCtx,
176 table_name: String,
177 column_count: usize,
178 row_count: usize,
179) -> &mut TableData {
180 match tables.entry(prom_ctx).or_default().entry(table_name) {
181 Entry::Occupied(entry) => {
182 let table_data = entry.into_mut();
183 table_data.reserve_rows(row_count);
184 table_data
185 }
186 Entry::Vacant(entry) => entry.insert(TableData::new(column_count, row_count)),
187 }
188}
189
190fn write_samples(
191 table_data: &mut TableData,
192 mut samples: Vec<Sample>,
193 tags: PromTags,
194) -> Result<()> {
195 let Some(last_sample) = samples.pop() else {
196 return Ok(());
197 };
198
199 for sample in &samples {
200 write_sample(table_data, sample, tags.iter().cloned())?;
201 }
202
203 write_sample(table_data, &last_sample, tags.into_iter())
204}
205
206fn write_sample(
207 table_data: &mut TableData,
208 sample: &Sample,
209 tags: impl Iterator<Item = (String, String)>,
210) -> Result<()> {
211 let mut row = table_data.alloc_one_row();
212 row_writer::write_ts_to_millis(
213 table_data,
214 greptime_timestamp(),
215 Some(sample.timestamp),
216 Precision::Millisecond,
217 &mut row,
218 )?;
219 row_writer::write_f64(table_data, greptime_value(), sample.value, &mut row)?;
220 row_writer::write_tags(table_data, tags, &mut row)?;
221 table_data.add_row(row);
222
223 Ok(())
224}
225
226fn write_native_histogram(
227 table_data: &mut TableData,
228 histogram: &Histogram,
229 tags: impl Iterator<Item = (String, String)>,
230) -> Result<()> {
231 let mut row = table_data.alloc_one_row();
235 row_writer::write_ts_to_millis(
236 table_data,
237 greptime_timestamp(),
238 Some(histogram.timestamp),
239 Precision::Millisecond,
240 &mut row,
241 )?;
242
243 write_native_histogram_value(table_data, histogram, &mut row)?;
244
245 row_writer::write_tags(table_data, tags, &mut row)?;
246 table_data.add_row(row);
247
248 Ok(())
249}
250
251fn write_native_histogram_value(
252 table_data: &mut TableData,
253 histogram: &Histogram,
254 row: &mut Vec<Value>,
255) -> Result<()> {
256 let column_schema = native_histogram_column_schema();
257 let value = native_histogram_struct_value(histogram)?;
258
259 row_writer::write_by_schema(
260 table_data,
261 std::iter::once((column_schema, Some(value))),
262 row,
263 )
264}
265
266fn native_histogram_column_schema() -> ColumnSchema {
267 let (datatype, datatype_extension) =
268 ColumnDataTypeWrapper::try_from(native_histogram_value_type().clone())
269 .expect("native histogram type is convertible to protobuf")
270 .into_parts();
271
272 ColumnSchema {
273 column_name: NATIVE_HISTOGRAM_FIELD.to_string(),
274 datatype: datatype as i32,
275 semantic_type: SemanticType::Field as i32,
276 datatype_extension,
277 options: None,
278 }
279}
280
281fn native_histogram_struct_value(histogram: &Histogram) -> Result<ValueData> {
282 let mut items = Vec::with_capacity(NATIVE_HISTOGRAM_FIELD_NAMES.len());
283 items.extend([
284 pb_value(ValueData::I32Value(histogram.schema)),
285 pb_value(ValueData::F64Value(histogram.zero_threshold)),
286 pb_value(ValueData::F64Value(histogram.sum)),
287 pb_value(ValueData::I32Value(histogram.reset_hint)),
288 optional_pb_value((histogram.start_timestamp != 0).then_some(
289 ValueData::TimestampMillisecondValue(histogram.start_timestamp),
290 )),
291 f64_list_value(histogram.custom_values.iter().copied()),
292 i32_list_value(histogram.positive_spans.iter().map(|span| span.offset)),
293 u32_list_value(histogram.positive_spans.iter().map(|span| span.length)),
294 i32_list_value(histogram.negative_spans.iter().map(|span| span.offset)),
295 u32_list_value(histogram.negative_spans.iter().map(|span| span.length)),
296 ]);
297
298 let int_counts = match (&histogram.count, &histogram.zero_count) {
299 (Some(Count::CountInt(count)), Some(ZeroCount::ZeroCountInt(zero_count))) => {
300 (*count, *zero_count)
301 }
302 (Some(Count::CountInt(count)), _) => (*count, 0),
303 (_, Some(ZeroCount::ZeroCountInt(zero_count))) => (0, *zero_count),
304 _ => (0, 0),
305 };
306 let float_counts = match (&histogram.count, &histogram.zero_count) {
307 (Some(Count::CountFloat(count)), Some(ZeroCount::ZeroCountFloat(zero_count))) => {
308 Some((*count, *zero_count))
309 }
310 (Some(Count::CountFloat(count)), _) => Some((*count, 0.0)),
311 _ => None,
312 };
313
314 if let Some(counts) = float_counts {
315 items.extend([
316 null_pb_value(),
317 null_pb_value(),
318 i64_list_value(std::iter::empty()),
319 i64_list_value(std::iter::empty()),
320 pb_value(ValueData::F64Value(counts.0)),
321 pb_value(ValueData::F64Value(counts.1)),
322 f64_list_value(histogram.positive_counts.iter().copied()),
323 f64_list_value(histogram.negative_counts.iter().copied()),
324 ]);
325 } else {
326 let positive_buckets = bucket_counts_from_deltas(&histogram.positive_deltas)?;
327 let negative_buckets = bucket_counts_from_deltas(&histogram.negative_deltas)?;
328 items.extend([
329 pb_value(ValueData::U64Value(int_counts.0)),
330 pb_value(ValueData::U64Value(int_counts.1)),
331 i64_list_value(positive_buckets.iter().copied()),
332 i64_list_value(negative_buckets.iter().copied()),
333 null_pb_value(),
334 null_pb_value(),
335 f64_list_value(std::iter::empty()),
336 f64_list_value(std::iter::empty()),
337 ]);
338 }
339
340 Ok(ValueData::StructValue(api::v1::StructValue { items }))
341}
342
343fn pb_value(value_data: ValueData) -> Value {
344 optional_pb_value(Some(value_data))
345}
346
347fn null_pb_value() -> Value {
348 optional_pb_value(None)
349}
350
351fn optional_pb_value(value_data: Option<ValueData>) -> Value {
352 Value { value_data }
353}
354
355fn list_value(values: impl IntoIterator<Item = ValueData>) -> Value {
356 pb_value(ValueData::ListValue(ListValue {
357 items: values.into_iter().map(pb_value).collect(),
358 }))
359}
360
361fn i32_list_value(values: impl IntoIterator<Item = i32>) -> Value {
362 list_value(values.into_iter().map(ValueData::I32Value))
363}
364
365fn u32_list_value(values: impl IntoIterator<Item = u32>) -> Value {
366 list_value(values.into_iter().map(ValueData::U32Value))
367}
368
369fn i64_list_value(values: impl IntoIterator<Item = i64>) -> Value {
370 list_value(values.into_iter().map(ValueData::I64Value))
371}
372
373fn f64_list_value(values: impl IntoIterator<Item = f64>) -> Value {
374 list_value(values.into_iter().map(ValueData::F64Value))
375}
376
377fn bucket_counts_from_deltas(deltas: &[i64]) -> Result<Vec<i64>> {
378 let mut count = 0_i64;
379 let mut buckets = Vec::with_capacity(deltas.len());
380
381 for delta in deltas {
382 count = count
383 .checked_add(*delta)
384 .context(error::InvalidPromRemoteRequestSnafu {
385 msg: "remote write v2 native histogram bucket count overflows i64".to_string(),
386 })?;
387 ensure!(
388 count >= 0,
389 error::InvalidPromRemoteRequestSnafu {
390 msg: "remote write v2 native histogram bucket count is negative".to_string(),
391 }
392 );
393 buckets.push(count);
394 }
395
396 Ok(buckets)
397}
398
399fn ensure_no_internal_histogram_labels(tags: &PromTags) -> Result<()> {
400 for (name, _) in tags {
402 ensure!(
403 name != NATIVE_HISTOGRAM_FIELD,
404 error::InvalidPromRemoteRequestSnafu {
405 msg: format!(
406 "remote write v2 label `{name}` conflicts with an internal native histogram label"
407 ),
408 }
409 );
410 }
411
412 Ok(())
413}
414
415fn resolve_series_labels(symbols: &[String], series: &TimeSeries) -> Result<ResolvedSeriesLabels> {
416 ensure!(
417 series.labels_refs.len().is_multiple_of(2),
418 error::InvalidPromRemoteRequestSnafu {
419 msg: "remote write v2 labels_refs must contain name/value pairs".to_string(),
420 }
421 );
422
423 let mut prom_ctx = PromCtx::default();
424 let mut table_name = None;
425 let mut tags = Vec::with_capacity(series.labels_refs.len() / 2);
426 let mut label_names = HashSet::with_capacity(series.labels_refs.len() / 2);
427
428 for pair in series.labels_refs.chunks_exact(2) {
429 let name = symbol_ref(symbols, pair[0], "label name")?;
430 let value = symbol_ref(symbols, pair[1], "label value")?;
431 validate_label(name)?;
432 ensure!(
433 label_names.insert(name),
434 error::InvalidPromRemoteRequestSnafu {
435 msg: format!("remote write v2 label name `{name}` is repeated"),
436 }
437 );
438
439 if name == METRIC_NAME_LABEL {
440 table_name = Some(value.to_string());
441 continue;
442 }
443 if apply_remote_write_special_label(name, value, &mut prom_ctx) {
444 continue;
445 }
446
447 tags.push((name.to_string(), value.to_string()));
448 }
449
450 let table_name = table_name.context(error::InvalidPromRemoteRequestSnafu {
451 msg: "missing '__name__' label in time-series".to_string(),
452 })?;
453 ensure!(
454 !table_name.is_empty(),
455 error::InvalidPromRemoteRequestSnafu {
456 msg: "remote write v2 label `__name__` value must not be empty".to_string(),
457 }
458 );
459
460 Ok((prom_ctx, table_name, tags))
461}
462
463fn validate_label(name: &str) -> Result<()> {
464 ensure!(
465 validate_label_name(name.as_bytes()),
466 error::InvalidPromRemoteRequestSnafu {
467 msg: format!("remote write v2 invalid label name `{name}`"),
468 }
469 );
470
471 Ok(())
472}
473
474fn symbol_ref<'a>(symbols: &'a [String], idx: u32, field: &str) -> Result<&'a str> {
475 symbols
476 .get(idx as usize)
477 .map(String::as_str)
478 .with_context(|| error::InvalidPromRemoteRequestSnafu {
479 msg: format!(
480 "remote write v2 {field} symbol reference {idx} is out of range, symbols len: {}",
481 symbols.len()
482 ),
483 })
484}
485
486#[allow(deprecated)]
487fn apply_remote_write_special_label(name: &str, value: &str, prom_ctx: &mut PromCtx) -> bool {
488 match name {
489 SCHEMA_LABEL => {
490 prom_ctx.schema = Some(value.to_string());
491 true
492 }
493 DATABASE_LABEL | DATABASE_LABEL_ALT => {
494 if prom_ctx.schema.is_none() {
495 prom_ctx.schema = Some(value.to_string());
496 }
497 true
498 }
499 PHYSICAL_TABLE_LABEL | PHYSICAL_TABLE_LABEL_ALT => {
500 prom_ctx.physical_table = Some(value.to_string());
501 true
502 }
503 _ => false,
504 }
505}
506
507fn into_context_req(tables: HashMap<PromCtx, HashMap<String, TableData>>) -> ContextReq {
508 let mut ctx_req = ContextReq::default();
509 for (prom_ctx, tables) in tables {
510 let mut opt = ContextOpt::default();
511 if let Some(schema) = prom_ctx.schema {
512 opt.set_schema(schema);
513 }
514 if let Some(physical_table) = prom_ctx.physical_table {
515 opt.set_physical_table(physical_table);
516 }
517
518 ctx_req.add_rows(
519 opt,
520 tables.into_iter().map(|(table_name, table_data)| {
521 table_data_to_row_insert_request(table_name, table_data)
522 }),
523 );
524 }
525 ctx_req
526}
527
528fn table_data_to_row_insert_request(table_name: String, table_data: TableData) -> RowInsertRequest {
529 let num_columns = table_data.num_columns();
530 let (schema, mut rows) = table_data.into_schema_and_rows();
531 for row in &mut rows {
532 if num_columns > row.values.len() {
533 row.values.resize(num_columns, Value { value_data: None });
534 }
535 }
536
537 RowInsertRequest {
538 table_name,
539 rows: Some(Rows { schema, rows }),
540 }
541}
542
543#[cfg(any(test, feature = "testing"))]
544pub mod test_util {
545 use api::greptime_proto::io::prometheus::write::v2::{Histogram, Request, Sample, TimeSeries};
546 use api::v1::RowInsertRequest;
547 use bytes::Bytes;
548
549 use crate::error::Result;
550
551 pub fn request_with_labels_and_samples(
552 labels: Vec<(&str, &str)>,
553 samples: Vec<Sample>,
554 ) -> Request {
555 request_with_labels(labels, samples, Vec::new())
556 }
557
558 pub fn request_with_labels_and_histograms(
559 labels: Vec<(&str, &str)>,
560 histograms: Vec<Histogram>,
561 ) -> Request {
562 request_with_labels(labels, Vec::new(), histograms)
563 }
564
565 pub fn decode_request(is_zstd: bool, body: Bytes) -> Result<Request> {
566 super::decode_remote_write_v2_request(is_zstd, body)
567 }
568
569 pub fn write_requests(
570 request: Request,
571 ) -> Result<(Vec<RowInsertRequest>, Vec<RowInsertRequest>, u64, u64)> {
572 let requests = super::into_write_requests(request)?;
573 Ok((
574 requests.samples.all_req().collect(),
575 requests.histograms.all_req().collect(),
576 requests.sample_count,
577 requests.histogram_count,
578 ))
579 }
580
581 pub fn histogram(timestamp: i64) -> Histogram {
582 Histogram {
583 timestamp,
584 ..Default::default()
585 }
586 }
587
588 fn request_with_labels(
589 labels: Vec<(&str, &str)>,
590 samples: Vec<Sample>,
591 histograms: Vec<Histogram>,
592 ) -> Request {
593 let mut symbols = vec!["".to_string()];
594 let mut labels_refs = Vec::with_capacity(labels.len() * 2);
595 for (name, value) in labels {
596 labels_refs.push(push_symbol(&mut symbols, name));
597 labels_refs.push(push_symbol(&mut symbols, value));
598 }
599
600 Request {
601 symbols,
602 timeseries: vec![TimeSeries {
603 labels_refs,
604 samples,
605 histograms,
606 exemplars: Vec::new(),
607 metadata: None,
608 }],
609 }
610 }
611
612 fn push_symbol(symbols: &mut Vec<String>, symbol: &str) -> u32 {
613 if let Some(idx) = symbols.iter().position(|s| s == symbol) {
614 return idx as u32;
615 }
616
617 let idx = symbols.len();
618 symbols.push(symbol.to_string());
619 idx as u32
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use std::sync::Arc;
626
627 use api::v1::value::ValueData;
628 use common_query::prelude::{greptime_timestamp, greptime_value};
629 use session::context::QueryContext;
630
631 use super::*;
632 use crate::error;
633 use crate::http::prom_store::PHYSICAL_TABLE_PARAM;
634 use crate::prom_store::{DATABASE_LABEL, PHYSICAL_TABLE_LABEL};
635
636 #[test]
637 fn test_decode_remote_write_v2_request() {
638 let request = Request {
639 symbols: vec![
640 "".to_string(),
641 "__name__".to_string(),
642 "http_requests_total".to_string(),
643 ],
644 timeseries: vec![TimeSeries {
645 labels_refs: vec![1, 2],
646 samples: vec![Sample {
647 value: 42.0,
648 timestamp: 1000,
649 start_timestamp: 0,
650 }],
651 histograms: Vec::new(),
652 exemplars: Vec::new(),
653 metadata: Some(Metadata {
654 r#type: metadata::MetricType::Counter as i32,
655 help_ref: 0,
656 unit_ref: 0,
657 }),
658 }],
659 };
660 let body =
661 Bytes::from(crate::prom_store::snappy_compress(&request.encode_to_vec()).unwrap());
662
663 let decoded = decode_remote_write_v2_request(false, body).unwrap();
664
665 assert_eq!(decoded.symbols, request.symbols);
666 assert_eq!(decoded.timeseries.len(), 1);
667 assert_eq!(decoded.timeseries[0].labels_refs, vec![1, 2]);
668 assert_eq!(decoded.timeseries[0].samples.len(), 1);
669 assert_eq!(decoded.timeseries[0].samples[0].value, 42.0);
670 assert_eq!(decoded.timeseries[0].metadata.as_ref().unwrap().r#type, 1);
671 }
672
673 #[test]
674 fn test_into_context_req_samples() {
675 let ctx_req = into_write_requests(test_util::request_with_labels_and_samples(
676 vec![
677 (METRIC_NAME_LABEL, "http_requests_total"),
678 ("job", "api"),
679 ("instance", "localhost:9090"),
680 ],
681 vec![
682 Sample {
683 value: 42.0,
684 timestamp: 1000,
685 start_timestamp: 0,
686 },
687 Sample {
688 value: 43.0,
689 timestamp: 2000,
690 start_timestamp: 0,
691 },
692 ],
693 ))
694 .unwrap();
695
696 assert_eq!(ctx_req.sample_count, 2);
697 assert_eq!(ctx_req.histogram_count, 0);
698 assert_eq!(ctx_req.histograms.all_req().count(), 0);
699 let mut inserts = ctx_req.samples.all_req().collect::<Vec<_>>();
700 assert_eq!(inserts.len(), 1);
701
702 let request = inserts.pop().unwrap();
703 assert_eq!(request.table_name, "http_requests_total");
704 let rows = request.rows.unwrap();
705 assert_eq!(rows.rows.len(), 2);
706 assert_eq!(
707 rows.schema
708 .iter()
709 .map(|col| col.column_name.as_str())
710 .collect::<Vec<_>>(),
711 vec![greptime_timestamp(), greptime_value(), "job", "instance"]
712 );
713 assert_eq!(
714 rows.rows[0].values[0].value_data,
715 Some(ValueData::TimestampMillisecondValue(1000))
716 );
717 assert_eq!(
718 rows.rows[0].values[1].value_data,
719 Some(ValueData::F64Value(42.0))
720 );
721 assert_eq!(
722 rows.rows[0].values[2].value_data,
723 Some(ValueData::StringValue("api".to_string()))
724 );
725 assert_eq!(
726 rows.rows[0].values[3].value_data,
727 Some(ValueData::StringValue("localhost:9090".to_string()))
728 );
729 assert_eq!(
730 rows.rows[1].values[0].value_data,
731 Some(ValueData::TimestampMillisecondValue(2000))
732 );
733 assert_eq!(
734 rows.rows[1].values[1].value_data,
735 Some(ValueData::F64Value(43.0))
736 );
737 }
738
739 #[test]
740 fn test_into_context_req_special_labels() {
741 let ctx_req = into_write_requests(test_util::request_with_labels_and_samples(
742 vec![
743 (METRIC_NAME_LABEL, "cpu_usage"),
744 (DATABASE_LABEL, "tenant_a"),
745 (PHYSICAL_TABLE_LABEL, "metrics_physical"),
746 ("job", "api"),
747 ],
748 vec![Sample {
749 value: 1.0,
750 timestamp: 1000,
751 start_timestamp: 0,
752 }],
753 ))
754 .unwrap();
755
756 let mut iter = ctx_req
757 .samples
758 .as_req_iter(Arc::new(QueryContext::with("greptime", "public")));
759 let (ctx, reqs) = iter.next().unwrap();
760 assert!(iter.next().is_none());
761
762 assert_eq!(ctx.current_schema(), "tenant_a");
763 assert_eq!(
764 ctx.extension(PHYSICAL_TABLE_PARAM),
765 Some("metrics_physical")
766 );
767 assert_eq!(reqs.inserts.len(), 1);
768
769 let rows = reqs.inserts[0].rows.as_ref().unwrap();
770 assert_eq!(
771 rows.schema
772 .iter()
773 .map(|col| col.column_name.as_str())
774 .collect::<Vec<_>>(),
775 vec![greptime_timestamp(), greptime_value(), "job"]
776 );
777 }
778
779 #[test]
780 fn test_into_context_req_rejects_invalid_requests() {
781 let mut cases = Vec::new();
782
783 cases.push((
784 "missing metric name",
785 request_with_sample(vec![("job", "api")]),
786 "missing '__name__'",
787 ));
788
789 let mut request = request_with_sample(vec![(METRIC_NAME_LABEL, "metric")]);
790 request.timeseries[0].labels_refs.push(1);
791 cases.push((
792 "odd label refs",
793 request,
794 "labels_refs must contain name/value pairs",
795 ));
796
797 let mut request = request_with_sample(vec![(METRIC_NAME_LABEL, "metric")]);
798 request.timeseries[0].labels_refs[1] = 99;
799 cases.push((
800 "out of range symbol ref",
801 request,
802 "symbol reference 99 is out of range",
803 ));
804
805 let mut request = request_with_sample(vec![(METRIC_NAME_LABEL, "metric")]);
806 request.symbols[0] = "not-empty".to_string();
807 cases.push((
808 "non-empty first symbol",
809 request,
810 "symbols must start with an empty string",
811 ));
812
813 cases.push((
814 "repeated label name",
815 request_with_sample(vec![
816 (METRIC_NAME_LABEL, "metric"),
817 ("job", "api"),
818 ("job", "worker"),
819 ]),
820 "label name `job` is repeated",
821 ));
822
823 cases.push((
824 "empty label name",
825 request_with_sample(vec![(METRIC_NAME_LABEL, "metric"), ("", "api")]),
826 "invalid label name",
827 ));
828
829 cases.push((
830 "invalid label name",
831 request_with_sample(vec![(METRIC_NAME_LABEL, "metric"), ("has-dash", "api")]),
832 "invalid label name",
833 ));
834
835 cases.push((
836 "dotted label name",
837 request_with_sample(vec![(METRIC_NAME_LABEL, "metric"), ("service.name", "api")]),
838 "invalid label name",
839 ));
840
841 cases.push((
842 "non-ascii label name",
843 request_with_sample(vec![(METRIC_NAME_LABEL, "metric"), ("区域", "api")]),
844 "invalid label name",
845 ));
846
847 cases.push((
848 "empty metric name",
849 request_with_sample(vec![(METRIC_NAME_LABEL, "")]),
850 "label `__name__` value must not be empty",
851 ));
852
853 cases.push((
854 "internal histogram label on samples",
855 request_with_sample(vec![
856 (METRIC_NAME_LABEL, "metric"),
857 (NATIVE_HISTOGRAM_FIELD, "user_value"),
858 ]),
859 "conflicts with an internal native histogram label",
860 ));
861
862 for (name, request, expected) in cases {
863 assert_invalid(name, request, expected);
864 }
865 }
866
867 #[test]
868 fn test_into_context_req_allows_empty_label_values() {
869 let ctx_req = into_write_requests(test_util::request_with_labels_and_samples(
870 vec![(METRIC_NAME_LABEL, "metric"), ("job", "")],
871 vec![Sample {
872 value: 1.0,
873 timestamp: 1000,
874 start_timestamp: 0,
875 }],
876 ))
877 .unwrap();
878
879 let rows = ctx_req.samples.all_req().next().unwrap().rows.unwrap();
880 let job_idx = column_index(&rows.schema, "job");
881 assert_eq!(
882 rows.rows[0].values[job_idx].value_data,
883 Some(ValueData::StringValue(String::new()))
884 );
885 }
886
887 #[test]
888 fn test_into_context_req_rejects_same_metric_samples_and_histograms() {
889 let mut request = test_util::request_with_labels_and_samples(
890 vec![(METRIC_NAME_LABEL, "metric")],
891 vec![Sample {
892 value: 1.0,
893 timestamp: 1000,
894 start_timestamp: 0,
895 }],
896 );
897 request.timeseries[0].histograms.push(Histogram::default());
898
899 assert_invalid(
900 "same metric samples and histograms",
901 request,
902 "contains both samples and native histograms",
903 );
904 }
905
906 #[test]
907 fn test_into_context_req_converts_histograms_and_ignores_exemplars() {
908 let request = Request {
909 symbols: vec![
910 "".to_string(),
911 METRIC_NAME_LABEL.to_string(),
912 "sample_metric".to_string(),
913 "histogram_metric".to_string(),
914 ],
915 timeseries: vec![
916 TimeSeries {
917 labels_refs: vec![1, 2],
918 samples: vec![Sample {
919 value: 1.0,
920 timestamp: 1000,
921 start_timestamp: 0,
922 }],
923 ..Default::default()
924 },
925 TimeSeries {
926 labels_refs: vec![1, 3],
927 histograms: vec![Histogram::default()],
928 exemplars: vec![Exemplar::default()],
929 ..Default::default()
930 },
931 ],
932 };
933
934 let ctx_req = into_write_requests(request).unwrap();
935
936 assert_eq!(ctx_req.sample_count, 1);
937 assert_eq!(ctx_req.histogram_count, 1);
938 assert_eq!(ctx_req.samples.all_req().count(), 1);
939 assert_eq!(ctx_req.histograms.all_req().count(), 1);
940 }
941
942 #[test]
943 fn test_into_context_req_converts_histogram_only_series() {
944 let mut request =
945 test_util::request_with_labels_and_samples(vec![(METRIC_NAME_LABEL, "metric")], vec![]);
946 request.timeseries[0].histograms.push(Histogram::default());
947
948 let ctx_req = into_write_requests(request).unwrap();
949
950 assert_eq!(ctx_req.sample_count, 0);
951 assert_eq!(ctx_req.histogram_count, 1);
952 assert_eq!(ctx_req.samples.all_req().count(), 0);
953 let mut inserts = ctx_req.histograms.all_req().collect::<Vec<_>>();
954 assert_eq!(inserts.len(), 1);
955
956 let request = inserts.pop().unwrap();
957 assert_eq!(request.table_name, "metric");
958 let rows = request.rows.unwrap();
959 assert_eq!(rows.rows.len(), 1);
960 assert_eq!(
961 rows.schema
962 .iter()
963 .map(|col| col.column_name.as_str())
964 .collect::<Vec<_>>(),
965 vec![greptime_timestamp(), NATIVE_HISTOGRAM_FIELD]
966 );
967 assert_eq!(
968 rows.rows[0].values[0].value_data,
969 Some(ValueData::TimestampMillisecondValue(0))
970 );
971 assert_eq!(
972 histogram_field_value(&rows, 0, SCHEMA_FIELD),
973 Some(ValueData::I32Value(0))
974 );
975 assert_eq!(
976 histogram_field_value(&rows, 0, COUNT_U64_FIELD),
977 Some(ValueData::U64Value(0))
978 );
979 assert_eq!(histogram_field_value(&rows, 0, COUNT_F64_FIELD), None);
980 }
981
982 #[test]
983 fn test_into_context_req_preserves_histogram_start_timestamp() {
984 let ctx_req = into_write_requests(test_util::request_with_labels_and_histograms(
985 vec![(METRIC_NAME_LABEL, "metric")],
986 vec![Histogram {
987 timestamp: 2000,
988 start_timestamp: 1000,
989 ..Default::default()
990 }],
991 ))
992 .unwrap();
993
994 let mut inserts = ctx_req.histograms.all_req().collect::<Vec<_>>();
995 let rows = inserts.pop().unwrap().rows.unwrap();
996
997 assert_eq!(
998 histogram_field_value(&rows, 0, START_TIMESTAMP_FIELD),
999 Some(ValueData::TimestampMillisecondValue(1000))
1000 );
1001 }
1002
1003 #[test]
1004 fn test_into_context_req_rejects_internal_histogram_labels() {
1005 let mut request = test_util::request_with_labels_and_samples(
1006 vec![
1007 (METRIC_NAME_LABEL, "metric"),
1008 (NATIVE_HISTOGRAM_FIELD, "user_value"),
1009 ],
1010 vec![],
1011 );
1012 request.timeseries[0].histograms.push(Histogram::default());
1013
1014 let err = match into_write_requests(request) {
1015 Ok(_) => panic!("expected invalid request error"),
1016 Err(err) => err,
1017 };
1018 assert_eq!(
1019 err.to_string(),
1020 "Invalid prometheus remote request, msg: remote write v2 label `greptime_native_histogram` conflicts with an internal native histogram label"
1021 );
1022 }
1023
1024 #[test]
1025 fn test_into_context_req_converts_int_and_float_histograms_to_one_schema() {
1026 let float_histogram = Histogram {
1027 count: Some(api::greptime_proto::io::prometheus::write::v2::histogram::Count::CountFloat(3.5)),
1028 zero_count: Some(
1029 api::greptime_proto::io::prometheus::write::v2::histogram::ZeroCount::ZeroCountFloat(
1030 0.5,
1031 ),
1032 ),
1033 positive_counts: vec![2.0, 3.5],
1034 positive_spans: vec![api::greptime_proto::io::prometheus::write::v2::BucketSpan {
1035 offset: 3,
1036 length: 2,
1037 }],
1038 timestamp: 2000,
1039 ..Default::default()
1040 };
1041 let request = Request {
1042 symbols: vec![
1043 "".to_string(),
1044 METRIC_NAME_LABEL.to_string(),
1045 "metric".to_string(),
1046 ],
1047 timeseries: vec![
1048 TimeSeries {
1049 labels_refs: vec![1, 2],
1050 histograms: vec![test_util::histogram(1000)],
1051 ..Default::default()
1052 },
1053 TimeSeries {
1054 labels_refs: vec![1, 2],
1055 histograms: vec![float_histogram],
1056 ..Default::default()
1057 },
1058 ],
1059 };
1060
1061 let ctx_req = into_write_requests(request).unwrap();
1062
1063 assert_eq!(ctx_req.histogram_count, 2);
1064 let mut inserts = ctx_req.histograms.all_req().collect::<Vec<_>>();
1065 assert_eq!(inserts.len(), 1);
1066 let rows = inserts.pop().unwrap().rows.unwrap();
1067 assert_eq!(rows.rows.len(), 2);
1068 assert_eq!(
1069 rows.schema
1070 .iter()
1071 .map(|col| col.column_name.as_str())
1072 .collect::<Vec<_>>(),
1073 vec![greptime_timestamp(), NATIVE_HISTOGRAM_FIELD]
1074 );
1075
1076 assert_eq!(
1077 histogram_field_value(&rows, 0, COUNT_U64_FIELD),
1078 Some(ValueData::U64Value(0))
1079 );
1080 assert_eq!(histogram_field_value(&rows, 0, COUNT_F64_FIELD), None);
1081 assert!(matches!(
1082 histogram_field_value(&rows, 0, POSITIVE_BUCKETS_I64_FIELD),
1083 Some(ValueData::ListValue(_))
1084 ));
1085 assert!(is_empty_list(histogram_field_value(
1086 &rows,
1087 0,
1088 POSITIVE_BUCKETS_F64_FIELD
1089 )));
1090
1091 assert_eq!(histogram_field_value(&rows, 1, COUNT_U64_FIELD), None);
1092 assert_eq!(
1093 histogram_field_value(&rows, 1, COUNT_F64_FIELD),
1094 Some(ValueData::F64Value(3.5))
1095 );
1096 assert!(is_empty_list(histogram_field_value(
1097 &rows,
1098 1,
1099 POSITIVE_BUCKETS_I64_FIELD
1100 )));
1101 assert!(matches!(
1102 histogram_field_value(&rows, 1, POSITIVE_BUCKETS_F64_FIELD),
1103 Some(ValueData::ListValue(_))
1104 ));
1105 }
1106
1107 fn request_with_sample(labels: Vec<(&str, &str)>) -> Request {
1108 test_util::request_with_labels_and_samples(
1109 labels,
1110 vec![Sample {
1111 value: 1.0,
1112 timestamp: 1000,
1113 start_timestamp: 0,
1114 }],
1115 )
1116 }
1117
1118 fn assert_invalid(name: &str, request: Request, expected: &str) {
1119 let err = match into_write_requests(request) {
1120 Ok(_) => panic!("{name}: expected invalid request error"),
1121 Err(err) => err,
1122 };
1123 assert!(
1124 matches!(err, error::Error::InvalidPromRemoteRequest { .. }),
1125 "{name}: expected invalid request error, got {err}"
1126 );
1127 assert!(
1128 err.to_string().contains(expected),
1129 "{name}: expected error containing {expected:?}, got {err}"
1130 );
1131 }
1132
1133 fn column_index(schema: &[ColumnSchema], column_name: &str) -> usize {
1134 schema
1135 .iter()
1136 .position(|column| column.column_name == column_name)
1137 .unwrap()
1138 }
1139
1140 fn histogram_field_value(rows: &Rows, row_idx: usize, field_name: &str) -> Option<ValueData> {
1141 let histogram_idx = column_index(&rows.schema, NATIVE_HISTOGRAM_FIELD);
1142 let Some(ValueData::StructValue(histogram)) =
1143 &rows.rows[row_idx].values[histogram_idx].value_data
1144 else {
1145 panic!("expected native histogram struct value");
1146 };
1147 let field_idx = NATIVE_HISTOGRAM_FIELD_NAMES
1148 .iter()
1149 .position(|name| *name == field_name)
1150 .unwrap();
1151 histogram.items[field_idx].value_data.clone()
1152 }
1153
1154 fn is_empty_list(value: Option<ValueData>) -> bool {
1155 matches!(value, Some(ValueData::ListValue(list)) if list.items.is_empty())
1156 }
1157}