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