diff --git a/public/app/features/transformers/extractFields/fieldExtractor.test.ts b/public/app/features/transformers/extractFields/fieldExtractor.test.ts index 10974eaaa43..fd1b62a7d8d 100644 --- a/public/app/features/transformers/extractFields/fieldExtractor.test.ts +++ b/public/app/features/transformers/extractFields/fieldExtractor.test.ts @@ -14,6 +14,66 @@ describe('Extract fields from text', () => { `); }); + it('Test key-values with single/double quotes', async () => { + const extractor = fieldExtractors.get(FieldExtractorID.KeyValues); + const out = extractor.parse('a="1", "b"=\'2\',c=3 x:y ;\r\nz="d and 4"'); + expect(out).toMatchInlineSnapshot(` + Object { + "a": "1", + "b": "2", + "c": "3", + "x": "y", + "z": "d and 4", + } + `); + }); + + it('Test key-values with nested single/double quotes', async () => { + const extractor = fieldExtractors.get(FieldExtractorID.KeyValues); + const out = extractor.parse( + `a="1", "b"=\'2\',c=3 x:y ;\r\nz="dbl_quotes=\\"Double Quotes\\" sgl_quotes='Single Quotes'"` + ); + + expect(out).toMatchInlineSnapshot(` + Object { + "a": "1", + "b": "2", + "c": "3", + "x": "y", + "z": "dbl_quotes=\\"Double Quotes\\" sgl_quotes='Single Quotes'", + } + `); + }); + + it('Test key-values with nested separator characters', async () => { + const extractor = fieldExtractors.get(FieldExtractorID.KeyValues); + const out = extractor.parse(`a="1", "b"=\'2\',c=3 x:y ;\r\nz="This is; testing& validating, 1=:2"`); + + expect(out).toMatchInlineSnapshot(` + Object { + "a": "1", + "b": "2", + "c": "3", + "x": "y", + "z": "This is; testing& validating, 1=:2", + } + `); + }); + + it('Test key-values where some values are null', async () => { + const extractor = fieldExtractors.get(FieldExtractorID.KeyValues); + const out = extractor.parse(`a=, "b"=\'2\',c=3 x: `); + + expect(out).toMatchInlineSnapshot(` + Object { + "a": "", + "b": "2", + "c": "3", + "x": "", + } + `); + }); + it('Split key+values', async () => { const extractor = fieldExtractors.get(FieldExtractorID.KeyValues); const out = extractor.parse('a="1", "b"=\'2\',c=3 x:y ;\r\nz="7"'); diff --git a/public/app/features/transformers/extractFields/fieldExtractors.ts b/public/app/features/transformers/extractFields/fieldExtractors.ts index 51f2ee6a9b0..45b91aecf4f 100644 --- a/public/app/features/transformers/extractFields/fieldExtractors.ts +++ b/public/app/features/transformers/extractFields/fieldExtractors.ts @@ -19,33 +19,99 @@ const extJSON: FieldExtractor = { }, }; -// strips quotes and leading/trailing braces in prom labels -const stripDecor = /['"]|^\{|\}$/g; -// splits on whitespace and other label pair delimiters -const splitLines = /[\s,;&]+/g; -// splits kv pairs -const splitPair = /[=:]/g; +function parseKeyValuePairs(raw: string): Record { + const buff: string[] = []; // array of characters + let esc = ''; + let key = ''; + const obj: Record = {}; + for (let i = 0; i < raw.length; i++) { + let c = raw[i]; + if (c === esc) { + esc = ''; + c = raw[++i]; + } + + const isEscaped = c === '\\'; + if (isEscaped) { + c = raw[++i]; + } + + // When escaped just append + if (isEscaped || esc.length) { + buff.push(c); + continue; + } + + if (c === `"` || c === `'`) { + esc = c; + } + + switch (c) { + case ':': + case '=': + if (buff.length) { + if (key) { + obj[key] = ''; + } + key = buff.join(''); + buff.length = 0; // clear values + } + break; + + // escape chars + case `"`: + case `'`: + // whitespace + case ` `: + case `\n`: + case `\t`: + case `\r`: + case `\n`: + if (buff.length && key === '') { + obj[buff.join('')] = ''; + buff.length = 0; + } + // seperators + case ',': + case ';': + case '&': + case '{': + case '}': + if (buff.length) { + const val = buff.join(''); + if (key.length) { + obj[key] = val; + key = ''; + } else { + key = val; + } + buff.length = 0; // clear values + } + break; + + // append our buffer + default: + buff.push(c); + if (i === raw.length - 1) { + if (key === '' && buff.length) { + obj[buff.join('')] = ''; + buff.length = 0; + } + } + } + } + + if (key.length) { + obj[key] = buff.join(''); + } + return obj; +} const extLabels: FieldExtractor = { id: FieldExtractorID.KeyValues, name: 'Key+value pairs', description: 'Look for a=b, c: d values in the line', - parse: (v: string) => { - const obj: Record = {}; - - v.trim() - .replace(stripDecor, '') - .split(splitLines) - .forEach((pair) => { - let [k, v] = pair.split(splitPair); - - if (k != null) { - obj[k] = v; - } - }); - - return obj; - }, + parse: parseKeyValuePairs, }; const fmts = [extJSON, extLabels];