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 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}