Skip to content

Commit 2fc7e1e

Browse files
Add helper methods to support kafka output in beatreceivers (#49768) (#49872)
* Add helper methods to support kafka output in beatreceivers (cherry picked from commit 91d75e4) Co-authored-by: Khushi Jain <khushi.jain@elastic.co>
1 parent f2ceffa commit 2fc7e1e

3 files changed

Lines changed: 237 additions & 14 deletions

File tree

libbeat/common/fmtstr/formatevents.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ func (e *eventFieldCompiler) compileEventField(
315315
defaultValue = op.param
316316
}
317317

318-
path, err := parseEventPath(field)
318+
path, err := ParseEventPath(field)
319319
if err != nil {
320320
return nil, err
321321
}
@@ -382,7 +382,7 @@ func (e *eventTimestampEvaler) Eval(c interface{}, out *bytes.Buffer) error {
382382
return err
383383
}
384384

385-
func parseEventPath(field string) (string, error) {
385+
func ParseEventPath(field string) (string, error) {
386386
field = strings.Trim(field, " \n\r\t")
387387
fields := []string{}
388388

libbeat/common/fmtstr/formatstring.go

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func Compile(in string, vc VariableCompiler) (StringFormatter, error) {
132132
}
133133

134134
func compile(ctx *compileCtx, in string) (StringFormatter, error) {
135-
lexer := makeLexer(in)
135+
lexer := MakeLexer(in)
136136
defer lexer.Finish()
137137

138138
// parse format string
@@ -265,33 +265,44 @@ func (e variableElement) compile(ctx *compileCtx) (FormatEvaler, error) {
265265
return ctx.compileVariable(e.field, e.ops)
266266
}
267267

268-
func parse(lex lexer) ([]formatElement, error) {
269-
var elems []formatElement
268+
// parseFormatTokens runs the shared lexer-driven loop
269+
func parseFormatTokens[T any](
270+
lex lexer,
271+
appendLiteral func(*[]T, string),
272+
parseVar func(lexer) (T, error),
273+
) ([]T, error) {
274+
var elems []T
270275

271276
for token := range lex.Tokens() {
272277
switch token.typ {
273278
case tokErr:
274279
return nil, errors.New(token.val)
275280

276281
case tokString:
277-
elems = append(elems, StringElement{token.val})
282+
appendLiteral(&elems, token.val)
278283

279284
case tokOpen:
280-
elem, err := parseVariable(lex)
285+
elem, err := parseVar(lex)
281286
if err != nil {
282287
return nil, err
283288
}
284289
elems = append(elems, elem)
285290

286291
case tokClose, tokOperator:
287292
// should not happen, but let's return error just in case
288-
return nil, fmt.Errorf("Token '%v'(%v) not allowed", token.val, token.typ)
293+
return nil, fmt.Errorf("token '%v'(%v) not allowed", token.val, token.typ)
289294
}
290295
}
291296

292297
return elems, nil
293298
}
294299

300+
func parse(lex lexer) ([]formatElement, error) {
301+
return parseFormatTokens(lex, func(elems *[]formatElement, s string) {
302+
*elems = append(*elems, StringElement{s})
303+
}, parseVariable)
304+
}
305+
295306
func parseVariable(lex lexer) (formatElement, error) {
296307
var strings []string
297308
var ops []string
@@ -312,7 +323,7 @@ func parseVariable(lex lexer) (formatElement, error) {
312323

313324
case tokString:
314325
if len(strings) != len(ops) {
315-
return nil, fmt.Errorf("Unexpected string token %v, expected operator", token.val)
326+
return nil, fmt.Errorf("unexpected string token %v, expected operator", token.val)
316327
}
317328
strings = append(strings, token.val)
318329

@@ -322,18 +333,82 @@ func parseVariable(lex lexer) (formatElement, error) {
322333
}
323334
ops = append(ops, token.val)
324335
if len(ops) > len(strings) {
325-
return nil, fmt.Errorf("Consecutive operator tokens '%v'", token.val)
336+
return nil, fmt.Errorf("consecutive operator tokens '%v'", token.val)
326337
}
327338

328339
default:
329-
return nil, fmt.Errorf("Unexpected token '%v' (%v)", token.val, token.typ)
340+
return nil, fmt.Errorf("unexpected token '%v' (%v)", token.val, token.typ)
330341
}
331342
}
332343

333344
return nil, errMissingClose
334345
}
335346

336-
func makeLexer(in string) lexer {
347+
type VariableToken string
348+
349+
// ParseRawTokens returns a slice of tokens as they occur.
350+
// variable tokens are stored as typed value
351+
func ParseRawTokens(lex lexer) ([]any, error) {
352+
return parseFormatTokens(lex, func(elems *[]any, s string) {
353+
*elems = append(*elems, s)
354+
}, func(lex lexer) (any, error) {
355+
s, err := parseVariableToken(lex)
356+
if err != nil {
357+
return nil, err
358+
}
359+
return VariableToken(s), nil
360+
})
361+
}
362+
363+
// parseVariableToken consumes lexer tokens inside %{...} and returns the full inner
364+
// expression as one string e.g. "unknown:default".
365+
// Callers must parse operators if needed
366+
func parseVariableToken(lex lexer) (string, error) {
367+
// finalValue appends each string chunk and operator as they appear.
368+
var finalValue string
369+
var strings []string
370+
var ops []string
371+
372+
for token := range lex.Tokens() {
373+
switch token.typ {
374+
case tokErr:
375+
return "", errors.New(token.val)
376+
377+
case tokOpen:
378+
return "", errNestedVar
379+
380+
case tokClose:
381+
if len(strings) == 0 {
382+
return "", errEmptyFormat
383+
}
384+
return finalValue, nil
385+
386+
case tokString:
387+
if len(strings) != len(ops) {
388+
return "", fmt.Errorf("unexpected string token %v, expected operator", token.val)
389+
}
390+
strings = append(strings, token.val)
391+
finalValue += token.val
392+
393+
case tokOperator:
394+
if len(strings) == 0 {
395+
return "", errUnexpectedOperator
396+
}
397+
ops = append(ops, token.val)
398+
if len(ops) > len(strings) {
399+
return "", fmt.Errorf("consecutive operator tokens '%v'", token.val)
400+
}
401+
finalValue += token.val
402+
403+
default:
404+
return "", fmt.Errorf("unexpected token '%v' (%v)", token.val, token.typ)
405+
}
406+
}
407+
408+
return "", errMissingClose
409+
}
410+
411+
func MakeLexer(in string) lexer {
337412
lex := make(chan token, 1)
338413

339414
go func() {
@@ -359,7 +434,10 @@ func makeLexer(in string) lexer {
359434

360435
varcount := 0
361436
for len(content) > 0 {
362-
idx := -1
437+
if off > len(content) {
438+
return
439+
}
440+
var idx int
363441
if varcount == 0 {
364442
idx = strings.IndexAny(content[off:], `%\`)
365443
} else {
@@ -387,7 +465,8 @@ func makeLexer(in string) lexer {
387465
op := ":"
388466
if strings.ContainsRune("!@#&*=+<>?", rune(content[off])) {
389467
off++
390-
op = content[idx : off+1]
468+
// Two-byte op e.g. ":!"
469+
op = content[idx:off]
391470
}
392471
lex <- opToken(op)
393472

libbeat/common/fmtstr/formatstring_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ package fmtstr
2020
import (
2121
"errors"
2222
"fmt"
23+
"strings"
2324
"testing"
2425

2526
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
2628
)
2729

2830
func TestFormatString(t *testing.T) {
@@ -171,3 +173,145 @@ func TestFormatStringErrors(t *testing.T) {
171173
assert.Error(t, err)
172174
}
173175
}
176+
177+
func TestParseRawTokens(t *testing.T) {
178+
179+
testCases := []struct {
180+
name string
181+
input string
182+
expectedList []any
183+
err error
184+
}{
185+
186+
{
187+
name: "empty string",
188+
input: "",
189+
expectedList: nil,
190+
},
191+
{
192+
name: `when two %%`,
193+
input: `%%`,
194+
expectedList: []any{"%%"},
195+
},
196+
{
197+
name: `when input is %%{}`,
198+
input: `%%{}`,
199+
err: fmt.Errorf("empty format expansion"),
200+
},
201+
{
202+
name: `when input is %\{}`,
203+
input: `%\{}`,
204+
expectedList: []any{"%{}"},
205+
},
206+
{
207+
name: `when input is %{}`,
208+
input: `%{}`,
209+
err: fmt.Errorf("empty format expansion"),
210+
},
211+
{
212+
name: `when input is %{key}\\`,
213+
input: `%{key}\\`,
214+
expectedList: []any{VariableToken("key"), `\`},
215+
},
216+
{
217+
name: `when input is %{a:b:c}`,
218+
input: `%{a:b:c}`,
219+
expectedList: []any{VariableToken("a:b:c")},
220+
},
221+
{
222+
name: `when input is %{a`,
223+
input: `%{a`,
224+
err: fmt.Errorf(`missing closing '}'`),
225+
},
226+
{
227+
name: "simple lookup start of string",
228+
input: "%{k} test",
229+
expectedList: []any{VariableToken("k"), " test"},
230+
},
231+
{
232+
name: "simple lookup end of string",
233+
input: "test %{k}",
234+
expectedList: []any{"test ", VariableToken("k")},
235+
},
236+
{
237+
name: "simple lookup middle of string",
238+
input: "pre %{k} post",
239+
expectedList: []any{"pre ", VariableToken("k"), " post"},
240+
},
241+
{
242+
name: "compile lookup default",
243+
input: "%{unknown:default}",
244+
expectedList: []any{VariableToken("unknown:default")},
245+
},
246+
{
247+
name: "with escaped % symbol",
248+
input: `\%{abc}`,
249+
expectedList: []any{`%{abc}`},
250+
},
251+
}
252+
253+
for _, test := range testCases {
254+
t.Run(test.name, func(t *testing.T) {
255+
lexer := MakeLexer(test.input)
256+
defer lexer.Finish()
257+
got, err := ParseRawTokens(lexer)
258+
if test.err != nil {
259+
require.Equal(t, test.err, err)
260+
return
261+
}
262+
require.NoError(t, err)
263+
require.Equal(t, test.expectedList, got)
264+
})
265+
}
266+
}
267+
268+
func FuzzParseRawTokens(f *testing.F) {
269+
var cases = []string{
270+
"",
271+
"%{k} test",
272+
"pre %{k} post",
273+
"%{unknown:default}",
274+
"100% literal",
275+
"%%",
276+
"%%{}",
277+
"%\\{}",
278+
"%{}",
279+
"%{a:b:c}",
280+
"%{a:b:?c}",
281+
"%{a",
282+
}
283+
284+
for _, c := range cases {
285+
f.Add(c)
286+
}
287+
288+
f.Fuzz(func(t *testing.T, a string) {
289+
lex := MakeLexer(a)
290+
defer lex.Finish()
291+
output, err := ParseRawTokens(lex)
292+
if err != nil {
293+
t.Logf("skipping input %s with error: %v", a, err)
294+
return // invalid input
295+
}
296+
297+
// stringify output and match it with original input
298+
var finalOutput string
299+
for _, out := range output {
300+
switch tok := out.(type) {
301+
case string:
302+
finalOutput += tok
303+
case VariableToken:
304+
finalOutput += "%{" + string(tok) + "}"
305+
default:
306+
assert.Fail(t, fmt.Sprintf("unexpected type %T", tok))
307+
}
308+
}
309+
310+
// We cannot accurately reconstruct with escaped characters, so we remove them before comparing.
311+
assert.Equalf(t, removeEscapes(a), removeEscapes(finalOutput), "unexpected output: %s != %s", a, finalOutput)
312+
})
313+
}
314+
315+
func removeEscapes(str string) string {
316+
return strings.ReplaceAll(str, `\`, "")
317+
}

0 commit comments

Comments
 (0)