Skip to main content

servers/prom_remote_write/
v2.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 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    // Match the v1 decoder's VictoriaMetrics fallback: some clients may send a
51    // mismatched content-encoding header, so try the other compression on failure.
52    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
68/// Converts a PRW v2 request into normal sample writes and native histogram writes.
69///
70/// A metric name may appear in only one payload kind per request because the
71/// metric-engine logical table is either a float metric or a native histogram.
72pub(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        // Exemplars are intentionally ignored for now.
95        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            // Fast path for regular sample-only series. Move the resolved labels into
129            // the sample writer instead of cloning them for a histogram path we won't use.
130            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            // The owned labels were moved above, so skip the mixed-series path below.
141            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    // Persist both int and float families into the logical table schema. Only one
232    // family is populated per row; the other is written as NULL so PromQL can
233    // infer the original histogram flavor without a separate type column.
234    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    // The histogram field column is generated from the protobuf payload.
401    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}