1use std::{path::PathBuf, sync::Arc, time::Duration};
2
3use serde_with::serde_as;
4use snafu::{ResultExt, Snafu};
5pub use vector_lib::event::lua;
6use vector_lib::{
7 codecs::MetricTagValues,
8 configurable::configurable_component,
9 transform::runtime_transform::{RuntimeTransform, Timer},
10};
11
12use crate::{
13 config::{self, CONFIG_PATHS, ComponentKey, DataType, Input, OutputId, TransformOutput},
14 event::{Event, lua::event::LuaEvent},
15 internal_events::{LuaBuildError, LuaGcTriggered},
16 schema,
17 schema::Definition,
18 transforms::Transform,
19};
20
21#[derive(Debug, Snafu)]
22pub enum BuildError {
23 #[snafu(display("Invalid \"search_dirs\": {}", source))]
24 InvalidSearchDirs { source: mlua::Error },
25 #[snafu(display("Cannot evaluate Lua code in \"source\": {}", source))]
26 InvalidSource { source: mlua::Error },
27
28 #[snafu(display("Cannot evaluate Lua code defining \"hooks.init\": {}", source))]
29 InvalidHooksInit { source: mlua::Error },
30 #[snafu(display("Cannot evaluate Lua code defining \"hooks.process\": {}", source))]
31 InvalidHooksProcess { source: mlua::Error },
32 #[snafu(display("Cannot evaluate Lua code defining \"hooks.shutdown\": {}", source))]
33 InvalidHooksShutdown { source: mlua::Error },
34 #[snafu(display("Cannot evaluate Lua code defining timer handler: {}", source))]
35 InvalidTimerHandler { source: mlua::Error },
36
37 #[snafu(display("Runtime error in \"hooks.init\" function: {}", source))]
38 RuntimeErrorHooksInit { source: mlua::Error },
39 #[snafu(display("Runtime error in \"hooks.process\" function: {}", source))]
40 RuntimeErrorHooksProcess { source: mlua::Error },
41 #[snafu(display("Runtime error in \"hooks.shutdown\" function: {}", source))]
42 RuntimeErrorHooksShutdown { source: mlua::Error },
43 #[snafu(display("Runtime error in timer handler: {}", source))]
44 RuntimeErrorTimerHandler { source: mlua::Error },
45
46 #[snafu(display("Cannot call GC in Lua runtime: {}", source))]
47 RuntimeErrorGc { source: mlua::Error },
48}
49
50#[configurable_component]
52#[derive(Clone, Debug)]
53#[serde(deny_unknown_fields)]
54pub struct LuaConfig {
55 #[configurable(metadata(
61 docs::examples = "function init()\n\tcount = 0\nend\n\nfunction process()\n\tcount = count + 1\nend\n\nfunction timer_handler(emit)\n\temit(make_counter(counter))\n\tcounter = 0\nend\n\nfunction shutdown(emit)\n\temit(make_counter(counter))\nend\n\nfunction make_counter(value)\n\treturn metric = {\n\t\tname = \"event_counter\",\n\t\tkind = \"incremental\",\n\t\ttimestamp = os.date(\"!*t\"),\n\t\tcounter = {\n\t\t\tvalue = value\n\t\t}\n \t}\nend",
62 docs::examples = "-- external file with hooks and timers defined\nrequire('custom_module')",
63 ))]
64 source: Option<String>,
65
66 #[serde(default = "default_config_paths")]
70 #[configurable(metadata(docs::examples = "/etc/vector/lua"))]
71 #[configurable(metadata(docs::human_name = "Search Directories"))]
72 search_dirs: Vec<PathBuf>,
73
74 #[configurable(derived)]
75 hooks: HooksConfig,
76
77 #[serde(default)]
79 timers: Vec<TimerConfig>,
80
81 #[serde(default)]
88 metric_tag_values: MetricTagValues,
89}
90
91fn default_config_paths() -> Vec<PathBuf> {
92 match CONFIG_PATHS.lock().ok() {
93 Some(config_paths) => config_paths
94 .clone()
95 .into_iter()
96 .map(|config_path| match config_path {
97 config::ConfigPath::File(mut path, _format) => {
98 path.pop();
99 path
100 }
101 config::ConfigPath::Dir(path) => path,
102 })
103 .collect(),
104 None => vec![],
105 }
106}
107
108#[configurable_component]
112#[derive(Clone, Debug)]
113#[serde(deny_unknown_fields)]
114struct HooksConfig {
115 #[configurable(metadata(
122 docs::examples = "function (emit)\n\t-- Custom Lua code here\nend",
123 docs::examples = "init",
124 ))]
125 init: Option<String>,
126
127 #[configurable(metadata(
135 docs::examples = "function (event, emit)\n\tevent.log.field = \"value\" -- set value of a field\n\tevent.log.another_field = nil -- remove field\n\tevent.log.first, event.log.second = nil, event.log.first -- rename field\n\t-- Very important! Emit the processed event.\n\temit(event)\nend",
136 docs::examples = "process",
137 ))]
138 process: String,
139
140 #[configurable(metadata(
147 docs::examples = "function (emit)\n\t-- Custom Lua code here\nend",
148 docs::examples = "shutdown",
149 ))]
150 shutdown: Option<String>,
151}
152
153#[serde_as]
155#[configurable_component]
156#[derive(Clone, Debug)]
157struct TimerConfig {
158 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
160 #[configurable(metadata(docs::human_name = "Interval"))]
161 interval_seconds: Duration,
162
163 #[configurable(metadata(docs::examples = "timer_handler"))]
171 handler: String,
172}
173
174impl LuaConfig {
175 pub fn build(&self, key: ComponentKey) -> crate::Result<Transform> {
176 Lua::new(self, key).map(Transform::event_task)
177 }
178
179 pub fn input(&self) -> Input {
180 Input::new(DataType::Metric | DataType::Log)
181 }
182
183 pub fn outputs(
184 &self,
185 input_definitions: &[(OutputId, schema::Definition)],
186 ) -> Vec<TransformOutput> {
187 let namespaces = input_definitions
189 .iter()
190 .flat_map(|(_output, definition)| definition.log_namespaces().clone())
191 .collect();
192
193 let definition = input_definitions
194 .iter()
195 .map(|(output, _definition)| {
196 (
197 output.clone(),
198 Definition::default_for_namespace(&namespaces),
199 )
200 })
201 .collect();
202
203 vec![TransformOutput::new(
204 DataType::Metric | DataType::Log,
205 definition,
206 )]
207 }
208}
209
210const GC_INTERVAL: usize = 16;
217
218pub struct Lua {
219 lua: mlua::Lua,
220 invocations_after_gc: usize,
221 hook_init: Option<mlua::RegistryKey>,
222 hook_process: mlua::RegistryKey,
223 hook_shutdown: Option<mlua::RegistryKey>,
224 timers: Vec<(Timer, mlua::RegistryKey)>,
225 multi_value_tags: bool,
226 source_id: Arc<ComponentKey>,
227}
228
229fn make_registry_value(lua: &mlua::Lua, source: &str) -> mlua::Result<mlua::RegistryKey> {
231 lua.load(source)
232 .eval::<mlua::Function>()
233 .and_then(|f| lua.create_registry_value(f))
234}
235
236impl Lua {
237 pub fn new(config: &LuaConfig, key: ComponentKey) -> crate::Result<Self> {
238 let lua = unsafe {
241 mlua::Lua::unsafe_new_with(mlua::StdLib::ALL_SAFE, mlua::LuaOptions::default())
242 };
243
244 let additional_paths = config
245 .search_dirs
246 .iter()
247 .map(|d| format!("{}/?.lua", d.to_string_lossy()))
248 .collect::<Vec<_>>()
249 .join(";");
250
251 let mut timers = Vec::new();
252
253 if !additional_paths.is_empty() {
254 let package = lua.globals().get::<mlua::Table>("package")?;
255 let current_paths = package
256 .get::<String>("path")
257 .unwrap_or_else(|_| ";".to_string());
258 let paths = format!("{additional_paths};{current_paths}");
259 package.set("path", paths)?;
260 }
261
262 if let Some(source) = &config.source {
263 lua.load(source).eval::<()>().context(InvalidSourceSnafu)?;
264 }
265
266 let hook_init_code = config.hooks.init.as_ref();
267 let hook_init = hook_init_code
268 .map(|code| make_registry_value(&lua, code))
269 .transpose()
270 .context(InvalidHooksInitSnafu)?;
271
272 let hook_process =
273 make_registry_value(&lua, &config.hooks.process).context(InvalidHooksProcessSnafu)?;
274
275 let hook_shutdown_code = config.hooks.shutdown.as_ref();
276 let hook_shutdown = hook_shutdown_code
277 .map(|code| make_registry_value(&lua, code))
278 .transpose()
279 .context(InvalidHooksShutdownSnafu)?;
280
281 for (id, timer) in config.timers.iter().enumerate() {
282 let handler_key = lua
283 .load(&timer.handler)
284 .eval::<mlua::Function>()
285 .and_then(|f| lua.create_registry_value(f))
286 .context(InvalidTimerHandlerSnafu)?;
287
288 let timer = Timer {
289 id: id as u32,
290 interval: timer.interval_seconds,
291 };
292 timers.push((timer, handler_key));
293 }
294
295 let multi_value_tags = config.metric_tag_values == MetricTagValues::Full;
296
297 Ok(Self {
298 lua,
299 invocations_after_gc: 0,
300 timers,
301 hook_init,
302 hook_process,
303 hook_shutdown,
304 multi_value_tags,
305 source_id: Arc::new(key),
306 })
307 }
308
309 #[cfg(test)]
310 fn process(&mut self, event: Event, output: &mut Vec<Event>) -> Result<(), mlua::Error> {
311 let source_id = event.source_id().cloned();
312 let lua = &self.lua;
313 let result = lua.scope(|scope| {
314 let emit = scope.create_function_mut(|_, mut event: Event| {
315 if let Some(source_id) = &source_id {
316 event.set_source_id(Arc::clone(source_id));
317 }
318 output.push(event);
319 Ok(())
320 })?;
321
322 lua.registry_value::<mlua::Function>(&self.hook_process)?
323 .call((
324 LuaEvent {
325 event,
326 metric_multi_value_tags: self.multi_value_tags,
327 },
328 emit,
329 ))
330 });
331
332 self.attempt_gc();
333 result
334 }
335
336 #[cfg(test)]
337 fn process_single(&mut self, event: Event) -> Result<Option<Event>, mlua::Error> {
338 let mut out = Vec::new();
339 self.process(event, &mut out)?;
340 assert!(out.len() <= 1);
341 Ok(out.into_iter().next())
342 }
343
344 fn attempt_gc(&mut self) {
345 self.invocations_after_gc += 1;
346 if self.invocations_after_gc.is_multiple_of(GC_INTERVAL) {
347 emit!(LuaGcTriggered {
348 used_memory: self.lua.used_memory()
349 });
350 _ = self
351 .lua
352 .gc_collect()
353 .context(RuntimeErrorGcSnafu)
354 .map_err(|error| error!(%error, rate_limit = 30));
355 self.invocations_after_gc = 0;
356 }
357 }
358}
359
360fn wrap_emit_fn<'scope, 'env, F: 'scope + FnMut(Event)>(
362 scope: &'scope mlua::Scope<'scope, 'env>,
363 mut emit_fn: F,
364 source_id: Arc<ComponentKey>,
365) -> mlua::Result<mlua::Function> {
366 scope.create_function_mut(move |_, mut event: Event| -> mlua::Result<()> {
367 event.set_source_id(Arc::clone(&source_id));
368 emit_fn(event);
369 Ok(())
370 })
371}
372
373impl RuntimeTransform for Lua {
374 fn hook_process<F>(&mut self, event: Event, emit_fn: F)
375 where
376 F: FnMut(Event),
377 {
378 let lua = &self.lua;
379 let source_id = Arc::clone(event.source_id().unwrap_or(&self.source_id));
380 _ = lua
381 .scope(|scope| -> mlua::Result<()> {
382 lua.registry_value::<mlua::Function>(&self.hook_process)?
383 .call((
384 LuaEvent {
385 event,
386 metric_multi_value_tags: self.multi_value_tags,
387 },
388 wrap_emit_fn(scope, emit_fn, source_id)?,
389 ))
390 })
391 .context(RuntimeErrorHooksProcessSnafu)
392 .map_err(|e| emit!(LuaBuildError { error: e }));
393
394 self.attempt_gc();
395 }
396
397 fn hook_init<F>(&mut self, emit_fn: F)
398 where
399 F: FnMut(Event),
400 {
401 let lua = &self.lua;
402 _ = lua
403 .scope(|scope| -> mlua::Result<()> {
404 match &self.hook_init {
405 Some(key) => lua
406 .registry_value::<mlua::Function>(key)?
407 .call(wrap_emit_fn(scope, emit_fn, Arc::clone(&self.source_id))?),
408 None => Ok(()),
409 }
410 })
411 .context(RuntimeErrorHooksInitSnafu)
412 .map_err(|error| error!(%error, rate_limit = 30));
413
414 self.attempt_gc();
415 }
416
417 fn hook_shutdown<F>(&mut self, emit_fn: F)
418 where
419 F: FnMut(Event),
420 {
421 let lua = &self.lua;
422 _ = lua
423 .scope(|scope| -> mlua::Result<()> {
424 match &self.hook_shutdown {
425 Some(key) => lua
426 .registry_value::<mlua::Function>(key)?
427 .call(wrap_emit_fn(scope, emit_fn, Arc::clone(&self.source_id))?),
428 None => Ok(()),
429 }
430 })
431 .context(RuntimeErrorHooksShutdownSnafu)
432 .map_err(|error| error!(%error, rate_limit = 30));
433
434 self.attempt_gc();
435 }
436
437 fn timer_handler<F>(&mut self, timer: Timer, emit_fn: F)
438 where
439 F: FnMut(Event),
440 {
441 let lua = &self.lua;
442 _ = lua
443 .scope(|scope| -> mlua::Result<()> {
444 let handler_key = &self.timers[timer.id as usize].1;
445 lua.registry_value::<mlua::Function>(handler_key)?
446 .call(wrap_emit_fn(scope, emit_fn, Arc::clone(&self.source_id))?)
447 })
448 .context(RuntimeErrorTimerHandlerSnafu)
449 .map_err(|error| error!(%error, rate_limit = 30));
450
451 self.attempt_gc();
452 }
453
454 fn timers(&self) -> Vec<Timer> {
455 self.timers.iter().map(|(timer, _)| *timer).collect()
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use std::{future::Future, sync::Arc};
462
463 use indoc::indoc;
464 use similar_asserts::assert_eq;
465 use tokio::sync::{
466 Mutex,
467 mpsc::{self, Receiver, Sender},
468 };
469 use tokio_stream::wrappers::ReceiverStream;
470
471 use vrl::event_path;
472
473 use super::*;
474 use crate::{
475 event::{
476 Event, LogEvent, Value,
477 metric::{Metric, MetricKind, MetricValue},
478 },
479 test_util,
480 test_util::{components::assert_transform_compliance, random_string},
481 transforms::test::create_topology,
482 };
483
484 fn format_error(error: &mlua::Error) -> String {
485 match error {
486 mlua::Error::CallbackError { traceback, cause } => {
487 format_error(cause) + "\n" + traceback
488 }
489 err => err.to_string(),
490 }
491 }
492
493 fn from_config(config: &str) -> crate::Result<Box<Lua>> {
494 Lua::new(&serde_yaml::from_str(config).unwrap(), "transform".into()).map(Box::new)
495 }
496
497 async fn run_transform<T: Future>(
498 config: &str,
499 func: impl FnOnce(Sender<Event>, Arc<Mutex<Receiver<Event>>>) -> T,
500 ) -> T::Output {
501 test_util::trace_init();
502 assert_transform_compliance(async move {
503 let config = super::super::LuaConfig::V2(serde_yaml::from_str(config).unwrap());
504 let (tx, rx) = mpsc::channel(1);
505 let (topology, out) = create_topology(ReceiverStream::new(rx), config).await;
506
507 let out = Arc::new(tokio::sync::Mutex::new(out));
508
509 let result = func(tx, Arc::clone(&out)).await;
510
511 topology.stop().await;
512 assert_eq!(out.lock().await.recv().await, None);
513
514 result
515 })
516 .await
517 }
518
519 async fn next_event(out: &Arc<Mutex<Receiver<Event>>>, source: &str) -> Event {
520 let event = out
521 .lock()
522 .await
523 .recv()
524 .await
525 .expect("Event was not received");
526 assert_eq!(
527 event.source_id(),
528 Some(&Arc::new(ComponentKey::from(source)))
529 );
530 event
531 }
532
533 #[tokio::test]
534 async fn lua_runs_init_hook() {
535 let line1 = random_string(9);
536 run_transform(
537 &indoc::formatdoc! {r#"
538 version: "2"
539 hooks:
540 init: |
541 function (emit)
542 event = {{log={{message="{line1}"}}}}
543 emit(event)
544 end
545 process: |
546 function (event, emit)
547 emit(event)
548 end
549 "#},
550 |tx, out| async move {
551 let line2 = random_string(9);
552 tx.send(Event::Log(LogEvent::from(line2.as_str())))
553 .await
554 .unwrap();
555 drop(tx);
556 assert_eq!(
557 next_event(&out, "transform").await.as_log()["message"],
558 line1.into()
559 );
560 assert_eq!(
561 next_event(&out, "in").await.as_log()["message"],
562 line2.into(),
563 );
564 },
565 )
566 .await;
567 }
568
569 #[tokio::test]
570 async fn lua_add_field() {
571 run_transform(
572 indoc! {r#"
573 version: "2"
574 hooks:
575 process: |
576 function (event, emit)
577 event["log"]["hello"] = "goodbye"
578 emit(event)
579 end
580 "#},
581 |tx, out| async move {
582 let event = Event::Log(LogEvent::from("program me"));
583 tx.send(event).await.unwrap();
584
585 assert_eq!(
586 next_event(&out, "in").await.as_log()["hello"],
587 "goodbye".into()
588 );
589 },
590 )
591 .await;
592 }
593
594 #[tokio::test]
595 async fn lua_read_field() {
596 run_transform(
597 indoc! {r#"
598 version: "2"
599 hooks:
600 process: |
601 function (event, emit)
602 _, _, name = string.find(event.log.message, "Hello, my name is (%a+).")
603 event.log.name = name
604 emit(event)
605 end
606 "#},
607 |tx, out| async move {
608 let event = Event::Log(LogEvent::from("Hello, my name is Bob."));
609 tx.send(event).await.unwrap();
610
611 assert_eq!(next_event(&out, "in").await.as_log()["name"], "Bob".into());
612 },
613 )
614 .await;
615 }
616
617 #[tokio::test]
618 async fn lua_remove_field() {
619 run_transform(
620 indoc! {r#"
621 version: "2"
622 hooks:
623 process: |
624 function (event, emit)
625 event.log.name = nil
626 emit(event)
627 end
628 "#},
629 |tx, out| async move {
630 let mut event = LogEvent::default();
631 event.insert(event_path!("name"), "Bob");
632
633 tx.send(event.into()).await.unwrap();
634
635 assert_eq!(
636 next_event(&out, "in")
637 .await
638 .as_log()
639 .get(event_path!("name")),
640 None
641 );
642 },
643 )
644 .await;
645 }
646
647 #[tokio::test]
648 async fn lua_drop_event() {
649 run_transform(
650 indoc! {r#"
651 version: "2"
652 hooks:
653 process: |
654 function (event, emit)
655 -- emit nothing
656 end
657 "#},
658 |tx, _out| async move {
659 let event = LogEvent::default().into();
660 tx.send(event).await.unwrap();
661
662 },
664 )
665 .await;
666 }
667
668 #[tokio::test]
669 async fn lua_duplicate_event() {
670 run_transform(
671 indoc! {r#"
672 version: "2"
673 hooks:
674 process: |
675 function (event, emit)
676 emit(event)
677 emit(event)
678 end
679 "#},
680 |tx, out| async move {
681 let mut event = LogEvent::default();
682 event.insert(event_path!("host"), "127.0.0.1");
683 tx.send(event.into()).await.unwrap();
684
685 assert!(out.lock().await.recv().await.is_some());
686 assert!(out.lock().await.recv().await.is_some());
687 },
688 )
689 .await;
690 }
691
692 #[tokio::test]
693 async fn lua_read_empty_field() {
694 run_transform(
695 indoc! {r#"
696 version: "2"
697 hooks:
698 process: |
699 function (event, emit)
700 if event["log"]["non-existent"] == nil then
701 event["log"]["result"] = "empty"
702 else
703 event["log"]["result"] = "found"
704 end
705 emit(event)
706 end
707 "#},
708 |tx, out| async move {
709 let event = LogEvent::default();
710 tx.send(event.into()).await.unwrap();
711
712 assert_eq!(
713 next_event(&out, "in").await.as_log()["result"],
714 "empty".into()
715 );
716 },
717 )
718 .await;
719 }
720
721 #[tokio::test]
722 async fn lua_integer_value() {
723 run_transform(
724 indoc! {r#"
725 version: "2"
726 hooks:
727 process: |
728 function (event, emit)
729 event["log"]["number"] = 3
730 emit(event)
731 end
732 "#},
733 |tx, out| async move {
734 let event = LogEvent::default();
735 tx.send(event.into()).await.unwrap();
736
737 assert_eq!(
738 next_event(&out, "in").await.as_log()["number"],
739 Value::Integer(3)
740 );
741 },
742 )
743 .await;
744 }
745
746 #[tokio::test]
747 async fn lua_numeric_value() {
748 run_transform(
749 indoc! {r#"
750 version: "2"
751 hooks:
752 process: |
753 function (event, emit)
754 event["log"]["number"] = 3.14159
755 emit(event)
756 end
757 "#},
758 |tx, out| async move {
759 let event = LogEvent::default();
760 tx.send(event.into()).await.unwrap();
761
762 assert_eq!(
763 next_event(&out, "in").await.as_log()["number"],
764 Value::from(3.14159)
765 );
766 },
767 )
768 .await;
769 }
770
771 #[tokio::test]
772 async fn lua_boolean_value() {
773 run_transform(
774 indoc! {r#"
775 version: "2"
776 hooks:
777 process: |
778 function (event, emit)
779 event["log"]["bool"] = true
780 emit(event)
781 end
782 "#},
783 |tx, out| async move {
784 let event = LogEvent::default();
785 tx.send(event.into()).await.unwrap();
786
787 assert_eq!(
788 next_event(&out, "in").await.as_log()["bool"],
789 Value::Boolean(true)
790 );
791 },
792 )
793 .await;
794 }
795
796 #[tokio::test]
797 async fn lua_non_coercible_value() {
798 run_transform(
799 indoc! {r#"
800 version: "2"
801 hooks:
802 process: |
803 function (event, emit)
804 event["log"]["junk"] = nil
805 emit(event)
806 end
807 "#},
808 |tx, out| async move {
809 let event = LogEvent::default();
810 tx.send(event.into()).await.unwrap();
811
812 assert_eq!(
813 next_event(&out, "in")
814 .await
815 .as_log()
816 .get(event_path!("junk")),
817 None
818 );
819 },
820 )
821 .await;
822 }
823
824 #[tokio::test]
825 async fn lua_non_string_key_write() -> crate::Result<()> {
826 let mut transform = from_config(indoc! {r#"
827 hooks:
828 process: |
829 function (event, emit)
830 event["log"][false] = "hello"
831 emit(event)
832 end
833 "#})
834 .unwrap();
835
836 let err = transform
837 .process_single(LogEvent::default().into())
838 .unwrap_err();
839 let err = format_error(&err);
840 assert!(
841 err.contains("error converting Lua boolean to String"),
842 "{}",
843 err
844 );
845 Ok(())
846 }
847
848 #[tokio::test]
849 async fn lua_non_string_key_read() {
850 run_transform(
851 indoc! {r#"
852 version: "2"
853 hooks:
854 process: |
855 function (event, emit)
856 event.log.result = event.log[false]
857 emit(event)
858 end
859 "#},
860 |tx, out| async move {
861 let event = LogEvent::default();
862 tx.send(event.into()).await.unwrap();
863
864 assert_eq!(
865 next_event(&out, "in")
866 .await
867 .as_log()
868 .get(event_path!("result")),
869 None
870 );
871 },
872 )
873 .await;
874 }
875
876 #[tokio::test]
877 async fn lua_script_error() -> crate::Result<()> {
878 let mut transform = from_config(indoc! {r#"
879 hooks:
880 process: |
881 function (event, emit)
882 error("this is an error")
883 end
884 "#})
885 .unwrap();
886
887 let err = transform
888 .process_single(LogEvent::default().into())
889 .unwrap_err();
890 let err = format_error(&err);
891 assert!(err.contains("this is an error"), "{}", err);
892 Ok(())
893 }
894
895 #[tokio::test]
896 async fn lua_syntax_error() -> crate::Result<()> {
897 let err = from_config(indoc! {r#"
898 hooks:
899 process: |
900 function (event, emit)
901 1234 = sadf <>&*!#@
902 end
903 "#})
904 .map(|_| ())
905 .unwrap_err()
906 .to_string();
907
908 assert!(err.contains("syntax error:"), "{}", err);
909 Ok(())
910 }
911
912 #[tokio::test]
913 async fn lua_load_file() {
914 use std::{fs::File, io::Write};
915
916 let dir = tempfile::tempdir().unwrap();
917 let mut file = File::create(dir.path().join("script2.lua")).unwrap();
918 write!(
919 &mut file,
920 r#"
921 local M = {{}}
922
923 local function modify(event2)
924 event2["log"]["new field"] = "new value"
925 end
926 M.modify = modify
927
928 return M
929 "#
930 )
931 .unwrap();
932
933 run_transform(
934 &indoc::formatdoc! {r#"
935 version: "2"
936 hooks:
937 process: |
938 function (event, emit)
939 local script2 = require("script2")
940 script2.modify(event)
941 emit(event)
942 end
943 search_dirs:
944 - {dir}
945 "#,
946 dir = dir.path().as_os_str().to_string_lossy(), },
948 |tx, out| async move {
949 let event = LogEvent::default();
950 tx.send(event.into()).await.unwrap();
951
952 assert_eq!(
953 next_event(&out, "in").await.as_log()["\"new field\""],
954 "new value".into()
955 );
956 },
957 )
958 .await;
959 }
960
961 #[tokio::test]
962 async fn lua_pairs() {
963 run_transform(
964 indoc! {r#"
965 version: "2"
966 hooks:
967 process: |
968 function (event, emit)
969 for k,v in pairs(event.log) do
970 event.log[k] = k .. v
971 end
972 emit(event)
973 end
974 "#},
975 |tx, out| async move {
976 let mut event = LogEvent::default();
977 event.insert(event_path!("name"), "Bob");
978 event.insert(event_path!("friend"), "Alice");
979 tx.send(event.into()).await.unwrap();
980
981 let output = next_event(&out, "in").await;
982
983 assert_eq!(output.as_log()["name"], "nameBob".into());
984 assert_eq!(output.as_log()["friend"], "friendAlice".into());
985 },
986 )
987 .await;
988 }
989
990 #[tokio::test]
991 async fn lua_metric() {
992 run_transform(
993 indoc! {r#"
994 version: "2"
995 hooks:
996 process: |
997 function (event, emit)
998 event.metric.counter.value = event.metric.counter.value + 1
999 emit(event)
1000 end
1001 "#},
1002 |tx, out| async move {
1003 let metric = Metric::new(
1004 "example counter",
1005 MetricKind::Absolute,
1006 MetricValue::Counter { value: 1.0 },
1007 );
1008
1009 let mut expected = metric
1010 .clone()
1011 .with_value(MetricValue::Counter { value: 2.0 });
1012 let metadata = expected.metadata_mut();
1013 metadata.set_upstream_id(Arc::new(OutputId::from("transform")));
1014 metadata.set_source_id(Arc::new(ComponentKey::from("in")));
1015
1016 tx.send(metric.into()).await.unwrap();
1017
1018 assert_eq!(next_event(&out, "in").await.as_metric(), &expected);
1019 },
1020 )
1021 .await;
1022 }
1023
1024 #[tokio::test]
1025 async fn lua_multiple_events() {
1026 run_transform(
1027 indoc! {r#"
1028 version: "2"
1029 hooks:
1030 process: |
1031 function (event, emit)
1032 event["log"]["hello"] = "goodbye"
1033 emit(event)
1034 end
1035 "#},
1036 |tx, out| async move {
1037 let n: usize = 10;
1038 let events = (0..n).map(|i| Event::Log(LogEvent::from(format!("program me {i}"))));
1039 for event in events {
1040 tx.send(event).await.unwrap();
1041 assert!(out.lock().await.recv().await.is_some());
1042 }
1043 },
1044 )
1045 .await;
1046 }
1047}