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::borrow::Cow;
16use std::collections::BTreeMap;
17use std::fmt::Display;
18use std::io::BufRead;
19use std::str::FromStr;
20use std::sync::Arc;
21use std::time::Instant;
22
23use api::helper::pb_value_to_value_ref;
24use async_trait::async_trait;
25use axum::body::Bytes;
26use axum::extract::{FromRequest, Multipart, Path, Query, Request, State};
27use axum::http::header::CONTENT_TYPE;
28use axum::http::{HeaderMap, StatusCode};
29use axum::response::{IntoResponse, Response};
30use axum::{Extension, Json};
31use axum_extra::TypedHeader;
32use common_catalog::consts::default_engine;
33use common_error::ext::{BoxedError, ErrorExt};
34use common_query::{Output, OutputData};
35use common_telemetry::{error, warn};
36use headers::ContentType;
37use lazy_static::lazy_static;
38use mime_guess::mime;
39use operator::expr_helper::{create_table_expr_by_column_schemas, expr_to_create};
40use pipeline::util::to_pipeline_version;
41use pipeline::{ContextReq, GreptimePipelineParams, PipelineContext, PipelineDefinition};
42use prometheus::{HistogramVec, IntCounterVec};
43use serde::{Deserialize, Serialize};
44use serde_json::{Deserializer, Map, Value as JsonValue, json};
45use session::context::{Channel, QueryContext, QueryContextRef};
46use simd_json::Buffers;
47use snafu::{OptionExt, ResultExt, ensure};
48use store_api::mito_engine_options::APPEND_MODE_KEY;
49use strum::{EnumIter, IntoEnumIterator};
50use table::table_reference::TableReference;
51use vrl::value::{KeyString, Value as VrlValue};
52
53use crate::error::{
54    Error, InvalidParameterSnafu, OtherSnafu, ParseJsonSnafu, PipelineSnafu, Result,
55    status_code_to_http_status,
56};
57use crate::http::HttpResponse;
58use crate::http::header::constants::{
59    GREPTIME_LOG_PIPELINE_NAME_HEADER_NAME, GREPTIME_PIPELINE_NAME_HEADER_NAME,
60    GREPTIME_PIPELINE_PARAMS_HEADER,
61};
62use crate::http::header::{
63    CONTENT_TYPE_JSON_STREAM_STR, CONTENT_TYPE_JSON_STREAM_SUBTYPE_STR, CONTENT_TYPE_NDJSON_STR,
64    CONTENT_TYPE_NDJSON_SUBTYPE_STR, CONTENT_TYPE_PROTOBUF_STR,
65};
66use crate::http::result::greptime_manage_resp::{GreptimedbManageResponse, SqlOutput};
67use crate::http::result::greptime_result_v1::GreptimedbV1Response;
68use crate::interceptor::{LogIngestInterceptor, LogIngestInterceptorRef};
69use crate::metrics::{
70    METRIC_FAILURE_VALUE, METRIC_HTTP_LOGS_INGESTION_COUNTER, METRIC_HTTP_LOGS_INGESTION_ELAPSED,
71    METRIC_HTTP_LOGS_UTF8_RETRIES_TOTAL, METRIC_SUCCESS_VALUE,
72};
73use crate::pipeline::run_pipeline;
74use crate::query_handler::PipelineHandlerRef;
75
76const GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX: &str = "greptime_";
77const GREPTIME_PIPELINE_SKIP_ERROR_KEY: &str = "skip_error";
78
79const CREATE_TABLE_SQL_SUFFIX_EXISTS: &str =
80    "the pipeline has dispatcher or table_suffix, the table name may not be fixed";
81const CREATE_TABLE_SQL_TABLE_EXISTS: &str =
82    "table already exists, the CREATE TABLE SQL may be different";
83
84lazy_static! {
85    pub static ref JSON_CONTENT_TYPE: ContentType = ContentType::json();
86    pub static ref TEXT_CONTENT_TYPE: ContentType = ContentType::text();
87    pub static ref TEXT_UTF8_CONTENT_TYPE: ContentType = ContentType::text_utf8();
88    pub static ref PB_CONTENT_TYPE: ContentType =
89        ContentType::from_str(CONTENT_TYPE_PROTOBUF_STR).unwrap();
90    pub static ref NDJSON_CONTENT_TYPE: ContentType =
91        ContentType::from_str(CONTENT_TYPE_NDJSON_STR).unwrap();
92    pub static ref JSON_STREAM_CONTENT_TYPE: ContentType =
93        ContentType::from_str(CONTENT_TYPE_JSON_STREAM_STR).unwrap();
94}
95
96/// LogIngesterQueryParams is used for query params of log ingester API.
97#[derive(Debug, Default, Serialize, Deserialize)]
98pub struct LogIngesterQueryParams {
99    /// The database where log data will be written to.
100    pub db: Option<String>,
101    /// The table where log data will be written to.
102    pub table: Option<String>,
103    /// The pipeline that will be used for log ingestion.
104    pub pipeline_name: Option<String>,
105    /// The version of the pipeline to be used for log ingestion.
106    pub version: Option<String>,
107    /// Whether to ignore payload parsing and custom timestamp errors during log ingestion.
108    /// Defaults to the server's `pipeline.ignore_errors` setting when omitted.
109    pub ignore_errors: Option<bool>,
110    /// The source of the log data.
111    pub source: Option<String>,
112    /// The JSON field name of the log message. If not provided, it will take the whole log as the message.
113    /// The field must be at the top level of the JSON structure.
114    pub msg_field: Option<String>,
115    /// Specify a custom time index from the input data rather than server's arrival time.
116    /// Valid formats:
117    /// - <field_name>;epoch;<resolution>
118    /// - <field_name>;datestr;<format>
119    ///
120    /// If an error occurs while parsing the config, the error will be returned in the response.
121    /// If an error occurs while ingesting the data, the `ignore_errors` will be used to determine if the error should be ignored.
122    /// If so, use the current server's timestamp as the event time.
123    pub custom_time_index: Option<String>,
124    /// Whether to skip errors during log ingestion.
125    /// If set to true, the ingestion will continue even if there are errors in the data.
126    /// If set to false, the ingestion will stop at the first error.
127    /// This controls pipeline execution errors, separately from `ignore_errors`.
128    /// The priority of query params is lower than that headers of x-greptime-pipeline-params.
129    pub skip_error: Option<bool>,
130}
131
132/// LogIngestRequest is the internal request for log ingestion. The raw log input can be transformed into multiple LogIngestRequests.
133/// Multiple LogIngestRequests will be ingested into the same database with the same pipeline.
134#[derive(Debug, PartialEq)]
135pub(crate) struct PipelineIngestRequest {
136    /// The table where the log data will be written to.
137    pub table: String,
138    /// The log data to be ingested.
139    pub values: Vec<VrlValue>,
140}
141
142pub struct PipelineContent(String);
143
144impl<S> FromRequest<S> for PipelineContent
145where
146    S: Send + Sync,
147{
148    type Rejection = Response;
149
150    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
151        let content_type_header = req.headers().get(CONTENT_TYPE);
152        let content_type = content_type_header.and_then(|value| value.to_str().ok());
153        if let Some(content_type) = content_type {
154            if content_type.ends_with("yaml") {
155                let payload = String::from_request(req, state)
156                    .await
157                    .map_err(IntoResponse::into_response)?;
158                return Ok(Self(payload));
159            }
160
161            if content_type.starts_with("multipart/form-data") {
162                let mut payload: Multipart = Multipart::from_request(req, state)
163                    .await
164                    .map_err(IntoResponse::into_response)?;
165                let file = payload
166                    .next_field()
167                    .await
168                    .map_err(IntoResponse::into_response)?;
169                let payload = file
170                    .ok_or(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())?
171                    .text()
172                    .await
173                    .map_err(IntoResponse::into_response)?;
174                return Ok(Self(payload));
175            }
176        }
177
178        Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
179    }
180}
181
182#[axum_macros::debug_handler]
183pub async fn query_pipeline(
184    State(state): State<LogState>,
185    Extension(mut query_ctx): Extension<QueryContext>,
186    Query(query_params): Query<LogIngesterQueryParams>,
187    Path(pipeline_name): Path<String>,
188) -> Result<GreptimedbManageResponse> {
189    let start = Instant::now();
190    let handler = state.log_handler;
191    ensure!(
192        !pipeline_name.is_empty(),
193        InvalidParameterSnafu {
194            reason: "pipeline_name is required in path",
195        }
196    );
197
198    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
199
200    query_ctx.set_channel(Channel::Log);
201    let query_ctx = Arc::new(query_ctx);
202
203    let (pipeline, pipeline_version) = handler
204        .get_pipeline_str(&pipeline_name, version, query_ctx)
205        .await?;
206
207    Ok(GreptimedbManageResponse::from_pipeline(
208        pipeline_name,
209        query_params
210            .version
211            .unwrap_or(pipeline_version.0.to_timezone_aware_string(None)),
212        start.elapsed().as_millis() as u64,
213        Some(pipeline),
214    ))
215}
216
217/// Generate DDL from pipeline definition.
218#[axum_macros::debug_handler]
219pub async fn query_pipeline_ddl(
220    State(state): State<LogState>,
221    Extension(mut query_ctx): Extension<QueryContext>,
222    Query(query_params): Query<LogIngesterQueryParams>,
223    Path(pipeline_name): Path<String>,
224) -> Result<GreptimedbManageResponse> {
225    let start = Instant::now();
226    let handler = state.log_handler;
227    ensure!(
228        !pipeline_name.is_empty(),
229        InvalidParameterSnafu {
230            reason: "pipeline_name is required in path",
231        }
232    );
233    ensure!(
234        !pipeline_name.starts_with(GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX),
235        InvalidParameterSnafu {
236            reason: "built-in pipelines don't have fixed table schema",
237        }
238    );
239    let table_name = query_params.table.context(InvalidParameterSnafu {
240        reason: "table name is required",
241    })?;
242
243    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
244
245    query_ctx.set_channel(Channel::Log);
246    let query_ctx = Arc::new(query_ctx);
247
248    handler.check_pipeline_query_permission(&query_ctx)?;
249    let pipeline = handler
250        .get_pipeline(&pipeline_name, version, query_ctx.clone())
251        .await?;
252
253    let schemas_def = pipeline.schemas().context(InvalidParameterSnafu {
254        reason: "auto transform doesn't have fixed table schema",
255    })?;
256
257    let schema = query_ctx.current_schema();
258    let table_name_ref = TableReference {
259        catalog: query_ctx.current_catalog(),
260        schema: &schema,
261        table: &table_name,
262    };
263
264    let mut create_table_expr =
265        create_table_expr_by_column_schemas(&table_name_ref, schemas_def, default_engine(), None)
266            .map_err(BoxedError::new)
267            .context(OtherSnafu)?;
268
269    // manually set the append_mode to true
270    create_table_expr
271        .table_options
272        .insert(APPEND_MODE_KEY.to_string(), "true".to_string());
273
274    let expr = expr_to_create(&create_table_expr, None)
275        .map_err(BoxedError::new)
276        .context(OtherSnafu)?;
277
278    let message = if handler.get_table(&table_name, &query_ctx).await?.is_some() {
279        Some(CREATE_TABLE_SQL_TABLE_EXISTS.to_string())
280    } else if pipeline.is_variant_table_name() {
281        Some(CREATE_TABLE_SQL_SUFFIX_EXISTS.to_string())
282    } else {
283        None
284    };
285
286    let sql = SqlOutput {
287        sql: format!("{:#}", expr),
288        message,
289    };
290
291    Ok(GreptimedbManageResponse::from_sql(
292        sql,
293        start.elapsed().as_millis() as u64,
294    ))
295}
296
297#[axum_macros::debug_handler]
298pub async fn add_pipeline(
299    State(state): State<LogState>,
300    Path(pipeline_name): Path<String>,
301    Extension(mut query_ctx): Extension<QueryContext>,
302    PipelineContent(payload): PipelineContent,
303) -> Result<GreptimedbManageResponse> {
304    let start = Instant::now();
305    let handler = state.log_handler;
306    ensure!(
307        !pipeline_name.is_empty(),
308        InvalidParameterSnafu {
309            reason: "pipeline_name is required in path",
310        }
311    );
312    ensure!(
313        !pipeline_name.starts_with(GREPTIME_INTERNAL_PIPELINE_NAME_PREFIX),
314        InvalidParameterSnafu {
315            reason: "pipeline_name cannot start with greptime_",
316        }
317    );
318    ensure!(
319        !payload.is_empty(),
320        InvalidParameterSnafu {
321            reason: "pipeline is required in body",
322        }
323    );
324
325    query_ctx.set_channel(Channel::Log);
326    let query_ctx = Arc::new(query_ctx);
327
328    let content_type = "yaml";
329    let result = handler
330        .insert_pipeline(&pipeline_name, content_type, &payload, query_ctx)
331        .await;
332
333    result
334        .map(|pipeline| {
335            GreptimedbManageResponse::from_pipeline(
336                pipeline_name,
337                pipeline.0.to_timezone_aware_string(None),
338                start.elapsed().as_millis() as u64,
339                None,
340            )
341        })
342        .map_err(|e| {
343            error!(e; "failed to insert pipeline");
344            e
345        })
346}
347
348#[axum_macros::debug_handler]
349pub async fn delete_pipeline(
350    State(state): State<LogState>,
351    Extension(mut query_ctx): Extension<QueryContext>,
352    Query(query_params): Query<LogIngesterQueryParams>,
353    Path(pipeline_name): Path<String>,
354) -> Result<GreptimedbManageResponse> {
355    let start = Instant::now();
356    let handler = state.log_handler;
357    ensure!(
358        !pipeline_name.is_empty(),
359        InvalidParameterSnafu {
360            reason: "pipeline_name is required",
361        }
362    );
363
364    let version_str = query_params.version.context(InvalidParameterSnafu {
365        reason: "version is required",
366    })?;
367
368    let version = to_pipeline_version(Some(&version_str)).context(PipelineSnafu)?;
369
370    query_ctx.set_channel(Channel::Log);
371    let query_ctx = Arc::new(query_ctx);
372
373    handler
374        .delete_pipeline(&pipeline_name, version, query_ctx)
375        .await
376        .map(|v| {
377            if v.is_some() {
378                GreptimedbManageResponse::from_pipeline(
379                    pipeline_name,
380                    version_str,
381                    start.elapsed().as_millis() as u64,
382                    None,
383                )
384            } else {
385                GreptimedbManageResponse::from_pipelines(vec![], start.elapsed().as_millis() as u64)
386            }
387        })
388        .map_err(|e| {
389            error!(e; "failed to delete pipeline");
390            e
391        })
392}
393
394/// Transform NDJSON array into a single array
395/// always return an array
396pub(crate) fn transform_ndjson_array_factory(
397    values: impl IntoIterator<Item = Result<VrlValue, serde_json::Error>>,
398    ignore_error: bool,
399) -> Result<Vec<VrlValue>> {
400    values
401        .into_iter()
402        .try_fold(Vec::with_capacity(100), |mut acc_array, item| match item {
403            Ok(item_value) => {
404                match item_value {
405                    VrlValue::Array(item_array) => {
406                        acc_array.extend(item_array);
407                    }
408                    VrlValue::Object(_) => {
409                        acc_array.push(item_value);
410                    }
411                    _ => {
412                        if !ignore_error {
413                            warn!("invalid item in array: {:?}", item_value);
414                            return InvalidParameterSnafu {
415                                reason: format!("invalid item: {} in array", item_value),
416                            }
417                            .fail();
418                        }
419                    }
420                }
421                Ok(acc_array)
422            }
423            Err(_) if !ignore_error => item.map(|x| vec![x]).context(ParseJsonSnafu),
424            Err(_) => {
425                warn!("invalid item in array: {:?}", item);
426                Ok(acc_array)
427            }
428        })
429}
430
431/// Dryrun pipeline with given data
432async fn dryrun_pipeline_inner(
433    value: Vec<VrlValue>,
434    pipeline: Arc<pipeline::Pipeline>,
435    pipeline_handler: PipelineHandlerRef,
436    query_ctx: &QueryContextRef,
437) -> Result<Response> {
438    let params = GreptimePipelineParams::default();
439
440    let pipeline_def = PipelineDefinition::Resolved(pipeline);
441    let pipeline_ctx = PipelineContext::new(&pipeline_def, &params, query_ctx.channel());
442    let results = run_pipeline(
443        &pipeline_handler,
444        &pipeline_ctx,
445        PipelineIngestRequest {
446            table: "dry_run".to_owned(),
447            values: value,
448        },
449        query_ctx,
450        true,
451    )
452    .await?;
453
454    let column_type_key = "column_type";
455    let data_type_key = "data_type";
456    let name_key = "name";
457
458    let results = results
459        .all_req()
460        .filter_map(|row| {
461            if let Some(rows) = row.rows {
462                let table_name = row.table_name;
463                let result_schema = rows.schema;
464
465                let schema = result_schema
466                    .iter()
467                    .map(|cs| {
468                        let mut map = Map::new();
469                        map.insert(
470                            name_key.to_string(),
471                            JsonValue::String(cs.column_name.clone()),
472                        );
473                        map.insert(
474                            data_type_key.to_string(),
475                            JsonValue::String(cs.datatype().as_str_name().to_string()),
476                        );
477                        map.insert(
478                            column_type_key.to_string(),
479                            JsonValue::String(cs.semantic_type().as_str_name().to_string()),
480                        );
481                        map.insert(
482                            "fulltext".to_string(),
483                            JsonValue::Bool(
484                                cs.options
485                                    .clone()
486                                    .is_some_and(|x| x.options.contains_key("fulltext")),
487                            ),
488                        );
489                        JsonValue::Object(map)
490                    })
491                    .collect::<Vec<_>>();
492
493                let rows = rows
494                    .rows
495                    .into_iter()
496                    .map(|row| {
497                        row.values
498                            .into_iter()
499                            .enumerate()
500                            .map(|(idx, v)| {
501                                let mut map = Map::new();
502                                let value_ref = pb_value_to_value_ref(
503                                    &v,
504                                    result_schema[idx].datatype_extension.as_ref(),
505                                );
506                                let greptime_value: datatypes::value::Value = value_ref.into();
507                                let serde_json_value =
508                                    serde_json::Value::try_from(greptime_value).unwrap();
509                                map.insert("value".to_string(), serde_json_value);
510                                map.insert("key".to_string(), schema[idx][name_key].clone());
511                                map.insert(
512                                    "semantic_type".to_string(),
513                                    schema[idx][column_type_key].clone(),
514                                );
515                                map.insert(
516                                    "data_type".to_string(),
517                                    schema[idx][data_type_key].clone(),
518                                );
519                                JsonValue::Object(map)
520                            })
521                            .collect()
522                    })
523                    .collect();
524
525                let mut result = Map::new();
526                result.insert("schema".to_string(), JsonValue::Array(schema));
527                result.insert("rows".to_string(), JsonValue::Array(rows));
528                result.insert("table_name".to_string(), JsonValue::String(table_name));
529                let result = JsonValue::Object(result);
530                Some(result)
531            } else {
532                None
533            }
534        })
535        .collect();
536    Ok(Json(JsonValue::Array(results)).into_response())
537}
538
539/// Dryrun pipeline with given data
540/// pipeline_name and pipeline_version to specify pipeline stored in db
541/// pipeline to specify pipeline raw content
542/// data to specify data
543/// data maght be list of string or list of object
544#[derive(Debug, Default, Serialize, Deserialize)]
545pub struct PipelineDryrunParams {
546    pub pipeline_name: Option<String>,
547    pub pipeline_version: Option<String>,
548    pub pipeline: Option<String>,
549    pub data_type: Option<String>,
550    pub data: String,
551}
552
553/// Check if the payload is valid json
554/// Check if the payload contains pipeline or pipeline_name and data
555/// Return Some if valid, None if invalid
556fn check_pipeline_dryrun_params_valid(payload: &Bytes) -> Option<PipelineDryrunParams> {
557    match serde_json::from_slice::<PipelineDryrunParams>(payload) {
558        // payload with pipeline or pipeline_name and data is array
559        Ok(params) if params.pipeline.is_some() || params.pipeline_name.is_some() => Some(params),
560        // because of the pipeline_name or pipeline is required
561        Ok(_) => None,
562        // invalid json
563        Err(_) => None,
564    }
565}
566
567/// Check if the pipeline_name exists
568fn check_pipeline_name_exists(pipeline_name: Option<String>) -> Result<String> {
569    pipeline_name.context(InvalidParameterSnafu {
570        reason: "pipeline_name is required",
571    })
572}
573
574/// Check if the data length less than 10
575fn check_data_valid(data_len: usize) -> Result<()> {
576    ensure!(
577        data_len <= 10,
578        InvalidParameterSnafu {
579            reason: "data is required",
580        }
581    );
582    Ok(())
583}
584
585fn add_step_info_for_pipeline_dryrun_error(step_msg: &str, e: Error) -> Response {
586    let body = Json(json!({
587        "error": format!("{}: {}", step_msg,e.output_msg()),
588    }));
589
590    (status_code_to_http_status(&e.status_code()), body).into_response()
591}
592
593/// Parse the data with given content type
594/// If the content type is invalid, return error
595/// content type is one of application/json, text/plain, application/x-ndjson
596fn parse_dryrun_data(
597    data_type: String,
598    data: String,
599    ignore_errors: bool,
600) -> Result<Vec<VrlValue>> {
601    if let Ok(content_type) = ContentType::from_str(&data_type) {
602        extract_pipeline_value_by_content_type(content_type, Bytes::from(data), ignore_errors)
603    } else {
604        InvalidParameterSnafu {
605            reason: format!(
606                "invalid content type: {}, expected: one of {}",
607                data_type,
608                EventPayloadResolver::support_content_type_list().join(", ")
609            ),
610        }
611        .fail()
612    }
613}
614
615#[axum_macros::debug_handler]
616pub async fn pipeline_dryrun(
617    State(log_state): State<LogState>,
618    Query(query_params): Query<LogIngesterQueryParams>,
619    Extension(mut query_ctx): Extension<QueryContext>,
620    TypedHeader(content_type): TypedHeader<ContentType>,
621    payload: Bytes,
622) -> Result<Response> {
623    let handler = log_state.log_handler;
624
625    query_ctx.set_channel(Channel::Log);
626    let query_ctx = Arc::new(query_ctx);
627    handler.check_pipeline_query_permission(&query_ctx)?;
628
629    let ignore_errors = query_params
630        .ignore_errors
631        .unwrap_or(handler.pipeline_options().ignore_errors);
632
633    match check_pipeline_dryrun_params_valid(&payload) {
634        Some(params) => {
635            let data = parse_dryrun_data(
636                params.data_type.unwrap_or("application/json".to_string()),
637                params.data,
638                ignore_errors,
639            )?;
640
641            check_data_valid(data.len())?;
642
643            match params.pipeline {
644                None => {
645                    let version = to_pipeline_version(params.pipeline_version.as_deref())
646                        .context(PipelineSnafu)?;
647                    let pipeline_name = check_pipeline_name_exists(params.pipeline_name)?;
648                    let pipeline = handler
649                        .get_pipeline(&pipeline_name, version, query_ctx.clone())
650                        .await?;
651                    dryrun_pipeline_inner(data, pipeline, handler, &query_ctx).await
652                }
653                Some(pipeline) => {
654                    let pipeline = handler.build_pipeline(&pipeline);
655                    match pipeline {
656                        Ok(pipeline) => {
657                            match dryrun_pipeline_inner(
658                                data,
659                                Arc::new(pipeline),
660                                handler,
661                                &query_ctx,
662                            )
663                            .await
664                            {
665                                Ok(response) => Ok(response),
666                                Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
667                                    "Failed to exec pipeline",
668                                    e,
669                                )),
670                            }
671                        }
672                        Err(e) => Ok(add_step_info_for_pipeline_dryrun_error(
673                            "Failed to build pipeline",
674                            e,
675                        )),
676                    }
677                }
678            }
679        }
680        None => {
681            // This path is for back compatibility with the previous dry run code
682            // where the payload is just data (JSON or plain text) and the pipeline name
683            // is specified using query param.
684            let pipeline_name = check_pipeline_name_exists(query_params.pipeline_name)?;
685
686            let version =
687                to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
688
689            let value =
690                extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
691
692            check_data_valid(value.len())?;
693
694            let pipeline = handler
695                .get_pipeline(&pipeline_name, version, query_ctx.clone())
696                .await?;
697
698            dryrun_pipeline_inner(value, pipeline, handler, &query_ctx).await
699        }
700    }
701}
702
703pub(crate) fn extract_pipeline_params_map_from_headers(
704    headers: &HeaderMap,
705) -> ahash::HashMap<String, String> {
706    GreptimePipelineParams::parse_header_str_to_map(
707        headers
708            .get(GREPTIME_PIPELINE_PARAMS_HEADER)
709            .and_then(|v| v.to_str().ok()),
710    )
711}
712
713/// Extracts the pipeline name from the request headers.
714///
715/// Both `x-greptime-pipeline-name` and the deprecated `x-greptime-log-pipeline-name`
716/// are accepted, matching the headers already honored by the OTLP/Elasticsearch/Splunk
717/// log ingestion endpoints. If both are present, the non-deprecated
718/// `x-greptime-pipeline-name` takes precedence. Empty header values are ignored so that
719/// they fall back to the `pipeline_name` query parameter.
720fn pipeline_name_from_headers(headers: &HeaderMap) -> Option<String> {
721    [
722        GREPTIME_PIPELINE_NAME_HEADER_NAME,
723        GREPTIME_LOG_PIPELINE_NAME_HEADER_NAME,
724    ]
725    .iter()
726    .find_map(|name| {
727        headers
728            .get(*name)
729            .and_then(|value| value.to_str().ok())
730            .map(str::trim)
731            .filter(|value| !value.is_empty())
732            .map(ToString::to_string)
733    })
734}
735
736#[axum_macros::debug_handler]
737pub async fn log_ingester(
738    State(log_state): State<LogState>,
739    Query(query_params): Query<LogIngesterQueryParams>,
740    Extension(mut query_ctx): Extension<QueryContext>,
741    TypedHeader(content_type): TypedHeader<ContentType>,
742    headers: HeaderMap,
743    payload: Bytes,
744) -> Result<HttpResponse> {
745    // validate source and payload
746    let source = query_params.source.as_deref();
747    let response = match &log_state.log_validator {
748        Some(validator) => validator.validate(source, &payload).await,
749        None => None,
750    };
751    if let Some(response) = response {
752        return response;
753    }
754
755    let handler = log_state.log_handler;
756
757    let table_name = query_params.table.context(InvalidParameterSnafu {
758        reason: "table is required",
759    })?;
760
761    let ignore_errors = query_params
762        .ignore_errors
763        .unwrap_or(handler.pipeline_options().ignore_errors);
764
765    // A pipeline name supplied via header takes precedence over the query parameter,
766    // consistent with how other pipeline options (e.g. `x-greptime-pipeline-params`)
767    // outrank their query-parameter counterparts.
768    let pipeline_name = pipeline_name_from_headers(&headers)
769        .or(query_params.pipeline_name)
770        .context(InvalidParameterSnafu {
771            reason: "pipeline_name is required",
772        })?;
773    let skip_error = query_params.skip_error.unwrap_or(false);
774    let version = to_pipeline_version(query_params.version.as_deref()).context(PipelineSnafu)?;
775    let pipeline = PipelineDefinition::from_name(
776        &pipeline_name,
777        version,
778        query_params.custom_time_index.map(|s| (s, ignore_errors)),
779    )
780    .context(PipelineSnafu)?;
781
782    let value = extract_pipeline_value_by_content_type(content_type, payload, ignore_errors)?;
783
784    query_ctx.set_channel(Channel::Log);
785    let query_ctx = Arc::new(query_ctx);
786
787    let value = log_state
788        .ingest_interceptor
789        .as_ref()
790        .pre_pipeline(value, query_ctx.clone())?;
791
792    let mut pipeline_params_map = extract_pipeline_params_map_from_headers(&headers);
793    if !pipeline_params_map.contains_key(GREPTIME_PIPELINE_SKIP_ERROR_KEY) && skip_error {
794        pipeline_params_map.insert(GREPTIME_PIPELINE_SKIP_ERROR_KEY.to_string(), "true".into());
795    }
796    let pipeline_params = GreptimePipelineParams::from_map(pipeline_params_map);
797
798    ingest_logs_inner(
799        handler,
800        pipeline,
801        vec![PipelineIngestRequest {
802            table: table_name,
803            values: value,
804        }],
805        query_ctx,
806        pipeline_params,
807    )
808    .await
809}
810
811#[derive(Debug, EnumIter)]
812enum EventPayloadResolverInner {
813    Json,
814    /// `application/x-json-stream`: concatenated JSON values without any
815    /// separator between them, e.g. `{"a":1}{"b":2}` (the `json_stream`
816    /// format of fluentbit).
817    JsonStream,
818    Ndjson,
819    Text,
820}
821
822impl Display for EventPayloadResolverInner {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        match self {
825            EventPayloadResolverInner::Json => write!(f, "{}", *JSON_CONTENT_TYPE),
826            EventPayloadResolverInner::JsonStream => write!(f, "{}", *JSON_STREAM_CONTENT_TYPE),
827            EventPayloadResolverInner::Ndjson => write!(f, "{}", *NDJSON_CONTENT_TYPE),
828            EventPayloadResolverInner::Text => write!(f, "{}", *TEXT_CONTENT_TYPE),
829        }
830    }
831}
832
833impl TryFrom<&ContentType> for EventPayloadResolverInner {
834    type Error = Error;
835
836    fn try_from(content_type: &ContentType) -> Result<Self> {
837        let mime: mime_guess::Mime = content_type.clone().into();
838        match (mime.type_(), mime.subtype()) {
839            (mime::APPLICATION, mime::JSON) => Ok(EventPayloadResolverInner::Json),
840            (mime::APPLICATION, subtype) if subtype == CONTENT_TYPE_JSON_STREAM_SUBTYPE_STR => {
841                Ok(EventPayloadResolverInner::JsonStream)
842            }
843            (mime::APPLICATION, subtype) if subtype == CONTENT_TYPE_NDJSON_SUBTYPE_STR => {
844                Ok(EventPayloadResolverInner::Ndjson)
845            }
846            (mime::TEXT, mime::PLAIN) => Ok(EventPayloadResolverInner::Text),
847            _ => InvalidParameterSnafu {
848                reason: format!(
849                    "invalid content type: {}, expected: one of {}",
850                    content_type,
851                    EventPayloadResolver::support_content_type_list().join(", ")
852                ),
853            }
854            .fail(),
855        }
856    }
857}
858
859#[derive(Debug)]
860struct EventPayloadResolver<'a> {
861    inner: EventPayloadResolverInner,
862    /// The content type of the payload.
863    /// keep it for logging original content type
864    #[allow(dead_code)]
865    content_type: &'a ContentType,
866}
867
868impl EventPayloadResolver<'_> {
869    pub(super) fn support_content_type_list() -> Vec<String> {
870        EventPayloadResolverInner::iter()
871            .map(|x| x.to_string())
872            .collect()
873    }
874}
875
876impl<'a> TryFrom<&'a ContentType> for EventPayloadResolver<'a> {
877    type Error = Error;
878
879    fn try_from(content_type: &'a ContentType) -> Result<Self> {
880        let inner = EventPayloadResolverInner::try_from(content_type)?;
881        Ok(EventPayloadResolver {
882            inner,
883            content_type,
884        })
885    }
886}
887
888impl EventPayloadResolver<'_> {
889    fn parse_payload(
890        &self,
891        payload: Bytes,
892        ignore_errors: bool,
893        utf8_retries: &IntCounterVec,
894    ) -> Result<Vec<VrlValue>> {
895        match self.inner {
896            EventPayloadResolverInner::Json | EventPayloadResolverInner::JsonStream => {
897                // Detect UTF-8 failures even when ignore_errors would discard them.
898                let mut saw_invalid_utf8 = false;
899                let result = transform_ndjson_array_factory(
900                    Deserializer::from_slice(&payload)
901                        .into_iter()
902                        .inspect(|item| {
903                            // serde_json keeps its error codes private.
904                            saw_invalid_utf8 |= item.as_ref().is_err_and(|error| {
905                                error.is_syntax()
906                                    && error
907                                        .to_string()
908                                        .starts_with("invalid unicode code point at line ")
909                            });
910                        }),
911                    ignore_errors,
912                );
913                if saw_invalid_utf8 && let Cow::Owned(text) = String::from_utf8_lossy(&payload) {
914                    // Release partially parsed records before retrying the entire payload.
915                    drop(result);
916                    let format = if matches!(self.inner, EventPayloadResolverInner::Json) {
917                        "json"
918                    } else {
919                        "json_stream"
920                    };
921                    utf8_retries.with_label_values(&[format]).inc();
922                    return transform_ndjson_array_factory(
923                        Deserializer::from_str(&text).into_iter(),
924                        ignore_errors,
925                    );
926                }
927                result
928            }
929            EventPayloadResolverInner::Ndjson => {
930                let mut result = Vec::with_capacity(1000);
931                let mut buffer = Buffers::new(1000);
932                let mut remaining = payload.as_ref();
933                let mut line_bytes = Vec::new();
934                // read_until keeps the optimized newline search without validating UTF-8.
935                for index in 0usize.. {
936                    let original = remaining;
937                    line_bytes.clear();
938                    let read = remaining.read_until(b'\n', &mut line_bytes)?;
939                    if read == 0 {
940                        break;
941                    }
942
943                    let line = &original[..read];
944                    let line = if let Some(line) = line.strip_suffix(b"\n") {
945                        line.strip_suffix(b"\r").unwrap_or(line)
946                    } else {
947                        line
948                    };
949                    if line.is_empty() {
950                        continue;
951                    }
952
953                    line_bytes.truncate(line.len());
954                    let mut parsed =
955                        simd_json::serde::from_slice_with_buffers(&mut line_bytes, &mut buffer);
956                    // Retry from the original line because SIMD parsing can rewrite its input.
957                    if parsed.as_ref().is_err_and(|error| {
958                        matches!(error.error(), simd_json::ErrorType::InvalidUtf8)
959                    }) && let Cow::Owned(text) = String::from_utf8_lossy(line)
960                    {
961                        line_bytes = text.into_bytes();
962                        utf8_retries.with_label_values(&["ndjson"]).inc();
963                        parsed =
964                            simd_json::serde::from_slice_with_buffers(&mut line_bytes, &mut buffer);
965                    }
966
967                    if let Ok(v) = parsed {
968                        result.push(v);
969                    } else if !ignore_errors {
970                        warn!(
971                            "invalid JSON at index: {}, content: {:?}",
972                            index,
973                            String::from_utf8_lossy(line)
974                        );
975                        return InvalidParameterSnafu {
976                            reason: format!("invalid JSON at index: {}", index),
977                        }
978                        .fail();
979                    }
980                }
981                Ok(result)
982            }
983            EventPayloadResolverInner::Text => {
984                let result = payload
985                    .lines()
986                    .filter_map(|line| line.ok().filter(|line| !line.is_empty()))
987                    .map(|line| {
988                        let mut map = BTreeMap::new();
989                        map.insert(
990                            KeyString::from("message"),
991                            VrlValue::Bytes(Bytes::from(line)),
992                        );
993                        VrlValue::Object(map)
994                    })
995                    .collect::<Vec<_>>();
996                Ok(result)
997            }
998        }
999    }
1000}
1001
1002fn extract_pipeline_value_by_content_type(
1003    content_type: ContentType,
1004    payload: Bytes,
1005    ignore_errors: bool,
1006) -> Result<Vec<VrlValue>> {
1007    EventPayloadResolver::try_from(&content_type).and_then(|resolver| {
1008        resolver
1009            .parse_payload(payload, ignore_errors, &METRIC_HTTP_LOGS_UTF8_RETRIES_TOTAL)
1010            .map_err(|e| match &e {
1011                Error::InvalidParameter { reason, .. } if content_type == *JSON_CONTENT_TYPE => {
1012                    if reason.contains("invalid item:") {
1013                        InvalidParameterSnafu {
1014                            reason: "json format error, please check the date is valid JSON.",
1015                        }
1016                        .build()
1017                    } else {
1018                        e
1019                    }
1020                }
1021                _ => e,
1022            })
1023    })
1024}
1025
1026pub(crate) async fn ingest_logs_inner(
1027    handler: PipelineHandlerRef,
1028    pipeline: PipelineDefinition,
1029    log_ingest_requests: Vec<PipelineIngestRequest>,
1030    query_ctx: QueryContextRef,
1031    pipeline_params: GreptimePipelineParams,
1032) -> Result<HttpResponse> {
1033    // Keep the timer boundary before pipeline execution to preserve existing
1034    // ingestion elapsed metrics.
1035    let exec_timer = Instant::now();
1036    let mut req = ContextReq::default();
1037
1038    let pipeline_ctx = PipelineContext::new(&pipeline, &pipeline_params, query_ctx.channel());
1039    for pipeline_req in log_ingest_requests {
1040        let requests =
1041            run_pipeline(&handler, &pipeline_ctx, pipeline_req, &query_ctx, true).await?;
1042
1043        req.merge(requests);
1044    }
1045
1046    execute_log_context_req(
1047        handler,
1048        req,
1049        query_ctx,
1050        exec_timer,
1051        &METRIC_HTTP_LOGS_INGESTION_COUNTER,
1052        &METRIC_HTTP_LOGS_INGESTION_ELAPSED,
1053    )
1054    .await
1055}
1056
1057pub(crate) async fn execute_log_context_req(
1058    handler: PipelineHandlerRef,
1059    ctx_req: ContextReq,
1060    query_ctx: QueryContextRef,
1061    exec_timer: Instant,
1062    counter: &IntCounterVec,
1063    elapsed: &HistogramVec,
1064) -> Result<HttpResponse> {
1065    let db = query_ctx.get_db_string();
1066
1067    let mut total_rows: u64 = 0;
1068    let mut fail = false;
1069    let batches = ctx_req.as_req_iter(query_ctx).collect::<Vec<_>>();
1070    let outputs = handler.insert_all(batches).await?;
1071    for output in &outputs {
1072        if let Ok(Output {
1073            data: OutputData::AffectedRows(rows),
1074            meta: _,
1075        }) = &output
1076        {
1077            total_rows += *rows as u64;
1078        } else {
1079            fail = true;
1080        }
1081    }
1082
1083    // Record one aggregate metric sample for the whole ingestion request.
1084    if total_rows > 0 {
1085        counter.with_label_values(&[db.as_str()]).inc_by(total_rows);
1086        elapsed
1087            .with_label_values(&[db.as_str(), METRIC_SUCCESS_VALUE])
1088            .observe(exec_timer.elapsed().as_secs_f64());
1089    }
1090    if fail {
1091        elapsed
1092            .with_label_values(&[db.as_str(), METRIC_FAILURE_VALUE])
1093            .observe(exec_timer.elapsed().as_secs_f64());
1094    }
1095
1096    let response = GreptimedbV1Response::from_output(outputs)
1097        .await
1098        .with_execution_time(exec_timer.elapsed().as_millis() as u64);
1099    Ok(response)
1100}
1101
1102#[async_trait]
1103pub trait LogValidator: Send + Sync {
1104    /// validate payload by source before processing
1105    /// Return a `Some` result to indicate validation failure.
1106    async fn validate(&self, source: Option<&str>, payload: &Bytes)
1107    -> Option<Result<HttpResponse>>;
1108}
1109
1110pub type LogValidatorRef = Arc<dyn LogValidator + 'static>;
1111
1112/// axum state struct to hold log handler and validator
1113#[derive(Clone)]
1114pub struct LogState {
1115    pub log_handler: PipelineHandlerRef,
1116    pub log_validator: Option<LogValidatorRef>,
1117    pub ingest_interceptor: Option<LogIngestInterceptorRef<Error>>,
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122
1123    use prometheus::Opts;
1124
1125    use super::*;
1126
1127    #[test]
1128    fn test_transform_ndjson() {
1129        let s = "{\"a\": 1}\n{\"b\": 2}";
1130        let a = serde_json::to_string(
1131            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1132        )
1133        .unwrap();
1134        assert_eq!(a, "[{\"a\":1},{\"b\":2}]");
1135
1136        let s = "{\"a\": 1}";
1137        let a = serde_json::to_string(
1138            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1139        )
1140        .unwrap();
1141        assert_eq!(a, "[{\"a\":1}]");
1142
1143        let s = "[{\"a\": 1}]";
1144        let a = serde_json::to_string(
1145            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1146        )
1147        .unwrap();
1148        assert_eq!(a, "[{\"a\":1}]");
1149
1150        let s = "[{\"a\": 1}, {\"b\": 2}]";
1151        let a = serde_json::to_string(
1152            &transform_ndjson_array_factory(Deserializer::from_str(s).into_iter(), false).unwrap(),
1153        )
1154        .unwrap();
1155        assert_eq!(a, "[{\"a\":1},{\"b\":2}]");
1156    }
1157
1158    #[test]
1159    fn test_extract_by_json_stream_content_type() {
1160        // `application/x-json-stream` accepts concatenated JSON values, with
1161        // or without line breaks between them.
1162        let json_stream = ContentType::from_str("application/x-json-stream").unwrap();
1163        let payload = r#"{"a": 1}{"b": 2}
1164{"c": 1}"#
1165            .as_bytes();
1166        let payload = Bytes::from_static(payload);
1167
1168        let values =
1169            extract_pipeline_value_by_content_type(json_stream.clone(), payload, false).unwrap();
1170        assert_eq!(
1171            values,
1172            vec![
1173                json!({"a": 1}).into(),
1174                json!({"b": 2}).into(),
1175                json!({"c": 1}).into()
1176            ]
1177        );
1178
1179        assert!(
1180            EventPayloadResolver::support_content_type_list().contains(&json_stream.to_string())
1181        );
1182    }
1183
1184    #[test]
1185    fn test_json_stream_matches_json_for_concatenated_values() {
1186        // a concatenated payload is parsed exactly like `application/json`
1187        let payload = Bytes::from_static(r#"{"a": 1}{"b": 2}"#.as_bytes());
1188        let via_json =
1189            extract_pipeline_value_by_content_type(ContentType::json(), payload.clone(), false)
1190                .unwrap();
1191        let via_stream = extract_pipeline_value_by_content_type(
1192            ContentType::from_str("application/x-json-stream").unwrap(),
1193            payload,
1194            false,
1195        )
1196        .unwrap();
1197        assert_eq!(via_json, via_stream);
1198        assert_eq!(via_stream.len(), 2);
1199    }
1200
1201    #[test]
1202    fn test_json_parser_invalid_utf8_errors() {
1203        for payload in [
1204            b"{\"message\":\"bad\xff\"}".as_slice(),
1205            b"{\"message\":\"bad\xc1\x89\xc8\"}".as_slice(),
1206            b"{\"message\":\"bad\xe2\x82\"}".as_slice(),
1207            b"{\"message\":\"line\\nbad\xff\"}".as_slice(),
1208            b"{\"bad\xff\":1}".as_slice(),
1209        ] {
1210            let error = Deserializer::from_slice(payload)
1211                .into_iter::<VrlValue>()
1212                .next()
1213                .unwrap()
1214                .unwrap_err();
1215            assert!(error.is_syntax(), "{error:?}");
1216            assert!(
1217                error
1218                    .to_string()
1219                    .starts_with("invalid unicode code point at line "),
1220                "{error:?}"
1221            );
1222
1223            let mut line = payload.to_vec();
1224            let error = simd_json::serde::from_slice_with_buffers::<VrlValue>(
1225                &mut line,
1226                &mut Buffers::new(payload.len()),
1227            )
1228            .unwrap_err();
1229            assert_eq!(error.error(), &simd_json::ErrorType::InvalidUtf8);
1230        }
1231    }
1232
1233    #[test]
1234    fn test_json_does_not_retry_other_errors() {
1235        for payload in [
1236            b"{\"message\":\"bad\xff\\q\"}".as_slice(),
1237            b"{\"message\":\"bad\xff".as_slice(),
1238        ] {
1239            let original_error = Deserializer::from_slice(payload)
1240                .into_iter::<VrlValue>()
1241                .next()
1242                .unwrap()
1243                .unwrap_err();
1244            assert!(
1245                !original_error
1246                    .to_string()
1247                    .starts_with("invalid unicode code point at line ")
1248            );
1249
1250            for content_type in [ContentType::json(), JSON_STREAM_CONTENT_TYPE.clone()] {
1251                let error = extract_pipeline_value_by_content_type(
1252                    content_type,
1253                    Bytes::from_static(payload),
1254                    false,
1255                )
1256                .unwrap_err();
1257                let Error::ParseJson { error: source, .. } = error else {
1258                    panic!("expected a JSON parse error, got {error}");
1259                };
1260                assert_eq!(source.to_string(), original_error.to_string());
1261            }
1262        }
1263    }
1264
1265    #[test]
1266    fn test_utf8_retry_counter() {
1267        // Keep exact retry counts independent of other parser tests.
1268        let utf8_retries = IntCounterVec::new(
1269            Opts::new("test_utf8_retries", "UTF-8 retry attempts"),
1270            &["format"],
1271        )
1272        .unwrap();
1273        for (content_type, format, multi_record_retries) in [
1274            (ContentType::json(), "json", 1),
1275            (JSON_STREAM_CONTENT_TYPE.clone(), "json_stream", 1),
1276            (NDJSON_CONTENT_TYPE.clone(), "ndjson", 2),
1277        ] {
1278            let resolver = EventPayloadResolver::try_from(&content_type).unwrap();
1279            let counter = utf8_retries.with_label_values(&[format]);
1280            for ignore_errors in [false, true] {
1281                for (payload, retries, valid_after_retry) in [
1282                    (br#"{"message":"ok"}"#.as_slice(), 0, true),
1283                    (
1284                        b"{\"message\":\"bad\xff\"}\n{\"message\":\"bad\xfe\"}".as_slice(),
1285                        multi_record_retries,
1286                        true,
1287                    ),
1288                    (br#"{"broken":}"#.as_slice(), 0, false),
1289                    (br#"{"message":"\uDC00"}"#.as_slice(), 0, false),
1290                    (
1291                        b"{\"message\":\"bad\xff\",\"broken\":}".as_slice(),
1292                        1,
1293                        false,
1294                    ),
1295                    // SIMD validates UTF-8 before decoding the invalid escape.
1296                    (
1297                        b"{\"message\":\"bad\xff\\q\"}".as_slice(),
1298                        u64::from(format == "ndjson"),
1299                        false,
1300                    ),
1301                ] {
1302                    let before = counter.get();
1303                    let result = resolver.parse_payload(
1304                        Bytes::from_static(payload),
1305                        ignore_errors,
1306                        &utf8_retries,
1307                    );
1308                    assert_eq!(result.is_ok(), valid_after_retry || ignore_errors);
1309                    assert_eq!(
1310                        counter.get() - before,
1311                        retries,
1312                        "format={format}, ignore_errors={ignore_errors}, payload={payload:?}"
1313                    );
1314                }
1315            }
1316        }
1317    }
1318
1319    #[test]
1320    fn test_json_invalid_utf8_retry() {
1321        let expected = vec![
1322            json!({"message": "before"}).into(),
1323            json!({"message": "bad\u{fffd}\u{fffd}\u{fffd}\u{7f}\u{7f}"}).into(),
1324            json!({"message": "after"}).into(),
1325        ];
1326
1327        for payload in [
1328            b"[{\"message\":\"before\"},{\"message\":\"bad\xc1\x89\xc8\x7f\x7f\"},{\"message\":\"after\"}]".as_slice(),
1329            b"{\"message\":\"before\"}\n{\"message\":\"bad\xc1\x89\xc8\x7f\x7f\"}\n{\"message\":\"after\"}".as_slice(),
1330        ] {
1331            for content_type in [ContentType::json(), JSON_STREAM_CONTENT_TYPE.clone()] {
1332                for ignore_errors in [false, true] {
1333                    let actual = extract_pipeline_value_by_content_type(
1334                        content_type.clone(),
1335                        Bytes::from_static(payload),
1336                        ignore_errors,
1337                    )
1338                    .unwrap();
1339                    assert_eq!(actual, expected);
1340                }
1341            }
1342        }
1343    }
1344
1345    #[test]
1346    fn test_json_invalid_utf8_retry_preserves_parse_errors() {
1347        let cases = [
1348            (
1349                br#"{"message":"before"}{"message":"\uD800"}{"message":"after"}"#.as_slice(),
1350                vec![json!({"message": "before"}).into()],
1351            ),
1352            (
1353                b"{\"message\":\"before\"}{\"message\":\"bad\xff\"}{\"broken\":}".as_slice(),
1354                vec![
1355                    json!({"message": "before"}).into(),
1356                    json!({"message": "bad\u{fffd}"}).into(),
1357                ],
1358            ),
1359        ];
1360
1361        for (payload, expected) in cases {
1362            for content_type in [ContentType::json(), JSON_STREAM_CONTENT_TYPE.clone()] {
1363                for ignore_errors in [false, true] {
1364                    let result = extract_pipeline_value_by_content_type(
1365                        content_type.clone(),
1366                        Bytes::from_static(payload),
1367                        ignore_errors,
1368                    );
1369                    if ignore_errors {
1370                        assert_eq!(result.unwrap(), expected);
1371                    } else {
1372                        assert!(matches!(result, Err(Error::ParseJson { .. })));
1373                    }
1374                }
1375            }
1376        }
1377    }
1378
1379    #[test]
1380    fn test_json_invalid_utf8_retry_preserves_ignored_items() {
1381        let payload = Bytes::from_static(b"0{\"message\":\"bad\xff\"}false{\"message\":\"after\"}");
1382        for content_type in [ContentType::json(), JSON_STREAM_CONTENT_TYPE.clone()] {
1383            let result = extract_pipeline_value_by_content_type(
1384                content_type.clone(),
1385                payload.clone(),
1386                false,
1387            );
1388            assert!(matches!(result, Err(Error::InvalidParameter { .. })));
1389
1390            let actual =
1391                extract_pipeline_value_by_content_type(content_type, payload.clone(), true)
1392                    .unwrap();
1393            assert_eq!(
1394                actual,
1395                vec![
1396                    json!({"message": "bad\u{fffd}"}).into(),
1397                    json!({"message": "after"}).into(),
1398                ],
1399            );
1400        }
1401    }
1402
1403    #[test]
1404    fn test_ndjson_invalid_utf8_retry() {
1405        for invalid_bytes in [1, 8192] {
1406            let mut payload = b"\n{\"message\":\"before\"}\r\n\r\n{\"escaped\":\"line\\nquote\\\"slash\\\\\",\"message\":\"bad".to_vec();
1407            payload.extend(std::iter::repeat_n(0xff, invalid_bytes));
1408            payload.extend_from_slice(b"\"}\r\n{\"message\":\"after\"}");
1409            let payload = Bytes::from(payload);
1410            let expected = vec![
1411                json!({"message": "before"}).into(),
1412                json!({
1413                    "escaped": "line\nquote\"slash\\",
1414                    "message": format!("bad{}", "\u{fffd}".repeat(invalid_bytes)),
1415                })
1416                .into(),
1417                json!({"message": "after"}).into(),
1418            ];
1419
1420            for ignore_errors in [false, true] {
1421                let actual = extract_pipeline_value_by_content_type(
1422                    NDJSON_CONTENT_TYPE.clone(),
1423                    payload.clone(),
1424                    ignore_errors,
1425                )
1426                .unwrap();
1427                assert_eq!(actual, expected);
1428            }
1429        }
1430    }
1431
1432    #[test]
1433    fn test_ndjson_invalid_utf8_retry_preserves_parse_errors() {
1434        let too_deep = format!("{}0{}", "[".repeat(1025), "]".repeat(1025));
1435        for bad_line in [
1436            br#"{"escaped":"line\n","broken":}"#.as_slice(),
1437            b"{\"escaped\":\"line\\n\",\"message\":\"bad\xff\",\"broken\":}".as_slice(),
1438            br#"{"escaped":"line\n","message":"\uDC00"}"#.as_slice(),
1439            br#"{"message":"\uD800"}"#.as_slice(),
1440            br#"{"value":1e4294967296}"#.as_slice(),
1441            b"{\"value\":123\0}".as_slice(),
1442            b"{\"message\":\"bad\xe2\x82".as_slice(),
1443            too_deep.as_bytes(),
1444        ] {
1445            let mut payload = b"{\"message\":\"before\"}\n".to_vec();
1446            payload.extend_from_slice(bad_line);
1447            payload.extend_from_slice(b"\n{\"message\":\"after\"}\n");
1448            let payload = Bytes::from(payload);
1449
1450            for ignore_errors in [false, true] {
1451                let result = extract_pipeline_value_by_content_type(
1452                    NDJSON_CONTENT_TYPE.clone(),
1453                    payload.clone(),
1454                    ignore_errors,
1455                );
1456                if ignore_errors {
1457                    assert_eq!(
1458                        result.unwrap(),
1459                        vec![
1460                            json!({"message": "before"}).into(),
1461                            json!({"message": "after"}).into(),
1462                        ],
1463                    );
1464                } else {
1465                    assert!(matches!(
1466                        result,
1467                        Err(Error::InvalidParameter { reason, .. })
1468                            if reason.ends_with("at index: 1")
1469                    ));
1470                }
1471            }
1472        }
1473    }
1474
1475    #[test]
1476    fn test_ndjson_large_exponents() {
1477        let payload = Bytes::from_static(br#"{"zero":0e99999999999,"underflow":1e-4294967295}"#);
1478        for ignore_errors in [false, true] {
1479            let actual = extract_pipeline_value_by_content_type(
1480                NDJSON_CONTENT_TYPE.clone(),
1481                payload.clone(),
1482                ignore_errors,
1483            )
1484            .unwrap();
1485            assert_eq!(actual, vec![json!({"zero": 0.0, "underflow": 0.0}).into()]);
1486        }
1487    }
1488
1489    #[test]
1490    fn test_ndjson_empty_and_whitespace_lines() {
1491        for ignore_errors in [false, true] {
1492            for payload in [b"".as_slice(), b"\n\r\n".as_slice()] {
1493                let actual = extract_pipeline_value_by_content_type(
1494                    NDJSON_CONTENT_TYPE.clone(),
1495                    Bytes::from_static(payload),
1496                    ignore_errors,
1497                )
1498                .unwrap();
1499                assert!(actual.is_empty());
1500            }
1501
1502            for payload in [b"\r".as_slice(), b" \n".as_slice(), b"\r\r\n".as_slice()] {
1503                let result = extract_pipeline_value_by_content_type(
1504                    NDJSON_CONTENT_TYPE.clone(),
1505                    Bytes::from_static(payload),
1506                    ignore_errors,
1507                );
1508                if ignore_errors {
1509                    assert!(result.unwrap().is_empty());
1510                } else {
1511                    assert!(matches!(result, Err(Error::InvalidParameter { .. })));
1512                }
1513            }
1514        }
1515    }
1516
1517    #[test]
1518    fn test_extract_by_content() {
1519        let payload = r#"
1520        {"a": 1}
1521        {"b": 2"}
1522        {"c": 1}
1523"#
1524        .as_bytes();
1525        let payload = Bytes::from_static(payload);
1526
1527        let fail_rest =
1528            extract_pipeline_value_by_content_type(ContentType::json(), payload.clone(), true);
1529        assert!(fail_rest.is_ok());
1530        assert_eq!(fail_rest.unwrap(), vec![json!({"a": 1}).into()]);
1531
1532        let fail_only_wrong =
1533            extract_pipeline_value_by_content_type(NDJSON_CONTENT_TYPE.clone(), payload, true);
1534        assert!(fail_only_wrong.is_ok());
1535
1536        let mut map1 = BTreeMap::new();
1537        map1.insert(KeyString::from("a"), VrlValue::Integer(1));
1538        let map1 = VrlValue::Object(map1);
1539        let mut map2 = BTreeMap::new();
1540        map2.insert(KeyString::from("c"), VrlValue::Integer(1));
1541        let map2 = VrlValue::Object(map2);
1542        assert_eq!(fail_only_wrong.unwrap(), vec![map1, map2]);
1543    }
1544}