Skip to main content

servers/http/
event.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::BTreeMap;
16use std::fmt::Display;
17use std::io::BufRead;
18use std::str::FromStr;
19use std::sync::Arc;
20use std::time::Instant;
21
22use api::helper::pb_value_to_value_ref;
23use async_trait::async_trait;
24use axum::body::Bytes;
25use axum::extract::{FromRequest, Multipart, Path, Query, Request, State};
26use axum::http::header::CONTENT_TYPE;
27use axum::http::{HeaderMap, StatusCode};
28use axum::response::{IntoResponse, Response};
29use axum::{Extension, Json};
30use axum_extra::TypedHeader;
31use common_catalog::consts::default_engine;
32use common_error::ext::{BoxedError, ErrorExt};
33use common_query::{Output, OutputData};
34use common_telemetry::{error, warn};
35use headers::ContentType;
36use lazy_static::lazy_static;
37use mime_guess::mime;
38use operator::expr_helper::{create_table_expr_by_column_schemas, expr_to_create};
39use pipeline::util::to_pipeline_version;
40use pipeline::{ContextReq, GreptimePipelineParams, PipelineContext, PipelineDefinition};
41use prometheus::{HistogramVec, IntCounterVec};
42use serde::{Deserialize, Serialize};
43use serde_json::{Deserializer, Map, Value as JsonValue, json};
44use session::context::{Channel, QueryContext, QueryContextRef};
45use simd_json::Buffers;
46use snafu::{OptionExt, ResultExt, ensure};
47use store_api::mito_engine_options::APPEND_MODE_KEY;
48use strum::{EnumIter, IntoEnumIterator};
49use table::table_reference::TableReference;
50use vrl::value::{KeyString, Value as VrlValue};
51
52use crate::error::{
53    Error, InvalidParameterSnafu, OtherSnafu, ParseJsonSnafu, PipelineSnafu, Result,
54    status_code_to_http_status,
55};
56use crate::http::HttpResponse;
57use crate::http::header::constants::{
58    GREPTIME_LOG_PIPELINE_NAME_HEADER_NAME, GREPTIME_PIPELINE_NAME_HEADER_NAME,
59    GREPTIME_PIPELINE_PARAMS_HEADER,
60};
61use crate::http::header::{
62    CONTENT_TYPE_NDJSON_STR, CONTENT_TYPE_NDJSON_SUBTYPE_STR, CONTENT_TYPE_PROTOBUF_STR,
63};
64use crate::http::result::greptime_manage_resp::{GreptimedbManageResponse, SqlOutput};
65use crate::http::result::greptime_result_v1::GreptimedbV1Response;
66use crate::interceptor::{LogIngestInterceptor, LogIngestInterceptorRef};
67use crate::metrics::{
68    METRIC_FAILURE_VALUE, METRIC_HTTP_LOGS_INGESTION_COUNTER, METRIC_HTTP_LOGS_INGESTION_ELAPSED,
69    METRIC_SUCCESS_VALUE,
70};
71use crate::pipeline::run_pipeline;
72use crate::query_handler::PipelineHandlerRef;
73
74const GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX: &str = "greptime_";
75const GREPTIME_PIPELINE_SKIP_ERROR_KEY: &str = "skip_error";
76
77const CREATE_TABLE_SQL_SUFFIX_EXISTS: &str =
78    "the pipeline has dispatcher or table_suffix, the table name may not be fixed";
79const CREATE_TABLE_SQL_TABLE_EXISTS: &str =
80    "table already exists, the CREATE TABLE SQL may be different";
81
82lazy_static! {
83    pub static ref JSON_CONTENT_TYPE: ContentType = ContentType::json();
84    pub static ref TEXT_CONTENT_TYPE: ContentType = ContentType::text();
85    pub static ref TEXT_UTF8_CONTENT_TYPE: ContentType = ContentType::text_utf8();
86    pub static ref PB_CONTENT_TYPE: ContentType =
87        ContentType::from_str(CONTENT_TYPE_PROTOBUF_STR).unwrap();
88    pub static ref NDJSON_CONTENT_TYPE: ContentType =
89        ContentType::from_str(CONTENT_TYPE_NDJSON_STR).unwrap();
90}
91
92/// LogIngesterQueryParams is used for query params of log ingester API.
93#[derive(Debug, Default, Serialize, Deserialize)]
94pub struct LogIngesterQueryParams {
95    /// The database where log data will be written to.
96    pub db: Option<String>,
97    /// The table where log data will be written to.
98    pub table: Option<String>,
99    /// The pipeline that will be used for log ingestion.
100    pub pipeline_name: Option<String>,
101    /// The version of the pipeline to be used for log ingestion.
102    pub version: Option<String>,
103    /// Whether to ignore errors during log ingestion.
104    pub ignore_errors: Option<bool>,
105    /// The source of the log data.
106    pub source: Option<String>,
107    /// The JSON field name of the log message. If not provided, it will take the whole log as the message.
108    /// The field must be at the top level of the JSON structure.
109    pub msg_field: Option<String>,
110    /// Specify a custom time index from the input data rather than server's arrival time.
111    /// Valid formats:
112    /// - <field_name>;epoch;<resolution>
113    /// - <field_name>;datestr;<format>
114    ///
115    /// If an error occurs while parsing the config, the error will be returned in the response.
116    /// If an error occurs while ingesting the data, the `ignore_errors` will be used to determine if the error should be ignored.
117    /// If so, use the current server's timestamp as the event time.
118    pub custom_time_index: Option<String>,
119    /// Whether to skip errors during log ingestion.
120    /// If set to true, the ingestion will continue even if there are errors in the data.
121    /// If set to false, the ingestion will stop at the first error.
122    /// This is different from `ignore_errors`, which is used to ignore errors during the pipeline execution.
123    /// The priority of query params is lower than that headers of x-greptime-pipeline-params.
124    pub skip_error: Option<bool>,
125}
126
127/// LogIngestRequest is the internal request for log ingestion. The raw log input can be transformed into multiple LogIngestRequests.
128/// Multiple LogIngestRequests will be ingested into the same database with the same pipeline.
129#[derive(Debug, PartialEq)]
130pub(crate) struct PipelineIngestRequest {
131    /// The table where the log data will be written to.
132    pub table: String,
133    /// The log data to be ingested.
134    pub values: Vec<VrlValue>,
135}
136
137pub struct PipelineContent(String);
138
139impl<S> FromRequest<S> for PipelineContent
140where
141    S: Send + Sync,
142{
143    type Rejection = Response;
144
145    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
146        let content_type_header = req.headers().get(CONTENT_TYPE);
147        let content_type = content_type_header.and_then(|value| value.to_str().ok());
148        if let Some(content_type) = content_type {
149            if content_type.ends_with("yaml") {
150                let payload = String::from_request(req, state)
151                    .await
152                    .map_err(IntoResponse::into_response)?;
153                return Ok(Self(payload));
154            }
155
156            if content_type.starts_with("multipart/form-data") {
157                let mut payload: Multipart = Multipart::from_request(req, state)
158                    .await
159                    .map_err(IntoResponse::into_response)?;
160                let file = payload
161                    .next_field()
162                    .await
163                    .map_err(IntoResponse::into_response)?;
164                let payload = file
165                    .ok_or(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())?
166                    .text()
167                    .await
168                    .map_err(IntoResponse::into_response)?;
169                return Ok(Self(payload));
170            }
171        }
172
173        Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
174    }
175}
176
177#[axum_macros::debug_handler]
178pub async fn query_pipeline(
179    State(state): State<LogState>,
180    Extension(mut query_ctx): Extension<QueryContext>,
181    Query(query_params): Query<LogIngesterQueryParams>,
182    Path(pipeline_name): Path<String>,
183) -> Result<GreptimedbManageResponse> {
184    let start = Instant::now();
185    let handler = state.log_handler;
186    ensure!(
187        !pipeline_name.is_empty(),
188        InvalidParameterSnafu {
189            reason: "pipeline_name is required in path",
190        }
191    );
192
193    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
194
195    query_ctx.set_channel(Channel::Log);
196    let query_ctx = Arc::new(query_ctx);
197
198    let (pipeline, pipeline_version) = handler
199        .get_pipeline_str(&pipeline_name, version, query_ctx)
200        .await?;
201
202    Ok(GreptimedbManageResponse::from_pipeline(
203        pipeline_name,
204        query_params
205            .version
206            .unwrap_or(pipeline_version.0.to_timezone_aware_string(None)),
207        start.elapsed().as_millis() as u64,
208        Some(pipeline),
209    ))
210}
211
212/// Generate DDL from pipeline definition.
213#[axum_macros::debug_handler]
214pub async fn query_pipeline_ddl(
215    State(state): State<LogState>,
216    Extension(mut query_ctx): Extension<QueryContext>,
217    Query(query_params): Query<LogIngesterQueryParams>,
218    Path(pipeline_name): Path<String>,
219) -> Result<GreptimedbManageResponse> {
220    let start = Instant::now();
221    let handler = state.log_handler;
222    ensure!(
223        !pipeline_name.is_empty(),
224        InvalidParameterSnafu {
225            reason: "pipeline_name is required in path",
226        }
227    );
228    ensure!(
229        !pipeline_name.starts_with(GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX),
230        InvalidParameterSnafu {
231            reason: "built-in pipelines don't have fixed table schema",
232        }
233    );
234    let table_name = query_params.table.context(InvalidParameterSnafu {
235        reason: "table name is required",
236    })?;
237
238    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
239
240    query_ctx.set_channel(Channel::Log);
241    let query_ctx = Arc::new(query_ctx);
242
243    let pipeline = handler
244        .get_pipeline(&pipeline_name, version, query_ctx.clone())
245        .await?;
246
247    let schemas_def = pipeline.schemas().context(InvalidParameterSnafu {
248        reason: "auto transform doesn't have fixed table schema",
249    })?;
250
251    let schema = query_ctx.current_schema();
252    let table_name_ref = TableReference {
253        catalog: query_ctx.current_catalog(),
254        schema: &schema,
255        table: &table_name,
256    };
257
258    let mut create_table_expr =
259        create_table_expr_by_column_schemas(&table_name_ref, schemas_def, default_engine(), None)
260            .map_err(BoxedError::new)
261            .context(OtherSnafu)?;
262
263    // manually set the append_mode to true
264    create_table_expr
265        .table_options
266        .insert(APPEND_MODE_KEY.to_string(), "true".to_string());
267
268    let expr = expr_to_create(&create_table_expr, None)
269        .map_err(BoxedError::new)
270        .context(OtherSnafu)?;
271
272    let message = if handler.get_table(&table_name, &query_ctx).await?.is_some() {
273        Some(CREATE_TABLE_SQL_TABLE_EXISTS.to_string())
274    } else if pipeline.is_variant_table_name() {
275        Some(CREATE_TABLE_SQL_SUFFIX_EXISTS.to_string())
276    } else {
277        None
278    };
279
280    let sql = SqlOutput {
281        sql: format!("{:#}", expr),
282        message,
283    };
284
285    Ok(GreptimedbManageResponse::from_sql(
286        sql,
287        start.elapsed().as_millis() as u64,
288    ))
289}
290
291#[axum_macros::debug_handler]
292pub async fn add_pipeline(
293    State(state): State<LogState>,
294    Path(pipeline_name): Path<String>,
295    Extension(mut query_ctx): Extension<QueryContext>,
296    PipelineContent(payload): PipelineContent,
297) -> Result<GreptimedbManageResponse> {
298    let start = Instant::now();
299    let handler = state.log_handler;
300    ensure!(
301        !pipeline_name.is_empty(),
302        InvalidParameterSnafu {
303            reason: "pipeline_name is required in path",
304        }
305    );
306    ensure!(
307        !pipeline_name.starts_with(GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX),
308        InvalidParameterSnafu {
309            reason: "pipeline_name cannot start with greptime_",
310        }
311    );
312    ensure!(
313        !payload.is_empty(),
314        InvalidParameterSnafu {
315            reason: "pipeline is required in body",
316        }
317    );
318
319    query_ctx.set_channel(Channel::Log);
320    let query_ctx = Arc::new(query_ctx);
321
322    let content_type = "yaml";
323    let result = handler
324        .insert_pipeline(&pipeline_name, content_type, &payload, query_ctx)
325        .await;
326
327    result
328        .map(|pipeline| {
329            GreptimedbManageResponse::from_pipeline(
330                pipeline_name,
331                pipeline.0.to_timezone_aware_string(None),
332                start.elapsed().as_millis() as u64,
333                None,
334            )
335        })
336        .map_err(|e| {
337            error!(e; "failed to insert pipeline");
338            e
339        })
340}
341
342#[axum_macros::debug_handler]
343pub async fn delete_pipeline(
344    State(state): State<LogState>,
345    Extension(mut query_ctx): Extension<QueryContext>,
346    Query(query_params): Query<LogIngesterQueryParams>,
347    Path(pipeline_name): Path<String>,
348) -> Result<GreptimedbManageResponse> {
349    let start = Instant::now();
350    let handler = state.log_handler;
351    ensure!(
352        !pipeline_name.is_empty(),
353        InvalidParameterSnafu {
354            reason: "pipeline_name is required",
355        }
356    );
357
358    let version_str = query_params.version.context(InvalidParameterSnafu {
359        reason: "version is required",
360    })?;
361
362    let version = to_pipeline_version(Some(&version_str)).context(PipelineSnafu)?;
363
364    query_ctx.set_channel(Channel::Log);
365    let query_ctx = Arc::new(query_ctx);
366
367    handler
368        .delete_pipeline(&pipeline_name, version, query_ctx)
369        .await
370        .map(|v| {
371            if v.is_some() {
372                GreptimedbManageResponse::from_pipeline(
373                    pipeline_name,
374                    version_str,
375                    start.elapsed().as_millis() as u64,
376                    None,
377                )
378            } else {
379                GreptimedbManageResponse::from_pipelines(vec![], start.elapsed().as_millis() as u64)
380            }
381        })
382        .map_err(|e| {
383            error!(e; "failed to delete pipeline");
384            e
385        })
386}
387
388/// Transform NDJSON array into a single array
389/// always return an array
390pub(crate) fn transform_ndjson_array_factory(
391    values: impl IntoIterator<Item = Result<VrlValue, serde_json::Error>>,
392    ignore_error: bool,
393) -> Result<Vec<VrlValue>> {
394    values
395        .into_iter()
396        .try_fold(Vec::with_capacity(100), |mut acc_array, item| match item {
397            Ok(item_value) => {
398                match item_value {
399                    VrlValue::Array(item_array) => {
400                        acc_array.extend(item_array);
401                    }
402                    VrlValue::Object(_) => {
403                        acc_array.push(item_value);
404                    }
405                    _ => {
406                        if !ignore_error {
407                            warn!("invalid item in array: {:?}", item_value);
408                            return InvalidParameterSnafu {
409                                reason: format!("invalid item: {} in array", item_value),
410                            }
411                            .fail();
412                        }
413                    }
414                }
415                Ok(acc_array)
416            }
417            Err(_) if !ignore_error => item.map(|x| vec![x]).context(ParseJsonSnafu),
418            Err(_) => {
419                warn!("invalid item in array: {:?}", item);
420                Ok(acc_array)
421            }
422        })
423}
424
425/// Dryrun pipeline with given data
426async fn dryrun_pipeline_inner(
427    value: Vec<VrlValue>,
428    pipeline: Arc<pipeline::Pipeline>,
429    pipeline_handler: PipelineHandlerRef,
430    query_ctx: &QueryContextRef,
431) -> Result<Response> {
432    let params = GreptimePipelineParams::default();
433
434    let pipeline_def = PipelineDefinition::Resolved(pipeline);
435    let pipeline_ctx = PipelineContext::new(&pipeline_def, &params, query_ctx.channel());
436    let results = run_pipeline(
437        &pipeline_handler,
438        &pipeline_ctx,
439        PipelineIngestRequest {
440            table: "dry_run".to_owned(),
441            values: value,
442        },
443        query_ctx,
444        true,
445    )
446    .await?;
447
448    let column_type_key = "column_type";
449    let data_type_key = "data_type";
450    let name_key = "name";
451
452    let results = results
453        .all_req()
454        .filter_map(|row| {
455            if let Some(rows) = row.rows {
456                let table_name = row.table_name;
457                let result_schema = rows.schema;
458
459                let schema = result_schema
460                    .iter()
461                    .map(|cs| {
462                        let mut map = Map::new();
463                        map.insert(
464                            name_key.to_string(),
465                            JsonValue::String(cs.column_name.clone()),
466                        );
467                        map.insert(
468                            data_type_key.to_string(),
469                            JsonValue::String(cs.datatype().as_str_name().to_string()),
470                        );
471                        map.insert(
472                            column_type_key.to_string(),
473                            JsonValue::String(cs.semantic_type().as_str_name().to_string()),
474                        );
475                        map.insert(
476                            "fulltext".to_string(),
477                            JsonValue::Bool(
478                                cs.options
479                                    .clone()
480                                    .is_some_and(|x| x.options.contains_key("fulltext")),
481                            ),
482                        );
483                        JsonValue::Object(map)
484                    })
485                    .collect::<Vec<_>>();
486
487                let rows = rows
488                    .rows
489                    .into_iter()
490                    .map(|row| {
491                        row.values
492                            .into_iter()
493                            .enumerate()
494                            .map(|(idx, v)| {
495                                let mut map = Map::new();
496                                let value_ref = pb_value_to_value_ref(
497                                    &v,
498                                    result_schema[idx].datatype_extension.as_ref(),
499                                );
500                                let greptime_value: datatypes::value::Value = value_ref.into();
501                                let serde_json_value =
502                                    serde_json::Value::try_from(greptime_value).unwrap();
503                                map.insert("value".to_string(), serde_json_value);
504                                map.insert("key".to_string(), schema[idx][name_key].clone());
505                                map.insert(
506                                    "semantic_type".to_string(),
507                                    schema[idx][column_type_key].clone(),
508                                );
509                                map.insert(
510                                    "data_type".to_string(),
511                                    schema[idx][data_type_key].clone(),
512                                );
513                                JsonValue::Object(map)
514                            })
515                            .collect()
516                    })
517                    .collect();
518
519                let mut result = Map::new();
520                result.insert("schema".to_string(), JsonValue::Array(schema));
521                result.insert("rows".to_string(), JsonValue::Array(rows));
522                result.insert("table_name".to_string(), JsonValue::String(table_name));
523                let result = JsonValue::Object(result);
524                Some(result)
525            } else {
526                None
527            }
528        })
529        .collect();
530    Ok(Json(JsonValue::Array(results)).into_response())
531}
532
533/// Dryrun pipeline with given data
534/// pipeline_name and pipeline_version to specify pipeline stored in db
535/// pipeline to specify pipeline raw content
536/// data to specify data
537/// data maght be list of string or list of object
538#[derive(Debug, Default, Serialize, Deserialize)]
539pub struct PipelineDryrunParams {
540    pub pipeline_name: Option<String>,
541    pub pipeline_version: Option<String>,
542    pub pipeline: Option<String>,
543    pub data_type: Option<String>,
544    pub data: String,
545}
546
547/// Check if the payload is valid json
548/// Check if the payload contains pipeline or pipeline_name and data
549/// Return Some if valid, None if invalid
550fn check_pipeline_dryrun_params_valid(payload: &Bytes) -> Option<PipelineDryrunParams> {
551    match serde_json::from_slice::<PipelineDryrunParams>(payload) {
552        // payload with pipeline or pipeline_name and data is array
553        Ok(params) if params.pipeline.is_some() || params.pipeline_name.is_some() => Some(params),
554        // because of the pipeline_name or pipeline is required
555        Ok(_) => None,
556        // invalid json
557        Err(_) => None,
558    }
559}
560
561/// Check if the pipeline_name exists
562fn check_pipeline_name_exists(pipeline_name: Option<String>) -> Result<String> {
563    pipeline_name.context(InvalidParameterSnafu {
564        reason: "pipeline_name is required",
565    })
566}
567
568/// Check if the data length less than 10
569fn check_data_valid(data_len: usize) -> Result<()> {
570    ensure!(
571        data_len <= 10,
572        InvalidParameterSnafu {
573            reason: "data is required",
574        }
575    );
576    Ok(())
577}
578
579fn add_step_info_for_pipeline_dryrun_error(step_msg: &str, e: Error) -> Response {
580    let body = Json(json!({
581        "error": format!("{}: {}", step_msg,e.output_msg()),
582    }));
583
584    (status_code_to_http_status(&e.status_code()), body).into_response()
585}
586
587/// Parse the data with given content type
588/// If the content type is invalid, return error
589/// content type is one of application/json, text/plain, application/x-ndjson
590fn parse_dryrun_data(data_type: String, data: String) -> Result<Vec<VrlValue>> {
591    if let Ok(content_type) = ContentType::from_str(&data_type) {
592        extract_pipeline_value_by_content_type(content_type, Bytes::from(data), false)
593    } else {
594        InvalidParameterSnafu {
595            reason: format!(
596                "invalid content type: {}, expected: one of {}",
597                data_type,
598                EventPayloadResolver::support_content_type_list().join(", ")
599            ),
600        }
601        .fail()
602    }
603}
604
605#[axum_macros::debug_handler]
606pub async fn pipeline_dryrun(
607    State(log_state): State<LogState>,
608    Query(query_params): Query<LogIngesterQueryParams>,
609    Extension(mut query_ctx): Extension<QueryContext>,
610    TypedHeader(content_type): TypedHeader<ContentType>,
611    payload: Bytes,
612) -> Result<Response> {
613    let handler = log_state.log_handler;
614
615    query_ctx.set_channel(Channel::Log);
616    let query_ctx = Arc::new(query_ctx);
617
618    match check_pipeline_dryrun_params_valid(&payload) {
619        Some(params) => {
620            let data = parse_dryrun_data(
621                params.data_type.unwrap_or("application/json".to_string()),
622                params.data,
623            )?;
624
625            check_data_valid(data.len())?;
626
627            match params.pipeline {
628                None => {
629                    let version = to_pipeline_version(params.pipeline_version.as_deref())
630                        .context(PipelineSnafu)?;
631                    let pipeline_name = check_pipeline_name_exists(params.pipeline_name)?;
632                    let pipeline = handler
633                        .get_pipeline(&pipeline_name, version, query_ctx.clone())
634                        .await?;
635                    dryrun_pipeline_inner(data, pipeline, handler, &query_ctx).await
636                }
637                Some(pipeline) => {
638                    let pipeline = handler.build_pipeline(&pipeline);
639                    match pipeline {
640                        Ok(pipeline) => {
641                            match dryrun_pipeline_inner(
642                                data,
643                                Arc::new(pipeline),
644                                handler,
645                                &query_ctx,
646                            )
647                            .await
648                            {
649                                Ok(response) => Ok(response),
650                                Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
651                                    "Failed to exec pipeline",
652                                    e,
653                                )),
654                            }
655                        }
656                        Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
657                            "Failed to build pipeline",
658                            e,
659                        )),
660                    }
661                }
662            }
663        }
664        None => {
665            // This path is for back compatibility with the previous dry run code
666            // where the payload is just data (JSON or plain text) and the pipeline name
667            // is specified using query param.
668            let pipeline_name = check_pipeline_name_exists(query_params.pipeline_name)?;
669
670            let version =
671                to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
672
673            let ignore_errors = query_params.ignore_errors.unwrap_or(false);
674
675            let value =
676                extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
677
678            check_data_valid(value.len())?;
679
680            let pipeline = handler
681                .get_pipeline(&pipeline_name, version, query_ctx.clone())
682                .await?;
683
684            dryrun_pipeline_inner(value, pipeline, handler, &query_ctx).await
685        }
686    }
687}
688
689pub(crate) fn extract_pipeline_params_map_from_headers(
690    headers: &HeaderMap,
691) -> ahash::HashMap<String, String> {
692    GreptimePipelineParams::parse_header_str_to_map(
693        headers
694            .get(GREPTIME_PIPELINE_PARAMS_HEADER)
695            .and_then(|v| v.to_str().ok()),
696    )
697}
698
699/// Extracts the pipeline name from the request headers.
700///
701/// Both `x-greptime-pipeline-name` and the deprecated `x-greptime-log-pipeline-name`
702/// are accepted, matching the headers already honored by the OTLP/Elasticsearch/Splunk
703/// log ingestion endpoints. If both are present, the non-deprecated
704/// `x-greptime-pipeline-name` takes precedence. Empty header values are ignored so that
705/// they fall back to the `pipeline_name` query parameter.
706fn pipeline_name_from_headers(headers: &HeaderMap) -> Option<String> {
707    [
708        GREPTIME_PIPELINE_NAME_HEADER_NAME,
709        GREPTIME_LOG_PIPELINE_NAME_HEADER_NAME,
710    ]
711    .iter()
712    .find_map(|name| {
713        headers
714            .get(*name)
715            .and_then(|value| value.to_str().ok())
716            .map(str::trim)
717            .filter(|value| !value.is_empty())
718            .map(ToString::to_string)
719    })
720}
721
722#[axum_macros::debug_handler]
723pub async fn log_ingester(
724    State(log_state): State<LogState>,
725    Query(query_params): Query<LogIngesterQueryParams>,
726    Extension(mut query_ctx): Extension<QueryContext>,
727    TypedHeader(content_type): TypedHeader<ContentType>,
728    headers: HeaderMap,
729    payload: Bytes,
730) -> Result<HttpResponse> {
731    // validate source and payload
732    let source = query_params.source.as_deref();
733    let response = match &log_state.log_validator {
734        Some(validator) => validator.validate(source, &payload).await,
735        None => None,
736    };
737    if let Some(response) = response {
738        return response;
739    }
740
741    let handler = log_state.log_handler;
742
743    let table_name = query_params.table.context(InvalidParameterSnafu {
744        reason: "table is required",
745    })?;
746
747    let ignore_errors = query_params.ignore_errors.unwrap_or(false);
748
749    // A pipeline name supplied via header takes precedence over the query parameter,
750    // consistent with how other pipeline options (e.g. `x-greptime-pipeline-params`)
751    // outrank their query-parameter counterparts.
752    let pipeline_name = pipeline_name_from_headers(&headers)
753        .or(query_params.pipeline_name)
754        .context(InvalidParameterSnafu {
755            reason: "pipeline_name is required",
756        })?;
757    let skip_error = query_params.skip_error.unwrap_or(false);
758    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
759    let pipeline = PipelineDefinition::from_name(
760        &pipeline_name,
761        version,
762        query_params.custom_time_index.map(|s| (s, ignore_errors)),
763    )
764    .context(PipelineSnafu)?;
765
766    let value = extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
767
768    query_ctx.set_channel(Channel::Log);
769    let query_ctx = Arc::new(query_ctx);
770
771    let value = log_state
772        .ingest_interceptor
773        .as_ref()
774        .pre_pipeline(value, query_ctx.clone())?;
775
776    let mut pipeline_params_map = extract_pipeline_params_map_from_headers(&headers);
777    if !pipeline_params_map.contains_key(GREPTIME_PIPELINE_SKIP_ERROR_KEY) && skip_error {
778        pipeline_params_map.insert(GREPTIME_PIPELINE_SKIP_ERROR_KEY.to_string(), "true".into());
779    }
780    let pipeline_params = GreptimePipelineParams::from_map(pipeline_params_map);
781
782    ingest_logs_inner(
783        handler,
784        pipeline,
785        vec![PipelineIngestRequest {
786            table: table_name,
787            values: value,
788        }],
789        query_ctx,
790        pipeline_params,
791    )
792    .await
793}
794
795#[derive(Debug, EnumIter)]
796enum EventPayloadResolverInner {
797    Json,
798    Ndjson,
799    Text,
800}
801
802impl Display for EventPayloadResolverInner {
803    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
804        match self {
805            EventPayloadResolverInner::Json => write!(f, "{}", *JSON_CONTENT_TYPE),
806            EventPayloadResolverInner::Ndjson => write!(f, "{}", *NDJSON_CONTENT_TYPE),
807            EventPayloadResolverInner::Text => write!(f, "{}", *TEXT_CONTENT_TYPE),
808        }
809    }
810}
811
812impl TryFrom<&ContentType> for EventPayloadResolverInner {
813    type Error = Error;
814
815    fn try_from(content_type: &ContentType) -> Result<Self> {
816        let mime: mime_guess::Mime = content_type.clone().into();
817        match (mime.type_(), mime.subtype()) {
818            (mime::APPLICATION, mime::JSON) => Ok(EventPayloadResolverInner::Json),
819            (mime::APPLICATION, subtype) if subtype == CONTENT_TYPE_NDJSON_SUBTYPE_STR => {
820                Ok(EventPayloadResolverInner::Ndjson)
821            }
822            (mime::TEXT, mime::PLAIN) => Ok(EventPayloadResolverInner::Text),
823            _ => InvalidParameterSnafu {
824                reason: format!(
825                    "invalid content type: {}, expected: one of {}",
826                    content_type,
827                    EventPayloadResolver::support_content_type_list().join(", ")
828                ),
829            }
830            .fail(),
831        }
832    }
833}
834
835#[derive(Debug)]
836struct EventPayloadResolver<'a> {
837    inner: EventPayloadResolverInner,
838    /// The content type of the payload.
839    /// keep it for logging original content type
840    #[allow(dead_code)]
841    content_type: &'a ContentType,
842}
843
844impl EventPayloadResolver<'_> {
845    pub(super) fn support_content_type_list() -> Vec<String> {
846        EventPayloadResolverInner::iter()
847            .map(|x| x.to_string())
848            .collect()
849    }
850}
851
852impl<'a> TryFrom<&'a ContentType> for EventPayloadResolver<'a> {
853    type Error = Error;
854
855    fn try_from(content_type: &'a ContentType) -> Result<Self> {
856        let inner = EventPayloadResolverInner::try_from(content_type)?;
857        Ok(EventPayloadResolver {
858            inner,
859            content_type,
860        })
861    }
862}
863
864impl EventPayloadResolver<'_> {
865    fn parse_payload(&self, payload: Bytes, ignore_errors: bool) -> Result<Vec<VrlValue>> {
866        match self.inner {
867            EventPayloadResolverInner::Json => transform_ndjson_array_factory(
868                Deserializer::from_slice(&payload).into_iter(),
869                ignore_errors,
870            ),
871            EventPayloadResolverInner::Ndjson => {
872                let mut result = Vec::with_capacity(1000);
873                let mut buffer = Buffers::new(1000);
874                for (index, line) in payload.lines().enumerate() {
875                    let mut line = match line {
876                        Ok(line) if !line.is_empty() => line,
877                        Ok(_) => continue, // Skip empty lines
878                        Err(_) if ignore_errors => continue,
879                        Err(e) => {
880                            warn!(e; "invalid string at index: {}", index);
881                            return InvalidParameterSnafu {
882                                reason: format!("invalid line at index: {}", index),
883                            }
884                            .fail();
885                        }
886                    };
887
888                    // simd_json, according to description, only de-escapes string at character level,
889                    // like any other json parser. So it should be safe here.
890                    if let Ok(v) = simd_json::serde::from_slice_with_buffers(
891                        unsafe { line.as_bytes_mut() },
892                        &mut buffer,
893                    ) {
894                        result.push(v);
895                    } else if !ignore_errors {
896                        warn!("invalid JSON at index: {}, content: {:?}", index, line);
897                        return InvalidParameterSnafu {
898                            reason: format!("invalid JSON at index: {}", index),
899                        }
900                        .fail();
901                    }
902                }
903                Ok(result)
904            }
905            EventPayloadResolverInner::Text => {
906                let result = payload
907                    .lines()
908                    .filter_map(|line| line.ok().filter(|line| !line.is_empty()))
909                    .map(|line| {
910                        let mut map = BTreeMap::new();
911                        map.insert(
912                            KeyString::from("message"),
913                            VrlValue::Bytes(Bytes::from(line)),
914                        );
915                        VrlValue::Object(map)
916                    })
917                    .collect::<Vec<_>>();
918                Ok(result)
919            }
920        }
921    }
922}
923
924fn extract_pipeline_value_by_content_type(
925    content_type: ContentType,
926    payload: Bytes,
927    ignore_errors: bool,
928) -> Result<Vec<VrlValue>> {
929    EventPayloadResolver::try_from(&content_type).and_then(|resolver| {
930        resolver
931            .parse_payload(payload, ignore_errors)
932            .map_err(|e| match &e {
933                Error::InvalidParameter { reason, .. } if content_type == *JSON_CONTENT_TYPE => {
934                    if reason.contains("invalid item:") {
935                        InvalidParameterSnafu {
936                            reason: "json format error, please check the date is valid JSON.",
937                        }
938                        .build()
939                    } else {
940                        e
941                    }
942                }
943                _ => e,
944            })
945    })
946}
947
948pub(crate) async fn ingest_logs_inner(
949    handler: PipelineHandlerRef,
950    pipeline: PipelineDefinition,
951    log_ingest_requests: Vec<PipelineIngestRequest>,
952    query_ctx: QueryContextRef,
953    pipeline_params: GreptimePipelineParams,
954) -> Result<HttpResponse> {
955    // Keep the timer boundary before pipeline execution to preserve existing
956    // ingestion elapsed metrics.
957    let exec_timer = Instant::now();
958    let mut req = ContextReq::default();
959
960    let pipeline_ctx = PipelineContext::new(&pipeline, &pipeline_params, query_ctx.channel());
961    for pipeline_req in log_ingest_requests {
962        let requests =
963            run_pipeline(&handler, &pipeline_ctx, pipeline_req, &query_ctx, true).await?;
964
965        req.merge(requests);
966    }
967
968    execute_log_context_req(
969        handler,
970        req,
971        query_ctx,
972        exec_timer,
973        &METRIC_HTTP_LOGS_INGESTION_COUNTER,
974        &METRIC_HTTP_LOGS_INGESTION_ELAPSED,
975    )
976    .await
977}
978
979pub(crate) async fn execute_log_context_req(
980    handler: PipelineHandlerRef,
981    ctx_req: ContextReq,
982    query_ctx: QueryContextRef,
983    exec_timer: Instant,
984    counter: &IntCounterVec,
985    elapsed: &HistogramVec,
986) -> Result<HttpResponse> {
987    let db = query_ctx.get_db_string();
988
989    let mut outputs = Vec::with_capacity(ctx_req.map_len());
990    let mut total_rows: u64 = 0;
991    let mut fail = false;
992    for (temp_ctx, act_req) in ctx_req.as_req_iter(query_ctx) {
993        let output = handler.insert(act_req, temp_ctx).await;
994
995        if let Ok(Output {
996            data: OutputData::AffectedRows(rows),
997            meta: _,
998        }) = &output
999        {
1000            total_rows += *rows as u64;
1001        } else {
1002            fail = true;
1003        }
1004        outputs.push(output);
1005    }
1006
1007    // Record one aggregate metric sample for the whole ingestion request.
1008    if total_rows > 0 {
1009        counter.with_label_values(&[db.as_str()]).inc_by(total_rows);
1010        elapsed
1011            .with_label_values(&[db.as_str(), METRIC_SUCCESS_VALUE])
1012            .observe(exec_timer.elapsed().as_secs_f64());
1013    }
1014    if fail {
1015        elapsed
1016            .with_label_values(&[db.as_str(), METRIC_FAILURE_VALUE])
1017            .observe(exec_timer.elapsed().as_secs_f64());
1018    }
1019
1020    let response = GreptimedbV1Response::from_output(outputs)
1021        .await
1022        .with_execution_time(exec_timer.elapsed().as_millis() as u64);
1023    Ok(response)
1024}
1025
1026#[async_trait]
1027pub trait LogValidator: Send + Sync {
1028    /// validate payload by source before processing
1029    /// Return a `Some` result to indicate validation failure.
1030    async fn validate(&self, source: Option<&str>, payload: &Bytes)
1031    -> Option<Result<HttpResponse>>;
1032}
1033
1034pub type LogValidatorRef = Arc<dyn LogValidator + 'static>;
1035
1036/// axum state struct to hold log handler and validator
1037#[derive(Clone)]
1038pub struct LogState {
1039    pub log_handler: PipelineHandlerRef,
1040    pub log_validator: Option<LogValidatorRef>,
1041    pub ingest_interceptor: Option<LogIngestInterceptorRef<Error>>,
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046
1047    use super::*;
1048
1049    #[test]
1050    fn test_transform_ndjson() {
1051        let s = "{\"a\": 1}\n{\"b\": 2}";
1052        let a = serde_json::to_string(
1053            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1054        )
1055        .unwrap();
1056        assert_eq!(a, "[{\"a\":1},{\"b\":2}]");
1057
1058        let s = "{\"a\": 1}";
1059        let a = serde_json::to_string(
1060            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1061        )
1062        .unwrap();
1063        assert_eq!(a, "[{\"a\":1}]");
1064
1065        let s = "[{\"a\": 1}]";
1066        let a = serde_json::to_string(
1067            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1068        )
1069        .unwrap();
1070        assert_eq!(a, "[{\"a\":1}]");
1071
1072        let s = "[{\"a\": 1}, {\"b\": 2}]";
1073        let a = serde_json::to_string(
1074            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1075        )
1076        .unwrap();
1077        assert_eq!(a, "[{\"a\":1},{\"b\":2}]");
1078    }
1079
1080    #[test]
1081    fn test_extract_by_content() {
1082        let payload = r#"
1083        {"a": 1}
1084        {"b": 2"}
1085        {"c": 1}
1086"#
1087        .as_bytes();
1088        let payload = Bytes::from_static(payload);
1089
1090        let fail_rest =
1091            extract_pipeline_value_by_content_type(ContentType::json(), payload.clone(), true);
1092        assert!(fail_rest.is_ok());
1093        assert_eq!(fail_rest.unwrap(), vec![json!({"a": 1}).into()]);
1094
1095        let fail_only_wrong =
1096            extract_pipeline_value_by_content_type(NDJSON_CONTENT_TYPE.clone(), payload, true);
1097        assert!(fail_only_wrong.is_ok());
1098
1099        let mut map1 = BTreeMap::new();
1100        map1.insert(KeyString::from("a"), VrlValue::Integer(1));
1101        let map1 = VrlValue::Object(map1);
1102        let mut map2 = BTreeMap::new();
1103        map2.insert(KeyString::from("c"), VrlValue::Integer(1));
1104        let map2 = VrlValue::Object(map2);
1105        assert_eq!(fail_only_wrong.unwrap(), vec![map1, map2]);
1106    }
1107}