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    handler.check_pipeline_query_permission(&query_ctx)?;
244    let pipeline = handler
245        .get_pipeline(&pipeline_name, version, query_ctx.clone())
246        .await?;
247
248    let schemas_def = pipeline.schemas().context(InvalidParameterSnafu {
249        reason: "auto transform doesn't have fixed table schema",
250    })?;
251
252    let schema = query_ctx.current_schema();
253    let table_name_ref = TableReference {
254        catalog: query_ctx.current_catalog(),
255        schema: &schema,
256        table: &table_name,
257    };
258
259    let mut create_table_expr =
260        create_table_expr_by_column_schemas(&table_name_ref, schemas_def, default_engine(), None)
261            .map_err(BoxedError::new)
262            .context(OtherSnafu)?;
263
264    // manually set the append_mode to true
265    create_table_expr
266        .table_options
267        .insert(APPEND_MODE_KEY.to_string(), "true".to_string());
268
269    let expr = expr_to_create(&create_table_expr, None)
270        .map_err(BoxedError::new)
271        .context(OtherSnafu)?;
272
273    let message = if handler.get_table(&table_name, &query_ctx).await?.is_some() {
274        Some(CREATE_TABLE_SQL_TABLE_EXISTS.to_string())
275    } else if pipeline.is_variant_table_name() {
276        Some(CREATE_TABLE_SQL_SUFFIX_EXISTS.to_string())
277    } else {
278        None
279    };
280
281    let sql = SqlOutput {
282        sql: format!("{:#}", expr),
283        message,
284    };
285
286    Ok(GreptimedbManageResponse::from_sql(
287        sql,
288        start.elapsed().as_millis() as u64,
289    ))
290}
291
292#[axum_macros::debug_handler]
293pub async fn add_pipeline(
294    State(state): State<LogState>,
295    Path(pipeline_name): Path<String>,
296    Extension(mut query_ctx): Extension<QueryContext>,
297    PipelineContent(payload): PipelineContent,
298) -> Result<GreptimedbManageResponse> {
299    let start = Instant::now();
300    let handler = state.log_handler;
301    ensure!(
302        !pipeline_name.is_empty(),
303        InvalidParameterSnafu {
304            reason: "pipeline_name is required in path",
305        }
306    );
307    ensure!(
308        !pipeline_name.starts_with(GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX),
309        InvalidParameterSnafu {
310            reason: "pipeline_name cannot start with greptime_",
311        }
312    );
313    ensure!(
314        !payload.is_empty(),
315        InvalidParameterSnafu {
316            reason: "pipeline is required in body",
317        }
318    );
319
320    query_ctx.set_channel(Channel::Log);
321    let query_ctx = Arc::new(query_ctx);
322
323    let content_type = "yaml";
324    let result = handler
325        .insert_pipeline(&pipeline_name, content_type, &payload, query_ctx)
326        .await;
327
328    result
329        .map(|pipeline| {
330            GreptimedbManageResponse::from_pipeline(
331                pipeline_name,
332                pipeline.0.to_timezone_aware_string(None),
333                start.elapsed().as_millis() as u64,
334                None,
335            )
336        })
337        .map_err(|e| {
338            error!(e; "failed to insert pipeline");
339            e
340        })
341}
342
343#[axum_macros::debug_handler]
344pub async fn delete_pipeline(
345    State(state): State<LogState>,
346    Extension(mut query_ctx): Extension<QueryContext>,
347    Query(query_params): Query<LogIngesterQueryParams>,
348    Path(pipeline_name): Path<String>,
349) -> Result<GreptimedbManageResponse> {
350    let start = Instant::now();
351    let handler = state.log_handler;
352    ensure!(
353        !pipeline_name.is_empty(),
354        InvalidParameterSnafu {
355            reason: "pipeline_name is required",
356        }
357    );
358
359    let version_str = query_params.version.context(InvalidParameterSnafu {
360        reason: "version is required",
361    })?;
362
363    let version = to_pipeline_version(Some(&version_str)).context(PipelineSnafu)?;
364
365    query_ctx.set_channel(Channel::Log);
366    let query_ctx = Arc::new(query_ctx);
367
368    handler
369        .delete_pipeline(&pipeline_name, version, query_ctx)
370        .await
371        .map(|v| {
372            if v.is_some() {
373                GreptimedbManageResponse::from_pipeline(
374                    pipeline_name,
375                    version_str,
376                    start.elapsed().as_millis() as u64,
377                    None,
378                )
379            } else {
380                GreptimedbManageResponse::from_pipelines(vec![], start.elapsed().as_millis() as u64)
381            }
382        })
383        .map_err(|e| {
384            error!(e; "failed to delete pipeline");
385            e
386        })
387}
388
389/// Transform NDJSON array into a single array
390/// always return an array
391pub(crate) fn transform_ndjson_array_factory(
392    values: impl IntoIterator<Item = Result<VrlValue, serde_json::Error>>,
393    ignore_error: bool,
394) -> Result<Vec<VrlValue>> {
395    values
396        .into_iter()
397        .try_fold(Vec::with_capacity(100), |mut acc_array, item| match item {
398            Ok(item_value) => {
399                match item_value {
400                    VrlValue::Array(item_array) => {
401                        acc_array.extend(item_array);
402                    }
403                    VrlValue::Object(_) => {
404                        acc_array.push(item_value);
405                    }
406                    _ => {
407                        if !ignore_error {
408                            warn!("invalid item in array: {:?}", item_value);
409                            return InvalidParameterSnafu {
410                                reason: format!("invalid item: {} in array", item_value),
411                            }
412                            .fail();
413                        }
414                    }
415                }
416                Ok(acc_array)
417            }
418            Err(_) if !ignore_error => item.map(|x| vec![x]).context(ParseJsonSnafu),
419            Err(_) => {
420                warn!("invalid item in array: {:?}", item);
421                Ok(acc_array)
422            }
423        })
424}
425
426/// Dryrun pipeline with given data
427async fn dryrun_pipeline_inner(
428    value: Vec<VrlValue>,
429    pipeline: Arc<pipeline::Pipeline>,
430    pipeline_handler: PipelineHandlerRef,
431    query_ctx: &QueryContextRef,
432) -> Result<Response> {
433    let params = GreptimePipelineParams::default();
434
435    let pipeline_def = PipelineDefinition::Resolved(pipeline);
436    let pipeline_ctx = PipelineContext::new(&pipeline_def, &params, query_ctx.channel());
437    let results = run_pipeline(
438        &pipeline_handler,
439        &pipeline_ctx,
440        PipelineIngestRequest {
441            table: "dry_run".to_owned(),
442            values: value,
443        },
444        query_ctx,
445        true,
446    )
447    .await?;
448
449    let column_type_key = "column_type";
450    let data_type_key = "data_type";
451    let name_key = "name";
452
453    let results = results
454        .all_req()
455        .filter_map(|row| {
456            if let Some(rows) = row.rows {
457                let table_name = row.table_name;
458                let result_schema = rows.schema;
459
460                let schema = result_schema
461                    .iter()
462                    .map(|cs| {
463                        let mut map = Map::new();
464                        map.insert(
465                            name_key.to_string(),
466                            JsonValue::String(cs.column_name.clone()),
467                        );
468                        map.insert(
469                            data_type_key.to_string(),
470                            JsonValue::String(cs.datatype().as_str_name().to_string()),
471                        );
472                        map.insert(
473                            column_type_key.to_string(),
474                            JsonValue::String(cs.semantic_type().as_str_name().to_string()),
475                        );
476                        map.insert(
477                            "fulltext".to_string(),
478                            JsonValue::Bool(
479                                cs.options
480                                    .clone()
481                                    .is_some_and(|x| x.options.contains_key("fulltext")),
482                            ),
483                        );
484                        JsonValue::Object(map)
485                    })
486                    .collect::<Vec<_>>();
487
488                let rows = rows
489                    .rows
490                    .into_iter()
491                    .map(|row| {
492                        row.values
493                            .into_iter()
494                            .enumerate()
495                            .map(|(idx, v)| {
496                                let mut map = Map::new();
497                                let value_ref = pb_value_to_value_ref(
498                                    &v,
499                                    result_schema[idx].datatype_extension.as_ref(),
500                                );
501                                let greptime_value: datatypes::value::Value = value_ref.into();
502                                let serde_json_value =
503                                    serde_json::Value::try_from(greptime_value).unwrap();
504                                map.insert("value".to_string(), serde_json_value);
505                                map.insert("key".to_string(), schema[idx][name_key].clone());
506                                map.insert(
507                                    "semantic_type".to_string(),
508                                    schema[idx][column_type_key].clone(),
509                                );
510                                map.insert(
511                                    "data_type".to_string(),
512                                    schema[idx][data_type_key].clone(),
513                                );
514                                JsonValue::Object(map)
515                            })
516                            .collect()
517                    })
518                    .collect();
519
520                let mut result = Map::new();
521                result.insert("schema".to_string(), JsonValue::Array(schema));
522                result.insert("rows".to_string(), JsonValue::Array(rows));
523                result.insert("table_name".to_string(), JsonValue::String(table_name));
524                let result = JsonValue::Object(result);
525                Some(result)
526            } else {
527                None
528            }
529        })
530        .collect();
531    Ok(Json(JsonValue::Array(results)).into_response())
532}
533
534/// Dryrun pipeline with given data
535/// pipeline_name and pipeline_version to specify pipeline stored in db
536/// pipeline to specify pipeline raw content
537/// data to specify data
538/// data maght be list of string or list of object
539#[derive(Debug, Default, Serialize, Deserialize)]
540pub struct PipelineDryrunParams {
541    pub pipeline_name: Option<String>,
542    pub pipeline_version: Option<String>,
543    pub pipeline: Option<String>,
544    pub data_type: Option<String>,
545    pub data: String,
546}
547
548/// Check if the payload is valid json
549/// Check if the payload contains pipeline or pipeline_name and data
550/// Return Some if valid, None if invalid
551fn check_pipeline_dryrun_params_valid(payload: &Bytes) -> Option<PipelineDryrunParams> {
552    match serde_json::from_slice::<PipelineDryrunParams>(payload) {
553        // payload with pipeline or pipeline_name and data is array
554        Ok(params) if params.pipeline.is_some() || params.pipeline_name.is_some() => Some(params),
555        // because of the pipeline_name or pipeline is required
556        Ok(_) => None,
557        // invalid json
558        Err(_) => None,
559    }
560}
561
562/// Check if the pipeline_name exists
563fn check_pipeline_name_exists(pipeline_name: Option<String>) -> Result<String> {
564    pipeline_name.context(InvalidParameterSnafu {
565        reason: "pipeline_name is required",
566    })
567}
568
569/// Check if the data length less than 10
570fn check_data_valid(data_len: usize) -> Result<()> {
571    ensure!(
572        data_len <= 10,
573        InvalidParameterSnafu {
574            reason: "data is required",
575        }
576    );
577    Ok(())
578}
579
580fn add_step_info_for_pipeline_dryrun_error(step_msg: &str, e: Error) -> Response {
581    let body = Json(json!({
582        "error": format!("{}: {}", step_msg,e.output_msg()),
583    }));
584
585    (status_code_to_http_status(&e.status_code()), body).into_response()
586}
587
588/// Parse the data with given content type
589/// If the content type is invalid, return error
590/// content type is one of application/json, text/plain, application/x-ndjson
591fn parse_dryrun_data(data_type: String, data: String) -> Result<Vec<VrlValue>> {
592    if let Ok(content_type) = ContentType::from_str(&data_type) {
593        extract_pipeline_value_by_content_type(content_type, Bytes::from(data), false)
594    } else {
595        InvalidParameterSnafu {
596            reason: format!(
597                "invalid content type: {}, expected: one of {}",
598                data_type,
599                EventPayloadResolver::support_content_type_list().join(", ")
600            ),
601        }
602        .fail()
603    }
604}
605
606#[axum_macros::debug_handler]
607pub async fn pipeline_dryrun(
608    State(log_state): State<LogState>,
609    Query(query_params): Query<LogIngesterQueryParams>,
610    Extension(mut query_ctx): Extension<QueryContext>,
611    TypedHeader(content_type): TypedHeader<ContentType>,
612    payload: Bytes,
613) -> Result<Response> {
614    let handler = log_state.log_handler;
615
616    query_ctx.set_channel(Channel::Log);
617    let query_ctx = Arc::new(query_ctx);
618    handler.check_pipeline_query_permission(&query_ctx)?;
619
620    match check_pipeline_dryrun_params_valid(&payload) {
621        Some(params) => {
622            let data = parse_dryrun_data(
623                params.data_type.unwrap_or("application/json".to_string()),
624                params.data,
625            )?;
626
627            check_data_valid(data.len())?;
628
629            match params.pipeline {
630                None => {
631                    let version = to_pipeline_version(params.pipeline_version.as_deref())
632                        .context(PipelineSnafu)?;
633                    let pipeline_name = check_pipeline_name_exists(params.pipeline_name)?;
634                    let pipeline = handler
635                        .get_pipeline(&pipeline_name, version, query_ctx.clone())
636                        .await?;
637                    dryrun_pipeline_inner(data, pipeline, handler, &query_ctx).await
638                }
639                Some(pipeline) => {
640                    let pipeline = handler.build_pipeline(&pipeline);
641                    match pipeline {
642                        Ok(pipeline) => {
643                            match dryrun_pipeline_inner(
644                                data,
645                                Arc::new(pipeline),
646                                handler,
647                                &query_ctx,
648                            )
649                            .await
650                            {
651                                Ok(response) => Ok(response),
652                                Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
653                                    "Failed to exec pipeline",
654                                    e,
655                                )),
656                            }
657                        }
658                        Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
659                            "Failed to build pipeline",
660                            e,
661                        )),
662                    }
663                }
664            }
665        }
666        None => {
667            // This path is for back compatibility with the previous dry run code
668            // where the payload is just data (JSON or plain text) and the pipeline name
669            // is specified using query param.
670            let pipeline_name = check_pipeline_name_exists(query_params.pipeline_name)?;
671
672            let version =
673                to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
674
675            let ignore_errors = query_params.ignore_errors.unwrap_or(false);
676
677            let value =
678                extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
679
680            check_data_valid(value.len())?;
681
682            let pipeline = handler
683                .get_pipeline(&pipeline_name, version, query_ctx.clone())
684                .await?;
685
686            dryrun_pipeline_inner(value, pipeline, handler, &query_ctx).await
687        }
688    }
689}
690
691pub(crate) fn extract_pipeline_params_map_from_headers(
692    headers: &HeaderMap,
693) -> ahash::HashMap<String, String> {
694    GreptimePipelineParams::parse_header_str_to_map(
695        headers
696            .get(GREPTIME_PIPELINE_PARAMS_HEADER)
697            .and_then(|v| v.to_str().ok()),
698    )
699}
700
701/// Extracts the pipeline name from the request headers.
702///
703/// Both `x-greptime-pipeline-name` and the deprecated `x-greptime-log-pipeline-name`
704/// are accepted, matching the headers already honored by the OTLP/Elasticsearch/Splunk
705/// log ingestion endpoints. If both are present, the non-deprecated
706/// `x-greptime-pipeline-name` takes precedence. Empty header values are ignored so that
707/// they fall back to the `pipeline_name` query parameter.
708fn pipeline_name_from_headers(headers: &HeaderMap) -> Option<String> {
709    [
710        GREPTIME_PIPELINE_NAME_HEADER_NAME,
711        GREPTIME_LOG_PIPELINE_NAME_HEADER_NAME,
712    ]
713    .iter()
714    .find_map(|name| {
715        headers
716            .get(*name)
717            .and_then(|value| value.to_str().ok())
718            .map(str::trim)
719            .filter(|value| !value.is_empty())
720            .map(ToString::to_string)
721    })
722}
723
724#[axum_macros::debug_handler]
725pub async fn log_ingester(
726    State(log_state): State<LogState>,
727    Query(query_params): Query<LogIngesterQueryParams>,
728    Extension(mut query_ctx): Extension<QueryContext>,
729    TypedHeader(content_type): TypedHeader<ContentType>,
730    headers: HeaderMap,
731    payload: Bytes,
732) -> Result<HttpResponse> {
733    // validate source and payload
734    let source = query_params.source.as_deref();
735    let response = match &log_state.log_validator {
736        Some(validator) => validator.validate(source, &payload).await,
737        None => None,
738    };
739    if let Some(response) = response {
740        return response;
741    }
742
743    let handler = log_state.log_handler;
744
745    let table_name = query_params.table.context(InvalidParameterSnafu {
746        reason: "table is required",
747    })?;
748
749    let ignore_errors = query_params.ignore_errors.unwrap_or(false);
750
751    // A pipeline name supplied via header takes precedence over the query parameter,
752    // consistent with how other pipeline options (e.g. `x-greptime-pipeline-params`)
753    // outrank their query-parameter counterparts.
754    let pipeline_name = pipeline_name_from_headers(&headers)
755        .or(query_params.pipeline_name)
756        .context(InvalidParameterSnafu {
757            reason: "pipeline_name is required",
758        })?;
759    let skip_error = query_params.skip_error.unwrap_or(false);
760    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
761    let pipeline = PipelineDefinition::from_name(
762        &pipeline_name,
763        version,
764        query_params.custom_time_index.map(|s| (s, ignore_errors)),
765    )
766    .context(PipelineSnafu)?;
767
768    let value = extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
769
770    query_ctx.set_channel(Channel::Log);
771    let query_ctx = Arc::new(query_ctx);
772
773    let value = log_state
774        .ingest_interceptor
775        .as_ref()
776        .pre_pipeline(value, query_ctx.clone())?;
777
778    let mut pipeline_params_map = extract_pipeline_params_map_from_headers(&headers);
779    if !pipeline_params_map.contains_key(GREPTIME_PIPELINE_SKIP_ERROR_KEY) && skip_error {
780        pipeline_params_map.insert(GREPTIME_PIPELINE_SKIP_ERROR_KEY.to_string(), "true".into());
781    }
782    let pipeline_params = GreptimePipelineParams::from_map(pipeline_params_map);
783
784    ingest_logs_inner(
785        handler,
786        pipeline,
787        vec![PipelineIngestRequest {
788            table: table_name,
789            values: value,
790        }],
791        query_ctx,
792        pipeline_params,
793    )
794    .await
795}
796
797#[derive(Debug, EnumIter)]
798enum EventPayloadResolverInner {
799    Json,
800    Ndjson,
801    Text,
802}
803
804impl Display for EventPayloadResolverInner {
805    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
806        match self {
807            EventPayloadResolverInner::Json => write!(f, "{}", *JSON_CONTENT_TYPE),
808            EventPayloadResolverInner::Ndjson => write!(f, "{}", *NDJSON_CONTENT_TYPE),
809            EventPayloadResolverInner::Text => write!(f, "{}", *TEXT_CONTENT_TYPE),
810        }
811    }
812}
813
814impl TryFrom<&ContentType> for EventPayloadResolverInner {
815    type Error = Error;
816
817    fn try_from(content_type: &ContentType) -> Result<Self> {
818        let mime: mime_guess::Mime = content_type.clone().into();
819        match (mime.type_(), mime.subtype()) {
820            (mime::APPLICATION, mime::JSON) => Ok(EventPayloadResolverInner::Json),
821            (mime::APPLICATION, subtype) if subtype == CONTENT_TYPE_NDJSON_SUBTYPE_STR => {
822                Ok(EventPayloadResolverInner::Ndjson)
823            }
824            (mime::TEXT, mime::PLAIN) => Ok(EventPayloadResolverInner::Text),
825            _ => InvalidParameterSnafu {
826                reason: format!(
827                    "invalid content type: {}, expected: one of {}",
828                    content_type,
829                    EventPayloadResolver::support_content_type_list().join(", ")
830                ),
831            }
832            .fail(),
833        }
834    }
835}
836
837#[derive(Debug)]
838struct EventPayloadResolver<'a> {
839    inner: EventPayloadResolverInner,
840    /// The content type of the payload.
841    /// keep it for logging original content type
842    #[allow(dead_code)]
843    content_type: &'a ContentType,
844}
845
846impl EventPayloadResolver<'_> {
847    pub(super) fn support_content_type_list() -> Vec<String> {
848        EventPayloadResolverInner::iter()
849            .map(|x| x.to_string())
850            .collect()
851    }
852}
853
854impl<'a> TryFrom<&'a ContentType> for EventPayloadResolver<'a> {
855    type Error = Error;
856
857    fn try_from(content_type: &'a ContentType) -> Result<Self> {
858        let inner = EventPayloadResolverInner::try_from(content_type)?;
859        Ok(EventPayloadResolver {
860            inner,
861            content_type,
862        })
863    }
864}
865
866impl EventPayloadResolver<'_> {
867    fn parse_payload(&self, payload: Bytes, ignore_errors: bool) -> Result<Vec<VrlValue>> {
868        match self.inner {
869            EventPayloadResolverInner::Json => transform_ndjson_array_factory(
870                Deserializer::from_slice(&payload).into_iter(),
871                ignore_errors,
872            ),
873            EventPayloadResolverInner::Ndjson => {
874                let mut result = Vec::with_capacity(1000);
875                let mut buffer = Buffers::new(1000);
876                for (index, line) in payload.lines().enumerate() {
877                    let mut line = match line {
878                        Ok(line) if !line.is_empty() => line,
879                        Ok(_) => continue, // Skip empty lines
880                        Err(_) if ignore_errors => continue,
881                        Err(e) => {
882                            warn!(e; "invalid string at index: {}", index);
883                            return InvalidParameterSnafu {
884                                reason: format!("invalid line at index: {}", index),
885                            }
886                            .fail();
887                        }
888                    };
889
890                    // simd_json, according to description, only de-escapes string at character level,
891                    // like any other json parser. So it should be safe here.
892                    if let Ok(v) = simd_json::serde::from_slice_with_buffers(
893                        unsafe { line.as_bytes_mut() },
894                        &mut buffer,
895                    ) {
896                        result.push(v);
897                    } else if !ignore_errors {
898                        warn!("invalid JSON at index: {}, content: {:?}", index, line);
899                        return InvalidParameterSnafu {
900                            reason: format!("invalid JSON at index: {}", index),
901                        }
902                        .fail();
903                    }
904                }
905                Ok(result)
906            }
907            EventPayloadResolverInner::Text => {
908                let result = payload
909                    .lines()
910                    .filter_map(|line| line.ok().filter(|line| !line.is_empty()))
911                    .map(|line| {
912                        let mut map = BTreeMap::new();
913                        map.insert(
914                            KeyString::from("message"),
915                            VrlValue::Bytes(Bytes::from(line)),
916                        );
917                        VrlValue::Object(map)
918                    })
919                    .collect::<Vec<_>>();
920                Ok(result)
921            }
922        }
923    }
924}
925
926fn extract_pipeline_value_by_content_type(
927    content_type: ContentType,
928    payload: Bytes,
929    ignore_errors: bool,
930) -> Result<Vec<VrlValue>> {
931    EventPayloadResolver::try_from(&content_type).and_then(|resolver| {
932        resolver
933            .parse_payload(payload, ignore_errors)
934            .map_err(|e| match &e {
935                Error::InvalidParameter { reason, .. } if content_type == *JSON_CONTENT_TYPE => {
936                    if reason.contains("invalid item:") {
937                        InvalidParameterSnafu {
938                            reason: "json format error, please check the date is valid JSON.",
939                        }
940                        .build()
941                    } else {
942                        e
943                    }
944                }
945                _ => e,
946            })
947    })
948}
949
950pub(crate) async fn ingest_logs_inner(
951    handler: PipelineHandlerRef,
952    pipeline: PipelineDefinition,
953    log_ingest_requests: Vec<PipelineIngestRequest>,
954    query_ctx: QueryContextRef,
955    pipeline_params: GreptimePipelineParams,
956) -> Result<HttpResponse> {
957    // Keep the timer boundary before pipeline execution to preserve existing
958    // ingestion elapsed metrics.
959    let exec_timer = Instant::now();
960    let mut req = ContextReq::default();
961
962    let pipeline_ctx = PipelineContext::new(&pipeline, &pipeline_params, query_ctx.channel());
963    for pipeline_req in log_ingest_requests {
964        let requests =
965            run_pipeline(&handler, &pipeline_ctx, pipeline_req, &query_ctx, true).await?;
966
967        req.merge(requests);
968    }
969
970    execute_log_context_req(
971        handler,
972        req,
973        query_ctx,
974        exec_timer,
975        &METRIC_HTTP_LOGS_INGESTION_COUNTER,
976        &METRIC_HTTP_LOGS_INGESTION_ELAPSED,
977    )
978    .await
979}
980
981pub(crate) async fn execute_log_context_req(
982    handler: PipelineHandlerRef,
983    ctx_req: ContextReq,
984    query_ctx: QueryContextRef,
985    exec_timer: Instant,
986    counter: &IntCounterVec,
987    elapsed: &HistogramVec,
988) -> Result<HttpResponse> {
989    let db = query_ctx.get_db_string();
990
991    let mut total_rows: u64 = 0;
992    let mut fail = false;
993    let batches = ctx_req.as_req_iter(query_ctx).collect::<Vec<_>>();
994    let outputs = handler.insert_all(batches).await?;
995    for output in &outputs {
996        if let Ok(Output {
997            data: OutputData::AffectedRows(rows),
998            meta: _,
999        }) = &output
1000        {
1001            total_rows += *rows as u64;
1002        } else {
1003            fail = true;
1004        }
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}