1use std::collections::BTreeMap;
19
20use snafu::OptionExt as _;
21use vrl::prelude::Bytes;
22use vrl::value::{KeyString, Value as VrlValue};
23
24use crate::Processor;
25use crate::error::{
26 Error, FieldMustBeTypeSnafu, KeyMustBeStringSnafu, LogfmtParseSnafu,
27 ProcessorMissingFieldSnafu, Result, ValueMustBeMapSnafu,
28};
29use crate::etl::field::Fields;
30use crate::etl::processor::{
31 FIELD_NAME, FIELDS_NAME, IGNORE_MISSING_NAME, yaml_bool, yaml_new_field, yaml_new_fields,
32};
33
34pub(crate) const PROCESSOR_LOGFMT: &str = "logfmt";
35
36#[derive(Debug, Default)]
44pub struct LogfmtProcessor {
45 fields: Fields,
46 ignore_missing: bool,
47}
48
49impl TryFrom<&yaml_rust::yaml::Hash> for LogfmtProcessor {
50 type Error = Error;
51
52 fn try_from(value: &yaml_rust::yaml::Hash) -> std::result::Result<Self, Self::Error> {
53 let mut fields = Fields::default();
54 let mut ignore_missing = false;
55
56 for (k, v) in value.iter() {
57 let key = k
58 .as_str()
59 .with_context(|| KeyMustBeStringSnafu { k: k.clone() })?;
60 match key {
61 FIELD_NAME => {
62 fields = Fields::one(yaml_new_field(v, FIELD_NAME)?);
63 }
64 FIELDS_NAME => {
65 fields = yaml_new_fields(v, FIELDS_NAME)?;
66 }
67 IGNORE_MISSING_NAME => {
68 ignore_missing = yaml_bool(v, IGNORE_MISSING_NAME)?;
69 }
70 _ => {}
71 }
72 }
73
74 Ok(LogfmtProcessor {
75 fields,
76 ignore_missing,
77 })
78 }
79}
80
81impl LogfmtProcessor {
82 fn process_field(&self, val: &VrlValue) -> Result<VrlValue> {
83 let Some(line) = val.as_str() else {
84 return FieldMustBeTypeSnafu {
85 field: val.to_string(),
86 ty: "string",
87 }
88 .fail();
89 };
90 let pairs = parse_logfmt(&line).map_err(|reason| {
91 LogfmtParseSnafu {
92 input: line.to_string(),
93 reason,
94 }
95 .build()
96 })?;
97 Ok(VrlValue::Object(pairs))
98 }
99}
100
101impl Processor for LogfmtProcessor {
102 fn kind(&self) -> &str {
103 PROCESSOR_LOGFMT
104 }
105
106 fn ignore_missing(&self) -> bool {
107 self.ignore_missing
108 }
109
110 fn exec_mut(&self, mut val: VrlValue) -> Result<VrlValue> {
111 for field in self.fields.iter() {
112 let index = field.input_field();
113 let val = val.as_object_mut().context(ValueMustBeMapSnafu)?;
114 match val.get(index) {
115 Some(VrlValue::Null) | None => {
116 if !self.ignore_missing {
117 return ProcessorMissingFieldSnafu {
118 processor: self.kind(),
119 field: field.input_field(),
120 }
121 .fail();
122 }
123 }
124 Some(v) => {
125 let processed = self.process_field(v)?;
126 let output_index = field.target_or_input_field();
127 val.insert(KeyString::from(output_index.to_string()), processed);
128 }
129 }
130 }
131 Ok(val)
132 }
133}
134
135fn parse_logfmt(line: &str) -> std::result::Result<BTreeMap<KeyString, VrlValue>, String> {
144 let mut output = BTreeMap::new();
145 let mut chars = line.char_indices().peekable();
146
147 loop {
148 while chars.next_if(|(_, c)| c.is_whitespace()).is_some() {}
150 let Some(&(key_start, first)) = chars.peek() else {
151 break;
152 };
153 if first == '=' || first == '"' {
154 return Err(format!("unexpected '{first}' at position {key_start}"));
155 }
156
157 let mut key_end = line.len();
158 while let Some(&(i, c)) = chars.peek() {
159 if c == '=' || c.is_whitespace() {
160 key_end = i;
161 break;
162 }
163 if c == '"' || c.is_control() {
164 return Err(format!("invalid character {c:?} in key at position {i}"));
165 }
166 chars.next();
167 }
168 let key = KeyString::from(&line[key_start..key_end]);
169
170 if chars.next_if(|(_, c)| *c == '=').is_none() {
171 output.insert(key, VrlValue::Boolean(true));
173 continue;
174 }
175
176 let value = match chars.peek() {
177 Some(&(_, '"')) => {
178 chars.next();
179 parse_quoted_value(&mut chars)?
180 }
181 Some(&(value_start, _)) => {
182 let mut value_end = line.len();
183 while let Some(&(i, c)) = chars.peek() {
184 if c.is_whitespace() {
185 value_end = i;
186 break;
187 }
188 if c == '"' || c == '=' {
189 return Err(format!(
190 "unexpected '{c}' in unquoted value at position {i}"
191 ));
192 }
193 chars.next();
194 }
195 line[value_start..value_end].to_string()
196 }
197 None => String::new(),
198 };
199 output.insert(key, VrlValue::Bytes(Bytes::from(value)));
200 }
201
202 Ok(output)
203}
204
205fn parse_quoted_value(
207 chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>,
208) -> std::result::Result<String, String> {
209 let mut value = String::new();
210 loop {
211 let Some((i, c)) = chars.next() else {
212 return Err("unterminated quoted value".to_string());
213 };
214 match c {
215 '"' => {
216 if let Some(&(j, next)) = chars.peek()
217 && !next.is_whitespace()
218 {
219 return Err(format!(
220 "expect whitespace after quoted value at position {j}, found '{next}'"
221 ));
222 }
223 return Ok(value);
224 }
225 '\\' => {
226 let Some((_, escaped)) = chars.next() else {
227 return Err("unterminated escape sequence".to_string());
228 };
229 match escaped {
230 '"' => value.push('"'),
231 '\\' => value.push('\\'),
232 '/' => value.push('/'),
233 'n' => value.push('\n'),
234 'r' => value.push('\r'),
235 't' => value.push('\t'),
236 'b' => value.push('\u{8}'),
237 'f' => value.push('\u{c}'),
238 'u' => {
239 let hex: String = chars.by_ref().take(4).map(|(_, c)| c).collect();
240 let decoded = (hex.len() == 4)
241 .then(|| u32::from_str_radix(&hex, 16).ok())
242 .flatten()
243 .and_then(char::from_u32)
244 .ok_or_else(|| format!("invalid unicode escape '\\u{hex}'"))?;
245 value.push(decoded);
246 }
247 other => {
248 return Err(format!("invalid escape '\\{other}' at position {i}"));
249 }
250 }
251 }
252 c => value.push(c),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 fn obj(pairs: &[(&str, VrlValue)]) -> BTreeMap<KeyString, VrlValue> {
262 pairs
263 .iter()
264 .map(|(k, v)| (KeyString::from(*k), v.clone()))
265 .collect()
266 }
267
268 fn s(v: &str) -> VrlValue {
269 VrlValue::Bytes(Bytes::from(v.to_string()))
270 }
271
272 #[test]
273 fn test_parse_logfmt() {
274 let parsed = parse_logfmt(
275 r#"ts=2026-10-14T10:00:00Z level=info msg="hello \"world\"" path=/api/v1 empty= debug"#,
276 )
277 .unwrap();
278 assert_eq!(
279 parsed,
280 obj(&[
281 ("ts", s("2026-10-14T10:00:00Z")),
282 ("level", s("info")),
283 ("msg", s(r#"hello "world""#)),
284 ("path", s("/api/v1")),
285 ("empty", s("")),
286 ("debug", VrlValue::Boolean(true)),
287 ])
288 );
289 }
290
291 #[test]
292 fn test_parse_logfmt_whitespace_escapes_and_duplicates() {
293 let parsed = parse_logfmt(" a=1\tb=\"x\\ny\\u00e9\" a=2 ").unwrap();
294 assert_eq!(parsed, obj(&[("a", s("2")), ("b", s("x\nyƩ"))]));
295 assert!(parse_logfmt("").unwrap().is_empty());
296 assert!(parse_logfmt(" ").unwrap().is_empty());
297 assert_eq!(parse_logfmt("k=\"\"").unwrap(), obj(&[("k", s(""))]));
298 assert_eq!(parse_logfmt("k=").unwrap(), obj(&[("k", s(""))]));
299 }
300
301 #[test]
302 fn test_parse_logfmt_invalid() {
303 for input in [
304 "=value",
305 "\"key\"=value",
306 "msg=\"unterminated",
307 "msg=\"bad\\q\"",
308 "msg=\"a\"b",
309 "k=a\"b",
310 "k=a=b",
311 "msg=\"\\u12\"",
312 ] {
313 assert!(parse_logfmt(input).is_err(), "expect error for {input:?}");
314 }
315 }
316
317 #[test]
318 fn test_logfmt_processor() {
319 let processor = LogfmtProcessor::default();
320 let result = processor.process_field(&s("level=warn code=42")).unwrap();
321 assert_eq!(
322 result,
323 VrlValue::Object(obj(&[("code", s("42")), ("level", s("warn"))]))
324 );
325 assert!(processor.process_field(&VrlValue::Integer(1)).is_err());
326 assert!(processor.process_field(&s("msg=\"oops")).is_err());
327 }
328}