1use 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#[derive(Debug, Default, Serialize, Deserialize)]
98pub struct LogIngesterQueryParams {
99 pub db: Option<String>,
101 pub table: Option<String>,
103 pub pipeline_name: Option<String>,
105 pub version: Option<String>,
107 pub ignore_errors: Option<bool>,
110 pub source: Option<String>,
112 pub msg_field: Option<String>,
115 pub custom_time_index: Option<String>,
124 pub skip_error: Option<bool>,
130}
131
132#[derive(Debug, PartialEq)]
135pub(crate) struct PipelineIngestRequest {
136 pub table: String,
138 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#[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 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
394pub(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
431async 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, ¶ms, 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#[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
553fn check_pipeline_dryrun_params_valid(payload: &Bytes) -> Option<PipelineDryrunParams> {
557 match serde_json::from_slice::<PipelineDryrunParams>(payload) {
558 Ok(params) if params.pipeline.is_some() || params.pipeline_name.is_some() => Some(params),
560 Ok(_) => None,
562 Err(_) => None,
564 }
565}
566
567fn check_pipeline_name_exists(pipeline_name: Option<String>) -> Result<String> {
569 pipeline_name.context(InvalidParameterSnafu {
570 reason: "pipeline_name is required",
571 })
572}
573
574fn 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
593fn 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 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
713fn 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 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 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 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 #[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 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 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 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 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 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 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 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 async fn validate(&self, source: Option<&str>, payload: &Bytes)
1107 -> Option<Result<HttpResponse>>;
1108}
1109
1110pub type LogValidatorRef = Arc<dyn LogValidator + 'static>;
1111
1112#[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 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 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 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 (
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}