vrl/stdlib/
log.rs

1use crate::compiler::function::EnumVariant;
2use crate::compiler::prelude::*;
3use std::sync::LazyLock;
4
5static DEFAULT_LEVEL: LazyLock<Value> = LazyLock::new(|| Value::Bytes(Bytes::from("info")));
6static DEFAULT_RATE_LIMIT_SECS: LazyLock<Value> = LazyLock::new(|| Value::Integer(1));
7
8static LEVEL_ENUM: &[EnumVariant] = &[
9    EnumVariant {
10        value: "trace",
11        description: "Log at the `trace` level.",
12    },
13    EnumVariant {
14        value: "debug",
15        description: "Log at the `debug` level.",
16    },
17    EnumVariant {
18        value: "info",
19        description: "Log at the `info` level.",
20    },
21    EnumVariant {
22        value: "warn",
23        description: "Log at the `warn` level.",
24    },
25    EnumVariant {
26        value: "error",
27        description: "Log at the `error` level.",
28    },
29];
30
31static PARAMETERS: LazyLock<Vec<Parameter>> = LazyLock::new(|| {
32    vec![
33        Parameter::required("value", kind::ANY, "The value to log."),
34        Parameter::optional("level", kind::BYTES, "The log level.")
35            .default(&DEFAULT_LEVEL)
36            .enum_variants(LEVEL_ENUM),
37        Parameter::optional("rate_limit_secs", kind::INTEGER, "Specifies that the log message is output no more than once per the given number of seconds.
38Use a value of `0` to turn rate limiting off.")
39            .default(&DEFAULT_RATE_LIMIT_SECS),
40    ]
41});
42
43#[derive(Clone, Copy, Debug)]
44pub struct Log;
45
46impl Function for Log {
47    fn identifier(&self) -> &'static str {
48        "log"
49    }
50
51    fn usage(&self) -> &'static str {
52        "Logs the `value` to [stdout](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)) at the specified `level`."
53    }
54
55    fn category(&self) -> &'static str {
56        Category::Debug.as_ref()
57    }
58
59    fn return_kind(&self) -> u16 {
60        kind::NULL
61    }
62
63    fn pure(&self) -> bool {
64        false
65    }
66
67    fn parameters(&self) -> &'static [Parameter] {
68        PARAMETERS.as_slice()
69    }
70
71    fn examples(&self) -> &'static [Example] {
72        &[
73            example! {
74                title: "Log a message",
75                source: r#"log("Hello, World!", level: "info", rate_limit_secs: 60)"#,
76                result: Ok("null"),
77            },
78            example! {
79                title: "Log an error",
80                source: indoc! {r#"
81                    . = { "field": "not an integer" }
82                    _, err = to_int(.field)
83                    if err != null {
84                        log(err, level: "error")
85                    }
86                "#},
87                result: Ok("null"),
88            },
89        ]
90    }
91
92    #[cfg(not(target_arch = "wasm32"))]
93    fn compile(
94        &self,
95        state: &state::TypeState,
96        ctx: &mut FunctionCompileContext,
97        arguments: ArgumentList,
98    ) -> Compiled {
99        let levels = vec![
100            "trace".into(),
101            "debug".into(),
102            "info".into(),
103            "warn".into(),
104            "error".into(),
105        ];
106
107        let value = arguments.required("value");
108        let level = arguments
109            .optional_enum("level", &levels, state)?
110            .unwrap_or_else(|| DEFAULT_LEVEL.clone())
111            .try_bytes()
112            .expect("log level not bytes");
113        let rate_limit_secs = arguments.optional("rate_limit_secs");
114
115        Ok(implementation::LogFn {
116            span: ctx.span(),
117            value,
118            level,
119            rate_limit_secs,
120        }
121        .as_expr())
122    }
123
124    #[cfg(target_arch = "wasm32")]
125    fn compile(
126        &self,
127        _state: &state::TypeState,
128        ctx: &mut FunctionCompileContext,
129        _arguments: ArgumentList,
130    ) -> Compiled {
131        Ok(super::WasmUnsupportedFunction::new(ctx.span(), TypeDef::null().infallible()).as_expr())
132    }
133}
134
135#[cfg(not(target_arch = "wasm32"))]
136mod implementation {
137    use tracing::{debug, error, info, trace, warn};
138
139    use super::DEFAULT_RATE_LIMIT_SECS;
140    use crate::compiler::prelude::*;
141
142    pub(super) fn log(
143        rate_limit_secs: Value,
144        level: &Bytes,
145        value: &Value,
146        span: Span,
147    ) -> Resolved {
148        let rate_limit_secs = rate_limit_secs.try_integer()?;
149        let res = value.to_string_lossy();
150        match level.as_ref() {
151            b"trace" => {
152                trace!(message = %res, internal_log_rate_secs = rate_limit_secs, vrl_position = span.start());
153            }
154            b"debug" => {
155                debug!(message = %res, internal_log_rate_secs = rate_limit_secs, vrl_position = span.start());
156            }
157            b"warn" => {
158                warn!(message = %res, internal_log_rate_secs = rate_limit_secs, vrl_position = span.start());
159            }
160            b"error" => {
161                error!(message = %res, internal_log_rate_secs = rate_limit_secs, vrl_position = span.start());
162            }
163            _ => {
164                info!(message = %res, internal_log_rate_secs = rate_limit_secs, vrl_position = span.start());
165            }
166        }
167        Ok(Value::Null)
168    }
169
170    #[derive(Debug, Clone)]
171    pub(super) struct LogFn {
172        pub(super) span: Span,
173        pub(super) value: Box<dyn Expression>,
174        pub(super) level: Bytes,
175        pub(super) rate_limit_secs: Option<Box<dyn Expression>>,
176    }
177
178    impl FunctionExpression for LogFn {
179        fn resolve(&self, ctx: &mut Context) -> Resolved {
180            let value = self.value.resolve(ctx)?;
181            let rate_limit_secs = self
182                .rate_limit_secs
183                .map_resolve_with_default(ctx, || DEFAULT_RATE_LIMIT_SECS.clone())?;
184
185            let span = self.span;
186
187            log(rate_limit_secs, &self.level, &value, span)
188        }
189
190        fn type_def(&self, _: &state::TypeState) -> TypeDef {
191            TypeDef::null().infallible().impure()
192        }
193    }
194}
195
196#[cfg(all(test, not(target_arch = "wasm32")))]
197mod tests {
198    use tracing_test::traced_test;
199
200    use super::*;
201    use crate::value;
202
203    test_function![
204        log => Log;
205
206        doesnotcrash {
207            args: func_args! [ value: value!(42),
208                               level: value!("warn"),
209                               rate_limit_secs: value!(5) ],
210            want: Ok(Value::Null),
211            tdef: TypeDef::null().infallible().impure(),
212        }
213    ];
214
215    #[traced_test]
216    #[test]
217    fn output_quotes() {
218        // Check that a message is logged without additional quotes
219        implementation::log(
220            value!(1),
221            &Bytes::from("warn"),
222            &value!("simple test message"),
223            Span::default(),
224        )
225        .unwrap();
226
227        assert!(!logs_contain("\"simple test message\""));
228        assert!(logs_contain("simple test message"));
229    }
230}