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