1#![allow(dead_code)]
16pub mod ctx_req;
17pub mod field;
18pub mod processor;
19pub mod transform;
20pub mod value;
21
22use std::collections::HashMap;
23
24use api::v1::Row;
25use common_time::timestamp::TimeUnit;
26use itertools::Itertools;
27use processor::{Processor, Processors};
28use snafu::{OptionExt, ResultExt, ensure};
29use transform::Transforms;
30use vrl::core::Value as VrlValue;
31use yaml_rust::{Yaml, YamlLoader};
32
33use crate::dispatcher::{Dispatcher, Rule};
34use crate::error::{
35 ArrayElementMustBeObjectSnafu, AutoTransformOneTimestampSnafu, Error,
36 IntermediateKeyIndexSnafu, InvalidVersionNumberSnafu, Result, TransformArrayElementSnafu,
37 YamlLoadSnafu, YamlParseSnafu,
38};
39use crate::etl::processor::ProcessorKind;
40use crate::etl::transform::transformer::greptime::{
41 RowWithTableSuffix, values_to_row, values_to_rows,
42};
43use crate::tablesuffix::TableSuffixTemplate;
44use crate::{
45 ContextOpt, GreptimeTransformer, IdentityTimeIndex, PipelineContext, PipelineOptions,
46 SchemaInfo, unwrap_or_continue_if_err,
47};
48
49const DESCRIPTION: &str = "description";
50const DOC_VERSION: &str = "version";
51const PROCESSORS: &str = "processors";
52const TRANSFORM: &str = "transform";
53const TRANSFORMS: &str = "transforms";
54const DISPATCHER: &str = "dispatcher";
55const TABLESUFFIX: &str = "table_suffix";
56
57pub enum Content<'a> {
58 Json(&'a str),
59 Yaml(&'a str),
60}
61
62pub fn parse(input: &Content) -> Result<Pipeline> {
63 parse_with_options(input, &PipelineOptions::default())
64}
65
66pub fn parse_with_options(input: &Content, options: &PipelineOptions) -> Result<Pipeline> {
68 match input {
69 Content::Yaml(str) => {
70 let docs = YamlLoader::load_from_str(str).context(YamlLoadSnafu)?;
71
72 ensure!(docs.len() == 1, YamlParseSnafu);
73
74 let doc = &docs[0];
75
76 let description = doc[DESCRIPTION].as_str().map(|s| s.to_string());
77
78 let doc_version = (&doc[DOC_VERSION]).try_into()?;
79
80 let processors = if let Some(v) = doc[PROCESSORS].as_vec() {
81 Processors::from_yaml(v, options.allow_vrl_host_access)?
82 } else {
83 Processors::default()
84 };
85
86 let transformers = if let Some(v) = doc[TRANSFORMS].as_vec().or(doc[TRANSFORM].as_vec())
87 {
88 v.try_into()?
89 } else {
90 Transforms::default()
91 };
92
93 let transformer = if transformers.is_empty() {
94 let cnt = processors
97 .iter()
98 .filter_map(|p| match p {
99 ProcessorKind::Date(d) if !d.ignore_missing() => Some(
100 d.fields
101 .iter()
102 .map(|f| (f.target_or_input_field(), TimeUnit::Nanosecond))
103 .collect_vec(),
104 ),
105 ProcessorKind::Epoch(e) if !e.ignore_missing() => Some(
106 e.fields
107 .iter()
108 .map(|f| (f.target_or_input_field(), (&e.resolution).into()))
109 .collect_vec(),
110 ),
111 _ => None,
112 })
113 .flatten()
114 .collect_vec();
115 ensure!(cnt.len() == 1, AutoTransformOneTimestampSnafu);
116
117 let (ts_name, timeunit) = cnt.first().unwrap();
118 TransformerMode::AutoTransform(ts_name.to_string(), *timeunit)
119 } else {
120 TransformerMode::GreptimeTransformer(GreptimeTransformer::new(
121 transformers,
122 &doc_version,
123 )?)
124 };
125
126 let dispatcher = if !doc[DISPATCHER].is_badvalue() {
127 Some(Dispatcher::try_from(&doc[DISPATCHER])?)
128 } else {
129 None
130 };
131
132 let tablesuffix = if !doc[TABLESUFFIX].is_badvalue() {
133 Some(TableSuffixTemplate::try_from(&doc[TABLESUFFIX])?)
134 } else {
135 None
136 };
137
138 Ok(Pipeline {
139 doc_version,
140 description,
141 processors,
142 transformer,
143 dispatcher,
144 tablesuffix,
145 })
146 }
147 Content::Json(_) => unimplemented!(),
148 }
149}
150
151#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
152pub enum PipelineDocVersion {
153 #[default]
156 V1,
157
158 V2,
165}
166
167impl TryFrom<&Yaml> for PipelineDocVersion {
168 type Error = Error;
169
170 fn try_from(value: &Yaml) -> Result<Self> {
171 if value.is_badvalue() || value.is_null() {
172 return Ok(PipelineDocVersion::V1);
173 }
174
175 let version = match value {
176 Yaml::String(s) => s
177 .parse::<i64>()
178 .map_err(|_| InvalidVersionNumberSnafu { version: s.clone() }.build())?,
179 Yaml::Integer(i) => *i,
180 _ => {
181 return InvalidVersionNumberSnafu {
182 version: value.as_str().unwrap_or_default().to_string(),
183 }
184 .fail();
185 }
186 };
187
188 match version {
189 1 => Ok(PipelineDocVersion::V1),
190 2 => Ok(PipelineDocVersion::V2),
191 _ => InvalidVersionNumberSnafu {
192 version: version.to_string(),
193 }
194 .fail(),
195 }
196 }
197}
198
199#[derive(Debug)]
200pub struct Pipeline {
201 doc_version: PipelineDocVersion,
202 description: Option<String>,
203 processors: processor::Processors,
204 dispatcher: Option<Dispatcher>,
205 transformer: TransformerMode,
206 tablesuffix: Option<TableSuffixTemplate>,
207}
208
209#[derive(Debug, Clone)]
210pub enum TransformerMode {
211 GreptimeTransformer(GreptimeTransformer),
212 AutoTransform(String, TimeUnit),
213}
214
215#[derive(Debug, Hash, PartialEq, Eq, Clone, PartialOrd, Ord)]
217pub struct DispatchedTo {
218 pub table_suffix: String,
219 pub pipeline: Option<String>,
220}
221
222impl From<&Rule> for DispatchedTo {
223 fn from(value: &Rule) -> Self {
224 DispatchedTo {
225 table_suffix: value.table_suffix.clone(),
226 pipeline: value.pipeline.clone(),
227 }
228 }
229}
230
231impl DispatchedTo {
232 pub fn dispatched_to_table_name(&self, original: &str) -> String {
234 [original, &self.table_suffix].concat()
235 }
236}
237
238#[derive(Debug)]
240pub enum PipelineExecOutput {
241 Transformed(TransformedOutput),
242 DispatchedTo(DispatchedTo, VrlValue),
243 Filtered,
244}
245
246#[derive(Debug)]
248pub enum PipelineProcessOutput {
249 Processed(VrlValue),
250 DispatchedTo(DispatchedTo, VrlValue),
251 Filtered,
252}
253
254#[derive(Debug)]
261pub struct TransformedOutput {
262 pub rows_by_context: HashMap<ContextOpt, Vec<RowWithTableSuffix>>,
264}
265
266impl PipelineExecOutput {
267 pub fn into_transformed(self) -> Option<Vec<RowWithTableSuffix>> {
269 if let Self::Transformed(TransformedOutput { rows_by_context }) = self {
270 Some(rows_by_context.into_values().flatten().collect())
272 } else {
273 None
274 }
275 }
276
277 pub fn into_transformed_hashmap(self) -> Option<HashMap<ContextOpt, Vec<RowWithTableSuffix>>> {
279 if let Self::Transformed(TransformedOutput { rows_by_context }) = self {
280 Some(rows_by_context)
281 } else {
282 None
283 }
284 }
285
286 pub fn into_dispatched(self) -> Option<DispatchedTo> {
288 if let Self::DispatchedTo(d, _) = self {
289 Some(d)
290 } else {
291 None
292 }
293 }
294}
295
296impl Pipeline {
297 fn is_v1(&self) -> bool {
298 self.doc_version == PipelineDocVersion::V1
299 }
300
301 pub fn exec_mut(
302 &self,
303 val: VrlValue,
304 pipeline_ctx: &PipelineContext<'_>,
305 schema_info: &mut SchemaInfo,
306 ) -> Result<PipelineExecOutput> {
307 match self.process_mut(val)? {
308 PipelineProcessOutput::Processed(val) => self
309 .transform_mut(val, pipeline_ctx, schema_info)
310 .map(PipelineExecOutput::Transformed),
311 PipelineProcessOutput::DispatchedTo(dispatched_to, val) => {
312 Ok(PipelineExecOutput::DispatchedTo(dispatched_to, val))
313 }
314 PipelineProcessOutput::Filtered => Ok(PipelineExecOutput::Filtered),
315 }
316 }
317
318 pub fn process_mut(&self, mut val: VrlValue) -> Result<PipelineProcessOutput> {
319 for processor in self.processors.iter() {
320 val = processor.exec_mut(val)?;
321 if val.is_null() {
322 return Ok(PipelineProcessOutput::Filtered);
323 }
324 }
325
326 if let Some(rule) = self.dispatcher.as_ref().and_then(|d| d.exec(&val)) {
327 return Ok(PipelineProcessOutput::DispatchedTo(rule.into(), val));
328 }
329
330 Ok(PipelineProcessOutput::Processed(val))
331 }
332
333 pub fn transform_mut(
334 &self,
335 val: VrlValue,
336 pipeline_ctx: &PipelineContext<'_>,
337 schema_info: &mut SchemaInfo,
338 ) -> Result<TransformedOutput> {
339 let mut val = if val.is_array() {
340 val
341 } else {
342 VrlValue::Array(vec![val])
343 };
344
345 let rows_by_context = match self.transformer() {
346 TransformerMode::GreptimeTransformer(greptime_transformer) => {
347 transform_array_elements_by_ctx(
348 val.as_array_mut().unwrap(),
350 greptime_transformer,
351 self.is_v1(),
352 schema_info,
353 pipeline_ctx,
354 self.tablesuffix.as_ref(),
355 )?
356 }
357 TransformerMode::AutoTransform(ts_name, time_unit) => {
358 let def = crate::PipelineDefinition::GreptimeIdentityPipeline(Some(
359 IdentityTimeIndex::Epoch(ts_name.clone(), *time_unit, false),
360 ));
361 let n_ctx =
362 PipelineContext::new(&def, pipeline_ctx.pipeline_param, pipeline_ctx.channel);
363 values_to_rows(
364 schema_info,
365 val,
366 &n_ctx,
367 None,
368 true,
369 self.tablesuffix.as_ref(),
370 )?
371 }
372 };
373
374 Ok(TransformedOutput { rows_by_context })
375 }
376
377 pub fn processors(&self) -> &processor::Processors {
378 &self.processors
379 }
380
381 pub fn transformer(&self) -> &TransformerMode {
382 &self.transformer
383 }
384
385 pub fn resolve_table_suffix(&self, value: &VrlValue) -> Option<String> {
386 ContextOpt::resolve_table_suffix(self.tablesuffix.as_ref(), value)
387 }
388
389 pub fn schemas(&self) -> Option<&Vec<greptime_proto::v1::ColumnSchema>> {
391 match &self.transformer {
392 TransformerMode::GreptimeTransformer(t) => Some(t.schemas()),
393 TransformerMode::AutoTransform(_, _) => None,
394 }
395 }
396
397 pub fn is_variant_table_name(&self) -> bool {
398 self.dispatcher.is_some() || self.tablesuffix.is_some()
401 }
402}
403
404fn transform_array_elements_by_ctx(
407 arr: &mut [VrlValue],
408 transformer: &GreptimeTransformer,
409 is_v1: bool,
410 schema_info: &mut SchemaInfo,
411 pipeline_ctx: &PipelineContext<'_>,
412 tablesuffix_template: Option<&TableSuffixTemplate>,
413) -> Result<HashMap<ContextOpt, Vec<RowWithTableSuffix>>> {
414 let skip_error = pipeline_ctx.pipeline_param.skip_error();
415 let mut rows_by_context = HashMap::new();
416
417 for (index, element) in arr.iter_mut().enumerate() {
418 if !element.is_object() {
419 unwrap_or_continue_if_err!(
420 ArrayElementMustBeObjectSnafu {
421 index,
422 actual_type: element.kind_str().to_string(),
423 }
424 .fail(),
425 skip_error
426 );
427 }
428
429 let table_suffix = ContextOpt::resolve_table_suffix(tablesuffix_template, element);
430 let values = unwrap_or_continue_if_err!(
431 transformer.transform_mut_with_schema(
432 element,
433 is_v1,
434 schema_info,
435 table_suffix.as_deref(),
436 ),
437 skip_error
438 );
439 if is_v1 {
440 let opt = unwrap_or_continue_if_err!(
442 ContextOpt::from_pipeline_map_to_opt(element),
443 skip_error
444 );
445 rows_by_context
446 .entry(opt)
447 .or_insert_with(Vec::new)
448 .push((Row { values }, table_suffix));
449 } else {
450 let mut value = element.clone();
452 let opt = unwrap_or_continue_if_err!(
453 ContextOpt::from_pipeline_map_to_opt(&mut value),
454 skip_error
455 );
456 let row = unwrap_or_continue_if_err!(
457 values_to_row(schema_info, value, pipeline_ctx, Some(values), false,)
458 .map_err(Box::new)
459 .context(TransformArrayElementSnafu { index }),
460 skip_error
461 );
462 rows_by_context
463 .entry(opt)
464 .or_default()
465 .push((row, table_suffix));
466 }
467 }
468
469 Ok(rows_by_context)
470}
471
472pub(crate) fn find_key_index(intermediate_keys: &[String], key: &str, kind: &str) -> Result<usize> {
473 intermediate_keys
474 .iter()
475 .position(|k| k == key)
476 .context(IntermediateKeyIndexSnafu { kind, key })
477}
478
479#[macro_export]
488macro_rules! setup_pipeline {
489 ($pipeline:expr) => {{
490 use std::sync::Arc;
491
492 use $crate::{GreptimePipelineParams, Pipeline, PipelineDefinition, SchemaInfo};
493
494 let pipeline: Arc<Pipeline> = Arc::new($pipeline);
495 let schema = pipeline.schemas().unwrap();
496 let schema_info = SchemaInfo::from_schema_list(schema.clone());
497
498 let pipeline_def = PipelineDefinition::Resolved(pipeline.clone());
499 let pipeline_param = GreptimePipelineParams::default();
500
501 (pipeline, schema_info, pipeline_def, pipeline_param)
502 }};
503}
504
505#[cfg(test)]
506mod tests {
507 use std::collections::BTreeMap;
508 use std::sync::Arc;
509
510 use api::v1::Rows;
511 use datatypes::schema::{FulltextOptions, SkippingIndexOptions};
512 use greptime_proto::v1::value::ValueData;
513 use greptime_proto::v1::{self, ColumnDataType, SemanticType};
514 use vrl::prelude::Bytes;
515 use vrl::value::KeyString;
516
517 use super::*;
518
519 #[test]
520 fn test_pipeline_prepare() {
521 let input_value_str = r#"
522 {
523 "my_field": "1,2",
524 "foo": "bar",
525 "ts": "1"
526 }
527 "#;
528 let input_value: serde_json::Value = serde_json::from_str(input_value_str).unwrap();
529
530 let pipeline_yaml = r#"description: 'Pipeline for Apache Tomcat'
531processors:
532 - csv:
533 field: my_field
534 target_fields: field1, field2
535 - epoch:
536 field: ts
537 resolution: ns
538transform:
539 - field: field1
540 type: uint32
541 - field: field2
542 type: uint32
543 - field: ts
544 type: timestamp, ns
545 index: time
546 "#;
547
548 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
549 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
550 let pipeline_ctx = PipelineContext::new(
551 &pipeline_def,
552 &pipeline_param,
553 session::context::Channel::Unknown,
554 );
555
556 let payload = input_value.into();
557 let mut result = pipeline
558 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
559 .unwrap()
560 .into_transformed()
561 .unwrap();
562
563 let (row, _table_suffix) = result.swap_remove(0);
564 assert_eq!(row.values[0].value_data, Some(ValueData::U32Value(1)));
565 assert_eq!(row.values[1].value_data, Some(ValueData::U32Value(2)));
566 match &row.values[2].value_data {
567 Some(ValueData::TimestampNanosecondValue(v)) => {
568 assert_ne!(v, &0);
569 }
570 _ => panic!("expect null value"),
571 }
572 }
573
574 #[test]
575 fn test_dissect_pipeline() {
576 let message = r#"129.37.245.88 - meln1ks [01/Aug/2024:14:22:47 +0800] "PATCH /observability/metrics/production HTTP/1.0" 501 33085"#.to_string();
577 let pipeline_str = r#"processors:
578 - dissect:
579 fields:
580 - message
581 patterns:
582 - "%{ip} %{?ignored} %{username} [%{ts}] \"%{method} %{path} %{proto}\" %{status} %{bytes}"
583 - date:
584 fields:
585 - ts
586 formats:
587 - "%d/%b/%Y:%H:%M:%S %z"
588
589transform:
590 - fields:
591 - ip
592 - username
593 - method
594 - path
595 - proto
596 type: string
597 - fields:
598 - status
599 type: uint16
600 - fields:
601 - bytes
602 type: uint32
603 - field: ts
604 type: timestamp, ns
605 index: time"#;
606 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_str)).unwrap();
607 let pipeline = Arc::new(pipeline);
608 let schema = pipeline.schemas().unwrap();
609 let mut schema_info = SchemaInfo::from_schema_list(schema.clone());
610
611 let pipeline_def = crate::PipelineDefinition::Resolved(pipeline.clone());
612 let pipeline_param = crate::GreptimePipelineParams::default();
613 let pipeline_ctx = PipelineContext::new(
614 &pipeline_def,
615 &pipeline_param,
616 session::context::Channel::Unknown,
617 );
618 let payload = VrlValue::Object(BTreeMap::from([(
619 KeyString::from("message"),
620 VrlValue::Bytes(Bytes::from(message)),
621 )]));
622
623 let result = pipeline
624 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
625 .unwrap()
626 .into_transformed()
627 .unwrap();
628
629 assert_eq!(schema_info.schema.len(), result[0].0.values.len());
630 let test = [
631 (
632 ColumnDataType::String as i32,
633 Some(ValueData::StringValue("129.37.245.88".into())),
634 ),
635 (
636 ColumnDataType::String as i32,
637 Some(ValueData::StringValue("meln1ks".into())),
638 ),
639 (
640 ColumnDataType::String as i32,
641 Some(ValueData::StringValue("PATCH".into())),
642 ),
643 (
644 ColumnDataType::String as i32,
645 Some(ValueData::StringValue(
646 "/observability/metrics/production".into(),
647 )),
648 ),
649 (
650 ColumnDataType::String as i32,
651 Some(ValueData::StringValue("HTTP/1.0".into())),
652 ),
653 (
654 ColumnDataType::Uint16 as i32,
655 Some(ValueData::U16Value(501)),
656 ),
657 (
658 ColumnDataType::Uint32 as i32,
659 Some(ValueData::U32Value(33085)),
660 ),
661 (
662 ColumnDataType::TimestampNanosecond as i32,
663 Some(ValueData::TimestampNanosecondValue(1722493367000000000)),
664 ),
665 ];
666 let schema = pipeline.schemas().unwrap();
668 for i in 0..schema.len() {
669 let schema = &schema[i];
670 let value = &result[0].0.values[i];
671 assert_eq!(schema.datatype, test[i].0);
672 assert_eq!(value.value_data, test[i].1);
673 }
674 }
675
676 #[test]
677 fn test_csv_pipeline() {
678 let input_value_str = r#"
679 {
680 "my_field": "1,2",
681 "foo": "bar",
682 "ts": "1"
683 }
684 "#;
685 let input_value: serde_json::Value = serde_json::from_str(input_value_str).unwrap();
686
687 let pipeline_yaml = r#"
688 description: Pipeline for Apache Tomcat
689 processors:
690 - csv:
691 field: my_field
692 target_fields: field1, field2
693 - epoch:
694 field: ts
695 resolution: ns
696 transform:
697 - field: field1
698 type: uint32
699 - field: field2
700 type: uint32
701 - field: ts
702 type: timestamp, ns
703 index: time
704 "#;
705
706 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
707 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
708 let pipeline_ctx = PipelineContext::new(
709 &pipeline_def,
710 &pipeline_param,
711 session::context::Channel::Unknown,
712 );
713
714 let payload = input_value.into();
715 let result = pipeline
716 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
717 .unwrap()
718 .into_transformed()
719 .unwrap();
720 assert_eq!(
721 result[0].0.values[0].value_data,
722 Some(ValueData::U32Value(1))
723 );
724 assert_eq!(
725 result[0].0.values[1].value_data,
726 Some(ValueData::U32Value(2))
727 );
728 match &result[0].0.values[2].value_data {
729 Some(ValueData::TimestampNanosecondValue(v)) => {
730 assert_ne!(v, &0);
731 }
732 _ => panic!("expect null value"),
733 }
734 }
735
736 #[test]
737 fn test_date_pipeline() {
738 let input_value_str = r#"
739 {
740 "my_field": "1,2",
741 "foo": "bar",
742 "test_time": "2014-5-17T04:34:56+00:00"
743 }
744 "#;
745 let input_value: serde_json::Value = serde_json::from_str(input_value_str).unwrap();
746
747 let pipeline_yaml = r#"---
748description: Pipeline for Apache Tomcat
749
750processors:
751 - date:
752 field: test_time
753
754transform:
755 - field: test_time
756 type: timestamp, ns
757 index: time
758 "#;
759
760 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
761 let pipeline = Arc::new(pipeline);
762 let schema = pipeline.schemas().unwrap();
763 let mut schema_info = SchemaInfo::from_schema_list(schema.clone());
764
765 let pipeline_def = crate::PipelineDefinition::Resolved(pipeline.clone());
766 let pipeline_param = crate::GreptimePipelineParams::default();
767 let pipeline_ctx = PipelineContext::new(
768 &pipeline_def,
769 &pipeline_param,
770 session::context::Channel::Unknown,
771 );
772 let schema = pipeline.schemas().unwrap().clone();
773 let result = input_value.into();
774
775 let rows_with_suffix = pipeline
776 .exec_mut(result, &pipeline_ctx, &mut schema_info)
777 .unwrap()
778 .into_transformed()
779 .unwrap();
780 let output = Rows {
781 schema,
782 rows: rows_with_suffix.into_iter().map(|(r, _)| r).collect(),
783 };
784 let schemas = output.schema;
785
786 assert_eq!(schemas.len(), 1);
787 let schema = schemas[0].clone();
788 assert_eq!("test_time", schema.column_name);
789 assert_eq!(ColumnDataType::TimestampNanosecond as i32, schema.datatype);
790 assert_eq!(SemanticType::Timestamp as i32, schema.semantic_type);
791
792 let row = output.rows[0].clone();
793 assert_eq!(1, row.values.len());
794 let value_data = row.values[0].clone().value_data;
795 assert_eq!(
796 Some(v1::value::ValueData::TimestampNanosecondValue(
797 1400301296000000000
798 )),
799 value_data
800 );
801 }
802
803 #[test]
804 fn test_dispatcher() {
805 let pipeline_yaml = r#"
806---
807description: Pipeline for Apache Tomcat
808
809processors:
810 - epoch:
811 field: ts
812 resolution: ns
813
814dispatcher:
815 field: typename
816 rules:
817 - value: http
818 table_suffix: http_events
819 - value: database
820 table_suffix: db_events
821 pipeline: database_pipeline
822
823transform:
824 - field: typename
825 type: string
826 - field: ts
827 type: timestamp, ns
828 index: time
829"#;
830 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
831 let dispatcher = pipeline.dispatcher.expect("expect dispatcher");
832 assert_eq!(dispatcher.field, "typename");
833
834 assert_eq!(dispatcher.rules.len(), 2);
835
836 assert_eq!(
837 dispatcher.rules[0],
838 crate::dispatcher::Rule {
839 value: VrlValue::Bytes(Bytes::from("http")),
840 table_suffix: "http_events".to_string(),
841 pipeline: None
842 }
843 );
844
845 assert_eq!(
846 dispatcher.rules[1],
847 crate::dispatcher::Rule {
848 value: VrlValue::Bytes(Bytes::from("database")),
849 table_suffix: "db_events".to_string(),
850 pipeline: Some("database_pipeline".to_string()),
851 }
852 );
853
854 let bad_yaml1 = r#"
855---
856description: Pipeline for Apache Tomcat
857
858processors:
859 - epoch:
860 field: ts
861 resolution: ns
862
863dispatcher:
864 _field: typename
865 rules:
866 - value: http
867 table_suffix: http_events
868 - value: database
869 table_suffix: db_events
870 pipeline: database_pipeline
871
872transform:
873 - field: typename
874 type: string
875 - field: ts
876 type: timestamp, ns
877 index: time
878"#;
879 let bad_yaml2 = r#"
880---
881description: Pipeline for Apache Tomcat
882
883processors:
884 - epoch:
885 field: ts
886 resolution: ns
887dispatcher:
888 field: typename
889 rules:
890 - value: http
891 _table_suffix: http_events
892 - value: database
893 _table_suffix: db_events
894 pipeline: database_pipeline
895
896transform:
897 - field: typename
898 type: string
899 - field: ts
900 type: timestamp, ns
901 index: time
902"#;
903 let bad_yaml3 = r#"
904---
905description: Pipeline for Apache Tomcat
906
907processors:
908 - epoch:
909 field: ts
910 resolution: ns
911dispatcher:
912 field: typename
913 rules:
914 - _value: http
915 table_suffix: http_events
916 - _value: database
917 table_suffix: db_events
918 pipeline: database_pipeline
919
920transform:
921 - field: typename
922 type: string
923 - field: ts
924 type: timestamp, ns
925 index: time
926"#;
927
928 let r: Result<Pipeline> = parse(&Content::Yaml(bad_yaml1));
929 assert!(r.is_err());
930 let r: Result<Pipeline> = parse(&Content::Yaml(bad_yaml2));
931 assert!(r.is_err());
932 let r: Result<Pipeline> = parse(&Content::Yaml(bad_yaml3));
933 assert!(r.is_err());
934 }
935
936 #[test]
939 fn test_one_to_many_vrl_expansion() {
940 let pipeline_yaml = r#"
941processors:
942 - epoch:
943 field: timestamp
944 resolution: ms
945 - vrl:
946 source: |
947 events = del(.events)
948 base_host = del(.host)
949 base_ts = del(.timestamp)
950 map_values(array!(events)) -> |event| {
951 {
952 "host": base_host,
953 "event_type": event.type,
954 "event_value": event.value,
955 "timestamp": base_ts
956 }
957 }
958
959transform:
960 - field: host
961 type: string
962 - field: event_type
963 type: string
964 - field: event_value
965 type: int32
966 - field: timestamp
967 type: timestamp, ms
968 index: time
969"#;
970
971 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
972 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
973 let pipeline_ctx = PipelineContext::new(
974 &pipeline_def,
975 &pipeline_param,
976 session::context::Channel::Unknown,
977 );
978
979 let input_value: serde_json::Value = serde_json::from_str(
981 r#"{
982 "host": "server1",
983 "timestamp": 1716668197217,
984 "events": [
985 {"type": "cpu", "value": 80},
986 {"type": "memory", "value": 60},
987 {"type": "disk", "value": 45}
988 ]
989 }"#,
990 )
991 .unwrap();
992
993 let payload = input_value.into();
994 let result = pipeline
995 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
996 .unwrap()
997 .into_transformed()
998 .unwrap();
999
1000 assert_eq!(result.len(), 3);
1002
1003 for (row, _table_suffix) in &result {
1005 assert_eq!(row.values.len(), 4); assert_eq!(
1008 row.values[0].value_data,
1009 Some(ValueData::StringValue("server1".to_string()))
1010 );
1011 assert_eq!(
1013 row.values[3].value_data,
1014 Some(ValueData::TimestampMillisecondValue(1716668197217))
1015 );
1016 }
1017
1018 let event_types: Vec<_> = result
1020 .iter()
1021 .map(|(r, _)| match &r.values[1].value_data {
1022 Some(ValueData::StringValue(s)) => s.clone(),
1023 _ => panic!("expected string"),
1024 })
1025 .collect();
1026 assert!(event_types.contains(&"cpu".to_string()));
1027 assert!(event_types.contains(&"memory".to_string()));
1028 assert!(event_types.contains(&"disk".to_string()));
1029 }
1030
1031 #[test]
1033 fn test_single_object_output_unchanged() {
1034 let pipeline_yaml = r#"
1035processors:
1036 - epoch:
1037 field: ts
1038 resolution: ms
1039 - vrl:
1040 source: |
1041 .processed = true
1042 .
1043
1044transform:
1045 - field: name
1046 type: string
1047 - field: processed
1048 type: boolean
1049 - field: ts
1050 type: timestamp, ms
1051 index: time
1052"#;
1053
1054 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1055 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1056 let pipeline_ctx = PipelineContext::new(
1057 &pipeline_def,
1058 &pipeline_param,
1059 session::context::Channel::Unknown,
1060 );
1061
1062 let input_value: serde_json::Value = serde_json::from_str(
1063 r#"{
1064 "name": "test",
1065 "ts": 1716668197217
1066 }"#,
1067 )
1068 .unwrap();
1069
1070 let payload = input_value.into();
1071 let result = pipeline
1072 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1073 .unwrap()
1074 .into_transformed()
1075 .unwrap();
1076
1077 assert_eq!(result.len(), 1);
1079 assert_eq!(
1080 result[0].0.values[0].value_data,
1081 Some(ValueData::StringValue("test".to_string()))
1082 );
1083 assert_eq!(
1084 result[0].0.values[1].value_data,
1085 Some(ValueData::BoolValue(true))
1086 );
1087 }
1088
1089 #[test]
1091 fn test_empty_array_produces_zero_rows() {
1092 let pipeline_yaml = r#"
1093processors:
1094 - vrl:
1095 source: |
1096 .events
1097
1098transform:
1099 - field: value
1100 type: int32
1101 - field: greptime_timestamp
1102 type: timestamp, ns
1103 index: time
1104"#;
1105
1106 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1107 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1108 let pipeline_ctx = PipelineContext::new(
1109 &pipeline_def,
1110 &pipeline_param,
1111 session::context::Channel::Unknown,
1112 );
1113
1114 let input_value: serde_json::Value = serde_json::from_str(r#"{"events": []}"#).unwrap();
1115
1116 let payload = input_value.into();
1117 let result = pipeline
1118 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1119 .unwrap()
1120 .into_transformed()
1121 .unwrap();
1122
1123 assert_eq!(result.len(), 0);
1125 }
1126
1127 #[test]
1129 fn test_array_element_must_be_object() {
1130 let pipeline_yaml = r#"
1131processors:
1132 - vrl:
1133 source: |
1134 .items
1135
1136transform:
1137 - field: value
1138 type: int32
1139 - field: greptime_timestamp
1140 type: timestamp, ns
1141 index: time
1142"#;
1143
1144 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1145 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1146 let pipeline_ctx = PipelineContext::new(
1147 &pipeline_def,
1148 &pipeline_param,
1149 session::context::Channel::Unknown,
1150 );
1151
1152 let input_value: serde_json::Value =
1154 serde_json::from_str(r#"{"items": [1, 2, 3]}"#).unwrap();
1155
1156 let payload = input_value.into();
1157 let result = pipeline.exec_mut(payload, &pipeline_ctx, &mut schema_info);
1158
1159 assert!(result.is_err());
1160 let err_msg = result.unwrap_err().to_string();
1161 assert!(
1162 err_msg.contains("must be an object"),
1163 "Expected error about non-object element, got: {}",
1164 err_msg
1165 );
1166 }
1167
1168 #[test]
1170 fn test_one_to_many_with_table_suffix_hint() {
1171 let pipeline_yaml = r#"
1172processors:
1173 - epoch:
1174 field: ts
1175 resolution: ms
1176 - vrl:
1177 source: |
1178 .greptime_table_suffix = "_" + string!(.category)
1179 .
1180
1181transform:
1182 - field: name
1183 type: string
1184 - field: category
1185 type: string
1186 - field: ts
1187 type: timestamp, ms
1188 index: time
1189"#;
1190
1191 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1192 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1193 let pipeline_ctx = PipelineContext::new(
1194 &pipeline_def,
1195 &pipeline_param,
1196 session::context::Channel::Unknown,
1197 );
1198
1199 let input_value: serde_json::Value = serde_json::from_str(
1200 r#"{
1201 "name": "test",
1202 "category": "metrics",
1203 "ts": 1716668197217
1204 }"#,
1205 )
1206 .unwrap();
1207
1208 let payload = input_value.into();
1209 let result = pipeline
1210 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1211 .unwrap()
1212 .into_transformed()
1213 .unwrap();
1214
1215 assert_eq!(result.len(), 1);
1217 assert_eq!(result[0].1, Some("_metrics".to_string()));
1218 }
1219
1220 #[test]
1222 fn test_one_to_many_per_row_table_suffix() {
1223 let pipeline_yaml = r#"
1224processors:
1225 - epoch:
1226 field: timestamp
1227 resolution: ms
1228 - vrl:
1229 source: |
1230 events = del(.events)
1231 base_ts = del(.timestamp)
1232
1233 map_values(array!(events)) -> |event| {
1234 suffix = "_" + string!(event.category)
1235 {
1236 "name": event.name,
1237 "value": event.value,
1238 "timestamp": base_ts,
1239 "greptime_table_suffix": suffix
1240 }
1241 }
1242
1243transform:
1244 - field: name
1245 type: string
1246 - field: value
1247 type: int32
1248 - field: timestamp
1249 type: timestamp, ms
1250 index: time
1251"#;
1252
1253 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1254 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1255 let pipeline_ctx = PipelineContext::new(
1256 &pipeline_def,
1257 &pipeline_param,
1258 session::context::Channel::Unknown,
1259 );
1260
1261 let input_value: serde_json::Value = serde_json::from_str(
1263 r#"{
1264 "timestamp": 1716668197217,
1265 "events": [
1266 {"name": "cpu_usage", "value": 80, "category": "cpu"},
1267 {"name": "mem_usage", "value": 60, "category": "memory"},
1268 {"name": "cpu_temp", "value": 45, "category": "cpu"}
1269 ]
1270 }"#,
1271 )
1272 .unwrap();
1273
1274 let payload = input_value.into();
1275 let result = pipeline
1276 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1277 .unwrap()
1278 .into_transformed()
1279 .unwrap();
1280
1281 assert_eq!(result.len(), 3);
1283
1284 let table_suffixes: Vec<_> = result.iter().map(|(_, suffix)| suffix.clone()).collect();
1286
1287 assert!(table_suffixes.contains(&Some("_cpu".to_string())));
1289 assert!(table_suffixes.contains(&Some("_memory".to_string())));
1290
1291 let cpu_count = table_suffixes
1293 .iter()
1294 .filter(|s| *s == &Some("_cpu".to_string()))
1295 .count();
1296 let memory_count = table_suffixes
1297 .iter()
1298 .filter(|s| *s == &Some("_memory".to_string()))
1299 .count();
1300 assert_eq!(cpu_count, 2);
1301 assert_eq!(memory_count, 1);
1302 }
1303
1304 #[test]
1306 fn test_one_to_many_hashmap_contextopt_preservation() {
1307 let pipeline_yaml = r#"
1308processors:
1309 - epoch:
1310 field: timestamp
1311 resolution: ms
1312 - vrl:
1313 source: |
1314 events = del(.events)
1315 base_ts = del(.timestamp)
1316
1317 map_values(array!(events)) -> |event| {
1318 # Set different TTL values per event type
1319 ttl = if event.type == "critical" {
1320 "1h"
1321 } else if event.type == "warning" {
1322 "24h"
1323 } else {
1324 "7d"
1325 }
1326
1327 {
1328 "host": del(.host),
1329 "event_type": event.type,
1330 "event_value": event.value,
1331 "timestamp": base_ts,
1332 "greptime_ttl": ttl
1333 }
1334 }
1335
1336transform:
1337 - field: host
1338 type: string
1339 - field: event_type
1340 type: string
1341 - field: event_value
1342 type: int32
1343 - field: timestamp
1344 type: timestamp, ms
1345 index: time
1346"#;
1347
1348 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1349 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1350 let pipeline_ctx = PipelineContext::new(
1351 &pipeline_def,
1352 &pipeline_param,
1353 session::context::Channel::Unknown,
1354 );
1355
1356 let input_value: serde_json::Value = serde_json::from_str(
1358 r#"{
1359 "host": "server1",
1360 "timestamp": 1716668197217,
1361 "events": [
1362 {"type": "critical", "value": 100},
1363 {"type": "warning", "value": 50},
1364 {"type": "info", "value": 25}
1365 ]
1366 }"#,
1367 )
1368 .unwrap();
1369
1370 let payload = input_value.into();
1371 let result = pipeline
1372 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1373 .unwrap();
1374
1375 let rows_by_context = result.into_transformed_hashmap().unwrap();
1377
1378 assert_eq!(rows_by_context.len(), 3);
1380
1381 let mut context_opts = Vec::new();
1383 for (opt, rows) in &rows_by_context {
1384 assert_eq!(rows.len(), 1); context_opts.push(opt.clone());
1386 }
1387
1388 assert_ne!(context_opts[0], context_opts[1]);
1390 assert_ne!(context_opts[1], context_opts[2]);
1391 assert_ne!(context_opts[0], context_opts[2]);
1392
1393 for rows in rows_by_context.values() {
1395 for (row, _table_suffix) in rows {
1396 assert_eq!(row.values.len(), 4); }
1398 }
1399 }
1400
1401 #[test]
1403 fn test_single_object_hashmap_compatibility() {
1404 let pipeline_yaml = r#"
1405processors:
1406 - epoch:
1407 field: ts
1408 resolution: ms
1409 - vrl:
1410 source: |
1411 .processed = true
1412 .
1413
1414transform:
1415 - field: name
1416 type: string
1417 - field: processed
1418 type: boolean
1419 - field: ts
1420 type: timestamp, ms
1421 index: time
1422"#;
1423
1424 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1425 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1426 let pipeline_ctx = PipelineContext::new(
1427 &pipeline_def,
1428 &pipeline_param,
1429 session::context::Channel::Unknown,
1430 );
1431
1432 let input_value: serde_json::Value = serde_json::from_str(
1433 r#"{
1434 "name": "test",
1435 "ts": 1716668197217
1436 }"#,
1437 )
1438 .unwrap();
1439
1440 let payload = input_value.into();
1441 let result = pipeline
1442 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1443 .unwrap();
1444
1445 let rows_by_context = result.into_transformed_hashmap().unwrap();
1447
1448 assert_eq!(rows_by_context.len(), 1);
1450
1451 let (_opt, rows) = rows_by_context.into_iter().next().unwrap();
1452 assert_eq!(rows.len(), 1);
1453
1454 let (row, _table_suffix) = &rows[0];
1456 assert_eq!(row.values.len(), 3); }
1458
1459 #[test]
1461 fn test_empty_array_hashmap() {
1462 let pipeline_yaml = r#"
1463processors:
1464 - vrl:
1465 source: |
1466 .events
1467
1468transform:
1469 - field: value
1470 type: int32
1471 - field: greptime_timestamp
1472 type: timestamp, ns
1473 index: time
1474"#;
1475
1476 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1477 let (pipeline, mut schema_info, pipeline_def, pipeline_param) = setup_pipeline!(pipeline);
1478 let pipeline_ctx = PipelineContext::new(
1479 &pipeline_def,
1480 &pipeline_param,
1481 session::context::Channel::Unknown,
1482 );
1483
1484 let input_value: serde_json::Value = serde_json::from_str(r#"{"events": []}"#).unwrap();
1485
1486 let payload = input_value.into();
1487 let result = pipeline
1488 .exec_mut(payload, &pipeline_ctx, &mut schema_info)
1489 .unwrap();
1490
1491 let rows_by_context = result.into_transformed_hashmap().unwrap();
1493
1494 assert_eq!(rows_by_context.len(), 0);
1496 }
1497
1498 #[test]
1499 fn test_pipeline_detailed_index_options_roundtrip() {
1500 let pipeline_yaml = r#"
1501transform:
1502 - field: message
1503 type: string
1504 index:
1505 type: fulltext
1506 options:
1507 analyzer: Chinese
1508 case_sensitive: true
1509 backend: tantivy
1510 - field: trace_id
1511 type: int64
1512 index:
1513 type: skipping
1514 options:
1515 granularity: 2048
1516 false_positive_rate: 0.02
1517 type: BLOOM
1518 - field: ts
1519 type: timestamp, ns
1520 index: time
1521"#;
1522
1523 let pipeline: Pipeline = parse(&Content::Yaml(pipeline_yaml)).unwrap();
1524 let schema = pipeline.schemas().unwrap().clone();
1525
1526 let message = schema
1527 .iter()
1528 .find(|column| column.column_name == "message")
1529 .unwrap();
1530 let trace_id = schema
1531 .iter()
1532 .find(|column| column.column_name == "trace_id")
1533 .unwrap();
1534 let message_options = message.options.clone();
1535 let trace_id_options = trace_id.options.clone();
1536
1537 let fulltext: FulltextOptions = serde_json::from_str(
1538 message
1539 .options
1540 .as_ref()
1541 .unwrap()
1542 .options
1543 .get("fulltext")
1544 .unwrap(),
1545 )
1546 .unwrap();
1547 assert!(fulltext.enable);
1548 assert_eq!(fulltext.analyzer.to_string(), "Chinese");
1549 assert!(fulltext.case_sensitive);
1550 assert_eq!(fulltext.backend.to_string(), "tantivy");
1551
1552 let skipping: SkippingIndexOptions = serde_json::from_str(
1553 trace_id
1554 .options
1555 .as_ref()
1556 .unwrap()
1557 .options
1558 .get("skipping_index")
1559 .unwrap(),
1560 )
1561 .unwrap();
1562 assert_eq!(skipping.granularity, 2048);
1563 assert_eq!(skipping.false_positive_rate(), 0.02);
1564 assert_eq!(skipping.index_type.to_string(), "BLOOM");
1565
1566 let roundtrip_schema = SchemaInfo::from_schema_list(schema)
1567 .column_schemas()
1568 .unwrap();
1569 let roundtrip_message = roundtrip_schema
1570 .iter()
1571 .find(|column| column.column_name == "message")
1572 .unwrap();
1573 let roundtrip_trace_id = roundtrip_schema
1574 .iter()
1575 .find(|column| column.column_name == "trace_id")
1576 .unwrap();
1577
1578 assert_eq!(message_options, roundtrip_message.options);
1579 assert_eq!(trace_id_options, roundtrip_trace_id.options);
1580 }
1581}