Skip to main content

servers/http/
prom_store.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::sync::Arc;
16
17use api::prom_store::remote::ReadRequest;
18use api::v1::RowInsertRequests;
19use async_trait::async_trait;
20use axum::Extension;
21use axum::body::Bytes;
22use axum::extract::{Query, State};
23use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
24use axum::response::IntoResponse;
25use axum_extra::TypedHeader;
26use common_catalog::consts::DEFAULT_SCHEMA_NAME;
27use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
28use common_telemetry::tracing;
29use mime_guess::mime;
30use pipeline::util::to_pipeline_version;
31use pipeline::{ContextReq, PipelineDefinition};
32use prometheus::HistogramTimer;
33use prost::Message;
34use serde::{Deserialize, Serialize};
35use session::context::{Channel, QueryContext, QueryContextRef};
36use snafu::prelude::*;
37use table::requests::{
38    METADATA_QUALITY_INFERRED, SEMANTIC_METRIC_METADATA_QUALITY, SEMANTIC_PER_TABLE_INDEX_KEY,
39    SEMANTIC_SIGNAL_TYPE, SEMANTIC_SOURCE, SEMANTIC_SOURCE_VERSION, SIGNAL_TYPE_METRIC,
40    SOURCE_PROMETHEUS,
41};
42
43use crate::error::{self, InternalSnafu, PipelineSnafu, Result};
44use crate::http::extractor::PipelineInfo;
45use crate::http::header::{
46    CONTENT_TYPE_PROTOBUF_STR, GREPTIME_DB_HEADER_METRICS, write_cost_header_map,
47};
48use crate::pending_rows_batcher::PendingRowsBatcher;
49use crate::prom_remote_write::decode::PromSeriesProcessor;
50use crate::prom_remote_write::v2::decode_remote_write_v2;
51use crate::prom_remote_write::validation::PromValidationMode;
52use crate::prom_remote_write::{
53    REMOTE_WRITE_V1_VERSION, REMOTE_WRITE_V2_VERSION, decode_remote_write_request,
54};
55use crate::prom_store::snappy_decompress;
56use crate::query_handler::{PipelineHandlerRef, PromStoreProtocolHandlerRef, PromStoreResponse};
57
58pub const PHYSICAL_TABLE_PARAM: &str = "physical_table";
59pub const DEFAULT_ENCODING: &str = "snappy";
60pub const VM_ENCODING: &str = "zstd";
61pub const VM_PROTO_VERSION: &str = "1";
62const REMOTE_WRITE_V1_PROTO: &str = "prometheus.WriteRequest";
63const REMOTE_WRITE_V2_PROTO: &str = "io.prometheus.write.v2.Request";
64const CONTENT_TYPE_PROTO_PARAM: &str = "proto";
65const REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER: &str = "x-prometheus-remote-write-samples-written";
66const REMOTE_WRITE_V2_HISTOGRAMS_WRITTEN_HEADER: &str =
67    "x-prometheus-remote-write-histograms-written";
68const REMOTE_WRITE_V2_EXEMPLARS_WRITTEN_HEADER: &str =
69    "x-prometheus-remote-write-exemplars-written";
70
71#[derive(Clone)]
72pub struct PromStoreState {
73    pub prom_store_handler: PromStoreProtocolHandlerRef,
74    pub pipeline_handler: Option<PipelineHandlerRef>,
75    pub prom_store_with_metric_engine: bool,
76    pub prom_validation_mode: PromValidationMode,
77    pub experimental_enable_prometheus_native_histogram: bool,
78    pub pending_rows_batcher: Option<Arc<PendingRowsBatcher>>,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
82pub struct RemoteWriteQuery {
83    pub db: Option<String>,
84    /// Specify which physical table to use for storing metrics.
85    /// This only works on remote write requests.
86    pub physical_table: Option<String>,
87    /// For VictoriaMetrics modified remote write protocol
88    pub get_vm_proto_version: Option<String>,
89}
90
91impl Default for RemoteWriteQuery {
92    fn default() -> RemoteWriteQuery {
93        Self {
94            db: Some(DEFAULT_SCHEMA_NAME.to_string()),
95            physical_table: Some(GREPTIME_PHYSICAL_TABLE.to_string()),
96            get_vm_proto_version: None,
97        }
98    }
99}
100
101#[axum_macros::debug_handler]
102#[tracing::instrument(
103    skip_all,
104    fields(protocol = "prometheus", request_type = "remote_write")
105)]
106pub async fn remote_write(
107    State(state): State<PromStoreState>,
108    Query(params): Query<RemoteWriteQuery>,
109    Extension(query_ctx): Extension<QueryContext>,
110    content_type: Option<TypedHeader<headers::ContentType>>,
111    pipeline_info: PipelineInfo,
112    content_encoding: TypedHeader<headers::ContentEncoding>,
113    body: Bytes,
114) -> Result<axum::response::Response> {
115    let is_zstd = content_encoding.contains(VM_ENCODING);
116
117    match remote_write_proto(content_type) {
118        RemoteWriteProto::V1 => {
119            remote_write_v1(state, params, query_ctx, pipeline_info, is_zstd, body).await
120        }
121        RemoteWriteProto::V2 => {
122            if let Some(response) = unsupported_remote_write_v2_encoding_response(&content_encoding)
123            {
124                return Ok(response);
125            }
126            remote_write_v2(state, params, query_ctx, pipeline_info, is_zstd, body).await
127        }
128        RemoteWriteProto::Unsupported(content_type) => Ok((
129            StatusCode::UNSUPPORTED_MEDIA_TYPE,
130            format!("unsupported prometheus remote write content type: {content_type}"),
131        )
132            .into_response()),
133    }
134}
135
136async fn remote_write_v1(
137    state: PromStoreState,
138    params: RemoteWriteQuery,
139    query_ctx: QueryContext,
140    pipeline_info: PipelineInfo,
141    is_zstd: bool,
142    body: Bytes,
143) -> Result<axum::response::Response> {
144    let PromStoreState {
145        prom_store_handler,
146        pipeline_handler,
147        prom_store_with_metric_engine,
148        prom_validation_mode,
149        experimental_enable_prometheus_native_histogram: _,
150        pending_rows_batcher,
151    } = state;
152
153    if let Some(response) = vm_proto_version_response(&params) {
154        return Ok(response);
155    }
156
157    let (db, query_ctx, _timer) =
158        prepare_remote_write_context(&params, query_ctx, REMOTE_WRITE_V1_VERSION);
159    let query_ctx = Arc::new(query_ctx);
160
161    let mut processor = PromSeriesProcessor::default_processor();
162
163    if let Some(pipeline_name) = pipeline_info.pipeline_name {
164        let pipeline_def = PipelineDefinition::from_name(
165            &pipeline_name,
166            to_pipeline_version(pipeline_info.pipeline_version.as_deref())
167                .context(PipelineSnafu)?,
168            None,
169        )
170        .context(PipelineSnafu)?;
171        let pipeline_handler = pipeline_handler.context(InternalSnafu {
172            err_msg: "pipeline handler is not set".to_string(),
173        })?;
174
175        processor.set_pipeline(pipeline_handler, query_ctx.clone(), pipeline_def);
176    }
177
178    let mut req = decode_remote_write_request(is_zstd, body, prom_validation_mode, &mut processor)?;
179
180    let req = if processor.use_pipeline {
181        processor.exec_pipeline().await?
182    } else {
183        req.as_insert_requests()
184    };
185    let batches = into_prom_write_batches(req, query_ctx);
186
187    let outcome = match write_prometheus_rows_with_progress(
188        prom_store_handler,
189        pending_rows_batcher,
190        prom_store_with_metric_engine,
191        batches,
192    )
193    .await
194    {
195        Ok(outcome) => outcome,
196        Err(error) => {
197            record_remote_write_samples(&db, REMOTE_WRITE_V1_VERSION, error.rows_written);
198            return Err(error.error);
199        }
200    };
201    record_remote_write_samples(&db, REMOTE_WRITE_V1_VERSION, outcome.rows_written);
202
203    Ok((
204        StatusCode::NO_CONTENT,
205        write_cost_header_map(outcome.write_cost),
206    )
207        .into_response())
208}
209
210async fn remote_write_v2(
211    state: PromStoreState,
212    params: RemoteWriteQuery,
213    query_ctx: QueryContext,
214    pipeline_info: PipelineInfo,
215    is_zstd: bool,
216    body: Bytes,
217) -> Result<axum::response::Response> {
218    let PromStoreState {
219        prom_store_handler,
220        pipeline_handler: _,
221        prom_store_with_metric_engine,
222        prom_validation_mode: _,
223        experimental_enable_prometheus_native_histogram,
224        pending_rows_batcher,
225    } = state;
226
227    if let Some(response) = vm_proto_version_response(&params) {
228        return Ok(response);
229    }
230
231    // Pipeline processing is not supported for remote write v2 yet. Ignore the
232    // optional pipeline parameter and ingest samples directly.
233    let _ = pipeline_info;
234
235    let (db, mut query_ctx, _timer) =
236        prepare_remote_write_context(&params, query_ctx, REMOTE_WRITE_V2_VERSION);
237
238    let req = match decode_remote_write_v2(
239        is_zstd,
240        body,
241        experimental_enable_prometheus_native_histogram,
242    ) {
243        Ok(req) => req,
244        Err(error) => return Ok(remote_write_v2_error_response(error, 0, 0, 0)),
245    };
246    // The v2 per-series metadata upgrades the written tables' semantic options
247    // (metric type/unit, declared quality) at auto-create time.
248    if let Some(index) = req.semantic_index.encode(&query_ctx.current_schema()) {
249        query_ctx.set_extension(SEMANTIC_PER_TABLE_INDEX_KEY, index);
250    }
251    let query_ctx = Arc::new(query_ctx);
252    let sample_count = req.sample_count;
253    let histogram_count = req.histogram_count;
254    let sample_batches = into_prom_write_batches(req.samples, query_ctx.clone());
255    let histogram_batches = into_prom_write_batches(req.histograms, query_ctx);
256    let outcome = match write_prometheus_v2_rows_with_progress(
257        prom_store_handler,
258        pending_rows_batcher,
259        prom_store_with_metric_engine,
260        sample_batches,
261        histogram_batches,
262    )
263    .await
264    {
265        Ok(outcome) => outcome,
266        Err(error) => {
267            record_remote_write_samples(&db, REMOTE_WRITE_V2_VERSION, error.samples_written);
268            record_remote_write_histograms(&db, REMOTE_WRITE_V2_VERSION, error.histograms_written);
269            return Ok(remote_write_v2_error_response(
270                error.error,
271                error.samples_written,
272                error.histograms_written,
273                0,
274            ));
275        }
276    };
277    debug_assert_eq!(outcome.samples_written, sample_count);
278    debug_assert_eq!(outcome.histograms_written, histogram_count);
279    record_remote_write_samples(&db, REMOTE_WRITE_V2_VERSION, outcome.samples_written);
280    record_remote_write_histograms(&db, REMOTE_WRITE_V2_VERSION, outcome.histograms_written);
281
282    let mut headers = write_cost_header_map(outcome.write_cost);
283    append_remote_write_v2_written_headers(
284        &mut headers,
285        outcome.samples_written,
286        outcome.histograms_written,
287        0,
288    );
289
290    Ok((StatusCode::NO_CONTENT, headers).into_response())
291}
292
293fn vm_proto_version_response(params: &RemoteWriteQuery) -> Option<axum::response::Response> {
294    params
295        .get_vm_proto_version
296        .as_ref()
297        .map(|_| VM_PROTO_VERSION.into_response())
298}
299
300/// Returns the context still un-shared so the caller can attach
301/// request-derived extensions (the v2 per-table metadata index) before
302/// wrapping it in an `Arc`.
303fn prepare_remote_write_context(
304    params: &RemoteWriteQuery,
305    mut query_ctx: QueryContext,
306    remote_write_version: &str,
307) -> (String, QueryContext, HistogramTimer) {
308    let db = params.db.clone().unwrap_or_default();
309    query_ctx.set_channel(Channel::Prometheus);
310    let physical_table = params
311        .physical_table
312        .clone()
313        .unwrap_or_else(|| GREPTIME_PHYSICAL_TABLE.to_string());
314    query_ctx.set_extension(PHYSICAL_TABLE_PARAM, physical_table);
315    // Stamp the Prometheus metric identity here, before `as_req_iter` splits into the
316    // batched and direct write paths, so both inherit it (the batched path bypasses
317    // `PromStoreProtocolHandler::write`). Prometheus remote-write metadata is weak
318    // here, so the type is inferred from naming; v2 upgrades tables whose series
319    // carry inline metadata via the per-table index.
320    query_ctx.set_extension(SEMANTIC_SIGNAL_TYPE, SIGNAL_TYPE_METRIC);
321    query_ctx.set_extension(SEMANTIC_SOURCE, SOURCE_PROMETHEUS);
322    query_ctx.set_extension(SEMANTIC_SOURCE_VERSION, remote_write_version);
323    query_ctx.set_extension(SEMANTIC_METRIC_METADATA_QUALITY, METADATA_QUALITY_INFERRED);
324    let timer = crate::metrics::METRIC_HTTP_PROM_STORE_WRITE_ELAPSED
325        .with_label_values(&[db.as_str(), remote_write_version])
326        .start_timer();
327
328    (db, query_ctx, timer)
329}
330
331struct PromWriteOutcome {
332    write_cost: usize,
333    rows_written: u64,
334}
335
336struct PromWriteError {
337    error: error::Error,
338    rows_written: u64,
339}
340
341struct PromWriteV2Outcome {
342    write_cost: usize,
343    samples_written: u64,
344    histograms_written: u64,
345}
346
347struct PromWriteV2Error {
348    error: error::Error,
349    samples_written: u64,
350    histograms_written: u64,
351}
352
353type PromWriteBatch = (QueryContextRef, RowInsertRequests);
354
355#[async_trait]
356trait PromWriteBatcher: Send + Sync {
357    async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64>;
358}
359
360#[async_trait]
361impl PromWriteBatcher for PendingRowsBatcher {
362    async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
363        PendingRowsBatcher::submit(self, requests, ctx).await
364    }
365}
366
367fn into_prom_write_batches(req: ContextReq, query_ctx: QueryContextRef) -> Vec<PromWriteBatch> {
368    req.as_req_iter(query_ctx).collect()
369}
370
371async fn preflight_prometheus_rows(
372    prom_store_handler: &PromStoreProtocolHandlerRef,
373    batches: &mut [PromWriteBatch],
374) -> Result<()> {
375    for (ctx, reqs) in batches {
376        prom_store_handler.pre_write(reqs, ctx.clone()).await?;
377        // Detach from context clones retained by pre-write hooks so the checked
378        // schema cannot change before this prepared batch is written.
379        *ctx = Arc::new(ctx.fork());
380    }
381    Ok(())
382}
383
384/// Writes preflighted PRW batches and keeps the number of persisted rows on error.
385///
386/// The v2 handler uses that partial progress to return Prometheus' written
387/// sample/histogram headers even when a later table write fails.
388async fn write_prometheus_rows_with_progress(
389    prom_store_handler: PromStoreProtocolHandlerRef,
390    pending_rows_batcher: Option<Arc<PendingRowsBatcher>>,
391    prom_store_with_metric_engine: bool,
392    mut batches: Vec<PromWriteBatch>,
393) -> std::result::Result<PromWriteOutcome, PromWriteError> {
394    if prom_store_with_metric_engine && let Some(batcher) = pending_rows_batcher {
395        preflight_prometheus_rows(&prom_store_handler, &mut batches)
396            .await
397            .map_err(|error| PromWriteError {
398                error,
399                rows_written: 0,
400            })?;
401        let mut rows_written = 0;
402        for (temp_ctx, reqs) in batches {
403            let rows = batcher
404                .submit(reqs, temp_ctx)
405                .await
406                .map_err(|error| PromWriteError {
407                    error,
408                    rows_written,
409                })?;
410            rows_written += rows;
411        }
412        return Ok(PromWriteOutcome {
413            write_cost: 0,
414            rows_written,
415        });
416    }
417
418    let row_counts = batches
419        .iter()
420        .map(|(_, request)| prom_write_row_count(request))
421        .collect::<Vec<_>>();
422    let batch_count = batches.len();
423    let outputs = prom_store_handler
424        .write_all(batches, prom_store_with_metric_engine)
425        .await
426        .map_err(|error| PromWriteError {
427            error,
428            rows_written: 0,
429        })?;
430    let output_count = outputs.len();
431    let mut write_cost = 0;
432    let mut rows_written = 0;
433    for (output, rows) in outputs.into_iter().zip(row_counts) {
434        let output = output.map_err(|error| PromWriteError {
435            error,
436            rows_written,
437        })?;
438        write_cost += output.meta.cost;
439        rows_written += rows;
440    }
441    if output_count != batch_count {
442        return Err(PromWriteError {
443            error: incomplete_prom_write_error(),
444            rows_written,
445        });
446    }
447
448    Ok(PromWriteOutcome {
449        write_cost,
450        rows_written,
451    })
452}
453
454async fn write_prometheus_v2_rows_with_progress(
455    prom_store_handler: PromStoreProtocolHandlerRef,
456    pending_rows_batcher: Option<Arc<PendingRowsBatcher>>,
457    prom_store_with_metric_engine: bool,
458    sample_batches: Vec<PromWriteBatch>,
459    histogram_batches: Vec<PromWriteBatch>,
460) -> std::result::Result<PromWriteV2Outcome, PromWriteV2Error> {
461    if histogram_batches.is_empty() {
462        return write_prometheus_rows_with_progress(
463            prom_store_handler,
464            pending_rows_batcher,
465            prom_store_with_metric_engine,
466            sample_batches,
467        )
468        .await
469        .map(|outcome| PromWriteV2Outcome {
470            write_cost: outcome.write_cost,
471            samples_written: outcome.rows_written,
472            histograms_written: 0,
473        })
474        .map_err(|error| PromWriteV2Error {
475            error: error.error,
476            samples_written: error.rows_written,
477            histograms_written: 0,
478        });
479    }
480
481    if prom_store_with_metric_engine && let Some(batcher) = pending_rows_batcher {
482        return write_batched_prometheus_v2_rows_with_progress(
483            prom_store_handler,
484            batcher.as_ref(),
485            prom_store_with_metric_engine,
486            sample_batches,
487            histogram_batches,
488        )
489        .await;
490    }
491
492    let sample_batch_count = sample_batches.len();
493    let mut batches = sample_batches;
494    batches.extend(histogram_batches);
495    let row_counts = batches
496        .iter()
497        .map(|(_, request)| prom_write_row_count(request))
498        .collect::<Vec<_>>();
499    let batch_count = batches.len();
500    let outputs = prom_store_handler
501        .write_all(batches, prom_store_with_metric_engine)
502        .await
503        .map_err(|error| PromWriteV2Error {
504            error,
505            samples_written: 0,
506            histograms_written: 0,
507        })?;
508
509    let mut write_cost = 0;
510    let mut samples_written = 0;
511    let mut histograms_written = 0;
512    let mut output_count = 0;
513    for (index, (output, rows)) in outputs.into_iter().zip(row_counts).enumerate() {
514        let output = output.map_err(|error| PromWriteV2Error {
515            error,
516            samples_written,
517            histograms_written,
518        })?;
519        write_cost += output.meta.cost;
520        if index < sample_batch_count {
521            samples_written += rows;
522        } else {
523            histograms_written += rows;
524        }
525        output_count += 1;
526    }
527    if output_count != batch_count {
528        return Err(PromWriteV2Error {
529            error: incomplete_prom_write_error(),
530            samples_written,
531            histograms_written,
532        });
533    }
534
535    Ok(PromWriteV2Outcome {
536        write_cost,
537        samples_written,
538        histograms_written,
539    })
540}
541
542async fn write_batched_prometheus_v2_rows_with_progress<B: PromWriteBatcher + ?Sized>(
543    prom_store_handler: PromStoreProtocolHandlerRef,
544    batcher: &B,
545    prom_store_with_metric_engine: bool,
546    sample_batches: Vec<PromWriteBatch>,
547    histogram_batches: Vec<PromWriteBatch>,
548) -> std::result::Result<PromWriteV2Outcome, PromWriteV2Error> {
549    let sample_batch_count = sample_batches.len();
550    let mut batches = sample_batches;
551    batches.extend(histogram_batches);
552    preflight_prometheus_rows(&prom_store_handler, &mut batches)
553        .await
554        .map_err(|error| PromWriteV2Error {
555            error,
556            samples_written: 0,
557            histograms_written: 0,
558        })?;
559
560    let mut samples_written = 0;
561    let mut histograms_written = 0;
562    let mut write_cost = 0;
563    let mut batches = batches.into_iter();
564    for (ctx, requests) in batches.by_ref().take(sample_batch_count) {
565        let rows = batcher
566            .submit(requests, ctx)
567            .await
568            .map_err(|error| PromWriteV2Error {
569                error,
570                samples_written,
571                histograms_written,
572            })?;
573        samples_written += rows;
574    }
575    for (ctx, requests) in batches {
576        let rows = prom_write_row_count(&requests);
577        let output = prom_store_handler
578            .write_prepared(requests, ctx, prom_store_with_metric_engine)
579            .await
580            .map_err(|error| PromWriteV2Error {
581                error,
582                samples_written,
583                histograms_written,
584            })?;
585        write_cost += output.meta.cost;
586        histograms_written += rows;
587    }
588
589    Ok(PromWriteV2Outcome {
590        write_cost,
591        samples_written,
592        histograms_written,
593    })
594}
595
596fn prom_write_row_count(request: &RowInsertRequests) -> u64 {
597    request
598        .inserts
599        .iter()
600        .filter_map(|insert| insert.rows.as_ref().map(|rows| rows.rows.len() as u64))
601        .sum()
602}
603
604fn incomplete_prom_write_error() -> error::Error {
605    InternalSnafu {
606        err_msg: "prometheus write handler returned before processing every batch".to_string(),
607    }
608    .build()
609}
610
611fn record_remote_write_samples(db: &str, version: &str, rows: u64) {
612    if rows == 0 {
613        return;
614    }
615    crate::metrics::PROM_STORE_REMOTE_WRITE_SAMPLES
616        .with_label_values(&[db, version])
617        .inc_by(rows);
618}
619
620fn record_remote_write_histograms(db: &str, version: &str, rows: u64) {
621    if rows == 0 {
622        return;
623    }
624    crate::metrics::PROM_STORE_REMOTE_WRITE_HISTOGRAMS
625        .with_label_values(&[db, version])
626        .inc_by(rows);
627}
628
629fn remote_write_v2_error_response(
630    error: error::Error,
631    samples: u64,
632    histograms: u64,
633    exemplars: u64,
634) -> axum::response::Response {
635    let mut response = error.into_response();
636    append_remote_write_v2_written_headers(response.headers_mut(), samples, histograms, exemplars);
637    response
638}
639
640fn append_remote_write_v2_written_headers(
641    headers: &mut HeaderMap,
642    samples: u64,
643    histograms: u64,
644    exemplars: u64,
645) {
646    headers.insert(
647        REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER,
648        HeaderValue::from_str(&samples.to_string()).expect("u64 header value is valid"),
649    );
650    headers.insert(
651        REMOTE_WRITE_V2_HISTOGRAMS_WRITTEN_HEADER,
652        HeaderValue::from_str(&histograms.to_string()).expect("u64 header value is valid"),
653    );
654    headers.insert(
655        REMOTE_WRITE_V2_EXEMPLARS_WRITTEN_HEADER,
656        HeaderValue::from_str(&exemplars.to_string()).expect("u64 header value is valid"),
657    );
658}
659
660enum RemoteWriteProto {
661    V1,
662    V2,
663    Unsupported(mime::Mime),
664}
665
666// ref: https://github.com/prometheus/client_golang/blob/74560058a7af7a695db8196c8e84a0754032c6af/exp/api/remote/remote_api.go#L544
667fn remote_write_proto(content_type: Option<TypedHeader<headers::ContentType>>) -> RemoteWriteProto {
668    let Some(TypedHeader(content_type)) = content_type else {
669        return RemoteWriteProto::V1;
670    };
671
672    let mime_type: mime::Mime = content_type.into();
673    if !mime_type
674        .essence_str()
675        .eq_ignore_ascii_case(CONTENT_TYPE_PROTOBUF_STR)
676    {
677        return RemoteWriteProto::Unsupported(mime_type);
678    }
679
680    for (name, value) in mime_type.params() {
681        if !name.as_str().eq_ignore_ascii_case(CONTENT_TYPE_PROTO_PARAM) {
682            continue;
683        }
684
685        return match value.as_str() {
686            REMOTE_WRITE_V1_PROTO => RemoteWriteProto::V1,
687            REMOTE_WRITE_V2_PROTO => RemoteWriteProto::V2,
688            _ => RemoteWriteProto::Unsupported(mime_type.clone()),
689        };
690    }
691
692    RemoteWriteProto::V1
693}
694
695fn unsupported_remote_write_v2_encoding_response(
696    content_encoding: &headers::ContentEncoding,
697) -> Option<axum::response::Response> {
698    if content_encoding.contains(DEFAULT_ENCODING) || content_encoding.contains(VM_ENCODING) {
699        return None;
700    }
701
702    Some((
703        StatusCode::UNSUPPORTED_MEDIA_TYPE,
704        format!(
705            "unsupported prometheus remote write content encoding: only {DEFAULT_ENCODING} and {VM_ENCODING} are supported"
706        ),
707    )
708        .into_response())
709}
710
711impl IntoResponse for PromStoreResponse {
712    fn into_response(self) -> axum::response::Response {
713        let mut header_map = HeaderMap::new();
714        header_map.insert(&header::CONTENT_TYPE, self.content_type);
715        header_map.insert(&header::CONTENT_ENCODING, self.content_encoding);
716
717        let metrics = if self.resp_metrics.is_empty() {
718            None
719        } else {
720            serde_json::to_string(&self.resp_metrics).ok()
721        };
722        if let Some(m) = metrics.and_then(|m| HeaderValue::from_str(&m).ok()) {
723            header_map.insert(&GREPTIME_DB_HEADER_METRICS, m);
724        }
725
726        (header_map, self.body).into_response()
727    }
728}
729
730#[axum_macros::debug_handler]
731#[tracing::instrument(
732    skip_all,
733    fields(protocol = "prometheus", request_type = "remote_read")
734)]
735pub async fn remote_read(
736    State(state): State<PromStoreState>,
737    Query(params): Query<RemoteWriteQuery>,
738    Extension(mut query_ctx): Extension<QueryContext>,
739    body: Bytes,
740) -> Result<PromStoreResponse> {
741    let db = params.db.clone().unwrap_or_default();
742    query_ctx.set_channel(Channel::Prometheus);
743
744    let request = decode_remote_read_request(body).await?;
745
746    let query_ctx = Arc::new(query_ctx);
747    let _timer = crate::metrics::METRIC_HTTP_PROM_STORE_READ_ELAPSED
748        .with_label_values(&[db.as_str()])
749        .start_timer();
750
751    state.prom_store_handler.read(request, query_ctx).await
752}
753
754async fn decode_remote_read_request(body: Bytes) -> Result<ReadRequest> {
755    let buf = snappy_decompress(&body[..])?;
756
757    ReadRequest::decode(&buf[..]).context(error::DecodePromRemoteRequestSnafu)
758}
759
760#[cfg(test)]
761mod tests {
762    use std::sync::Mutex;
763
764    use api::prom_store::remote::ReadRequest;
765    use api::v1::{Row, RowInsertRequest, Rows};
766    use async_trait::async_trait;
767    use common_query::Output;
768    use pipeline::GreptimePipelineParams;
769    use session::context::{QueryContext, QueryContextRef};
770
771    use super::*;
772    use crate::prom_remote_write::validation::PromValidationMode;
773    use crate::query_handler::PromStoreProtocolHandler;
774
775    #[test]
776    fn test_remote_write_proto() {
777        assert!(matches!(
778            remote_write_proto(content_type(
779                "application/x-protobuf;proto=io.prometheus.write.v2.Request"
780            )),
781            RemoteWriteProto::V2
782        ));
783        assert!(matches!(
784            remote_write_proto(content_type(
785                "application/x-protobuf; proto=\"io.prometheus.write.v2.Request\""
786            )),
787            RemoteWriteProto::V2
788        ));
789        assert!(matches!(
790            remote_write_proto(content_type(
791                "APPLICATION/X-PROTOBUF;proto=io.prometheus.write.v2.Request"
792            )),
793            RemoteWriteProto::V2
794        ));
795        assert!(matches!(
796            remote_write_proto(content_type("application/x-protobuf")),
797            RemoteWriteProto::V1
798        ));
799        assert!(matches!(
800            remote_write_proto(content_type(
801                "application/x-protobuf;proto=prometheus.WriteRequest"
802            )),
803            RemoteWriteProto::V1
804        ));
805        assert!(matches!(
806            remote_write_proto(content_type(
807                "application/x-protobuf;proto=unknown.WriteRequest"
808            )),
809            RemoteWriteProto::Unsupported(_)
810        ));
811        assert!(matches!(
812            remote_write_proto(content_type(
813                "application/json;proto=io.prometheus.write.v2.Request"
814            )),
815            RemoteWriteProto::Unsupported(_)
816        ));
817        assert!(matches!(remote_write_proto(None), RemoteWriteProto::V1));
818    }
819
820    fn content_type(value: &str) -> Option<TypedHeader<headers::ContentType>> {
821        Some(TypedHeader(std::str::FromStr::from_str(value).unwrap()))
822    }
823
824    #[test]
825    fn test_prepare_remote_write_context_stamps_semantics() {
826        let (_, query_ctx, _timer) = prepare_remote_write_context(
827            &RemoteWriteQuery::default(),
828            QueryContext::with("greptime", "public"),
829            REMOTE_WRITE_V2_VERSION,
830        );
831
832        assert_eq!(
833            query_ctx.extension(SEMANTIC_SIGNAL_TYPE),
834            Some(SIGNAL_TYPE_METRIC)
835        );
836        assert_eq!(
837            query_ctx.extension(SEMANTIC_SOURCE),
838            Some(SOURCE_PROMETHEUS)
839        );
840        assert_eq!(
841            query_ctx.extension(SEMANTIC_SOURCE_VERSION),
842            Some(REMOTE_WRITE_V2_VERSION)
843        );
844        assert_eq!(
845            query_ctx.extension(SEMANTIC_METRIC_METADATA_QUALITY),
846            Some(METADATA_QUALITY_INFERRED)
847        );
848    }
849
850    #[tokio::test]
851    async fn test_mixed_v2_preflights_all_then_batches_only_samples() {
852        let events = Arc::new(Mutex::new(Vec::new()));
853        let handler: PromStoreProtocolHandlerRef = Arc::new(RecordingPromStoreHandler {
854            events: events.clone(),
855        });
856        let batcher = RecordingPromWriteBatcher {
857            events: events.clone(),
858        };
859
860        let Ok(outcome) = write_batched_prometheus_v2_rows_with_progress(
861            handler,
862            &batcher,
863            true,
864            vec![test_prom_write_batch("sample")],
865            vec![test_prom_write_batch("histogram")],
866        )
867        .await
868        else {
869            panic!("mixed remote write should succeed")
870        };
871
872        assert_eq!(1, outcome.samples_written);
873        assert_eq!(1, outcome.histograms_written);
874        assert_eq!(
875            vec![
876                "pre:sample".to_string(),
877                "pre:histogram".to_string(),
878                "batch:sample".to_string(),
879                "direct:histogram".to_string(),
880            ],
881            *events.lock().unwrap()
882        );
883    }
884
885    fn test_prom_write_batch(table_name: &str) -> PromWriteBatch {
886        (
887            Arc::new(QueryContext::with("greptime", "public")),
888            RowInsertRequests {
889                inserts: vec![RowInsertRequest {
890                    table_name: table_name.to_string(),
891                    rows: Some(Rows {
892                        schema: Vec::new(),
893                        rows: vec![Row { values: Vec::new() }],
894                    }),
895                }],
896            },
897        )
898    }
899
900    fn record_write_event(events: &Mutex<Vec<String>>, phase: &str, request: &RowInsertRequests) {
901        events.lock().unwrap().push(format!(
902            "{phase}:{}",
903            request.inserts.first().unwrap().table_name
904        ));
905    }
906
907    struct RecordingPromWriteBatcher {
908        events: Arc<Mutex<Vec<String>>>,
909    }
910
911    #[async_trait]
912    impl PromWriteBatcher for RecordingPromWriteBatcher {
913        async fn submit(&self, requests: RowInsertRequests, _ctx: QueryContextRef) -> Result<u64> {
914            record_write_event(&self.events, "batch", &requests);
915            Ok(prom_write_row_count(&requests))
916        }
917    }
918
919    struct RecordingPromStoreHandler {
920        events: Arc<Mutex<Vec<String>>>,
921    }
922
923    #[async_trait]
924    impl PromStoreProtocolHandler for RecordingPromStoreHandler {
925        async fn pre_write(
926            &self,
927            request: &RowInsertRequests,
928            _ctx: QueryContextRef,
929        ) -> Result<()> {
930            record_write_event(&self.events, "pre", request);
931            Ok(())
932        }
933
934        async fn write_prepared(
935            &self,
936            request: RowInsertRequests,
937            _ctx: QueryContextRef,
938            _with_metric_engine: bool,
939        ) -> Result<Output> {
940            record_write_event(&self.events, "direct", &request);
941            Ok(Output::new_with_affected_rows(0))
942        }
943
944        async fn write(
945            &self,
946            _request: RowInsertRequests,
947            _ctx: QueryContextRef,
948            _with_metric_engine: bool,
949        ) -> Result<Output> {
950            unreachable!("mixed v2 writes use preflighted execution")
951        }
952
953        async fn write_all(
954            &self,
955            _requests: Vec<(QueryContextRef, RowInsertRequests)>,
956            _with_metric_engine: bool,
957        ) -> Result<Vec<Result<Output>>> {
958            unreachable!("mixed v2 writes preserve sample and histogram routing")
959        }
960
961        async fn read(
962            &self,
963            _request: ReadRequest,
964            _ctx: QueryContextRef,
965        ) -> Result<PromStoreResponse> {
966            unimplemented!()
967        }
968    }
969
970    #[tokio::test]
971    async fn test_remote_write_v2_ignores_pipeline() {
972        let request = api::greptime_proto::io::prometheus::write::v2::Request {
973            symbols: vec![String::new()],
974            timeseries: Vec::new(),
975        };
976        let body =
977            Bytes::from(crate::prom_store::snappy_compress(&request.encode_to_vec()).unwrap());
978
979        let response = remote_write_v2(
980            test_state(),
981            RemoteWriteQuery::default(),
982            QueryContext::with("greptime", "public"),
983            pipeline_info(Some("pipeline")),
984            false,
985            body,
986        )
987        .await
988        .unwrap();
989
990        assert_eq!(response.status(), StatusCode::NO_CONTENT);
991        assert_eq!(
992            Some("0"),
993            response
994                .headers()
995                .get(REMOTE_WRITE_V2_SAMPLES_WRITTEN_HEADER)
996                .map(|x| x.to_str().unwrap())
997        );
998    }
999
1000    fn test_state() -> PromStoreState {
1001        PromStoreState {
1002            prom_store_handler: Arc::new(NoopPromStoreHandler),
1003            pipeline_handler: None,
1004            prom_store_with_metric_engine: false,
1005            prom_validation_mode: PromValidationMode::Strict,
1006            experimental_enable_prometheus_native_histogram: false,
1007            pending_rows_batcher: None,
1008        }
1009    }
1010
1011    fn pipeline_info(pipeline_name: Option<&str>) -> PipelineInfo {
1012        PipelineInfo {
1013            pipeline_name: pipeline_name.map(ToString::to_string),
1014            pipeline_version: None,
1015            pipeline_params: GreptimePipelineParams::default(),
1016        }
1017    }
1018
1019    struct NoopPromStoreHandler;
1020
1021    #[async_trait]
1022    impl PromStoreProtocolHandler for NoopPromStoreHandler {
1023        async fn pre_write(
1024            &self,
1025            _request: &RowInsertRequests,
1026            _ctx: QueryContextRef,
1027        ) -> Result<()> {
1028            Ok(())
1029        }
1030
1031        async fn write_prepared(
1032            &self,
1033            _request: RowInsertRequests,
1034            _ctx: QueryContextRef,
1035            _with_metric_engine: bool,
1036        ) -> Result<Output> {
1037            unreachable!("empty remote write v2 request should not write")
1038        }
1039
1040        async fn write(
1041            &self,
1042            _request: RowInsertRequests,
1043            _ctx: QueryContextRef,
1044            _with_metric_engine: bool,
1045        ) -> Result<Output> {
1046            unreachable!("empty remote write v2 request should not write")
1047        }
1048
1049        async fn write_all(
1050            &self,
1051            requests: Vec<(QueryContextRef, RowInsertRequests)>,
1052            _with_metric_engine: bool,
1053        ) -> Result<Vec<Result<Output>>> {
1054            assert!(requests.is_empty());
1055            Ok(Vec::new())
1056        }
1057
1058        async fn read(
1059            &self,
1060            _request: ReadRequest,
1061            _ctx: QueryContextRef,
1062        ) -> Result<PromStoreResponse> {
1063            unimplemented!()
1064        }
1065    }
1066}