Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modify_fields processor and use it to set logName #474

Merged
merged 55 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
68f1f7f
First pass at converting Targets to Lua expressions
quentinmit Feb 25, 2022
26e4c17
Make it build
quentinmit Mar 2, 2022
e28c4e8
Generate and test arbitrary conf file sets
quentinmit Mar 3, 2022
a131440
Add modify_fields processor
quentinmit Mar 4, 2022
9c37654
Print golden diffs line-by-line for readability
quentinmit Mar 4, 2022
af082fd
Insert Lua code into filter chain
quentinmit Mar 4, 2022
b179d08
Add cleanup code
quentinmit Mar 4, 2022
5fd2fd0
Implement map_values and type
quentinmit Mar 7, 2022
e3c5d66
Generate Lua expressions for filters whenever possible
quentinmit Mar 8, 2022
583a511
Use Lua code to evaluate filters whenever possible
quentinmit Mar 8, 2022
98c7296
Fix missing end in modify_fields
quentinmit Mar 8, 2022
eefba52
Make update_golden remove files as well
quentinmit Mar 8, 2022
137280d
Update golden files for processor_exclude_logs test
quentinmit Mar 8, 2022
dc729e5
Fix Lua not-equals operator
quentinmit Mar 8, 2022
522d8aa
More != fixes
quentinmit Mar 9, 2022
1b57b3b
Remove unused code
quentinmit Mar 9, 2022
bf0783e
Don't use modify filter with Copy because it doesn't support record a…
quentinmit Mar 9, 2022
f59eed8
Don't look in record for local variables
quentinmit Mar 9, 2022
d0f0c57
Remove unused cleanup code
quentinmit Mar 14, 2022
083fb7c
Set logName using Lua
quentinmit Mar 17, 2022
32cfbc3
Report instrumentation source as a label
quentinmit Mar 17, 2022
46200fb
Update golden files
quentinmit Mar 17, 2022
cc99f52
Add missing return statement
quentinmit Mar 17, 2022
3c9748a
Add integration test for modify_fields
quentinmit Mar 19, 2022
386c4d2
Merge remote-tracking branch 'origin/master' into quentin-logtransform
quentinmit Mar 21, 2022
2181a74
Add unit test for modify_fields config
quentinmit Mar 22, 2022
2de7247
Emit MapValues in deterministic order
quentinmit Mar 22, 2022
7193ce0
Validate field names in modify_fields config
quentinmit Mar 22, 2022
e56d6a4
Fix update_golden to properly create golden_error
quentinmit Mar 22, 2022
bef8f6f
Add tests for field validation
quentinmit Mar 22, 2022
ae7c15a
Tentatively use agent.googleapis.com/receiver_type as the label name
quentinmit Apr 4, 2022
01e2ba7
Review comments
quentinmit Apr 4, 2022
27ba607
More review comments
quentinmit Apr 4, 2022
f947f77
Merge remote-tracking branch 'origin/master' into quentin-logtransform
quentinmit Apr 5, 2022
359e5e6
Add default_value to set a field only if the field isn't already set
quentinmit Apr 7, 2022
f026836
Implement Stringer for Filter and Member for debugging
quentinmit Apr 7, 2022
76a9adf
Allow bareword operators as expression RHS
quentinmit Apr 7, 2022
34f2dd2
Fix handling of Lua expressions with quotes or backslashes
quentinmit Apr 7, 2022
4e25ad5
Fix quoted and unquoted escaping behavior in Cloud Logging filter syntax
quentinmit Apr 7, 2022
1f15a7a
Improve test coverage
quentinmit Apr 7, 2022
51b86e9
Fix not-equals operator in type conversion
quentinmit Apr 8, 2022
8ae8016
Fluent-bit only supports Lua 5.1
quentinmit Apr 8, 2022
29b6c71
Add integration test for map_values
quentinmit Apr 11, 2022
ea38c66
Implement omit_if
quentinmit Apr 11, 2022
7dc9fe6
Add integration test for omit_if
quentinmit Apr 11, 2022
9d1e190
Restore escaped form of targets in errors
quentinmit Apr 11, 2022
d751bdc
Rename tests
quentinmit Apr 11, 2022
8c8ca73
Add semicolons to fix ambiguity
quentinmit Apr 11, 2022
7d6b70c
Merge remote-tracking branch 'origin/master' into quentin-logtransform
quentinmit Apr 11, 2022
3471a3a
Fix test flakiness by not reusing a variable across goroutines
quentinmit Apr 11, 2022
ea1c359
Validate that the same destination field is not specified multiple ti…
quentinmit Apr 12, 2022
a674602
Return validation errors in a deterministic order to prevent test fla…
quentinmit Apr 12, 2022
f733533
Fix typo in comment
quentinmit Apr 14, 2022
aa58619
Comment-out receiver_type for subsequent CLs
quentinmit Apr 14, 2022
750d86b
Merge remote-tracking branch 'origin/master' into quentin-logtransform
quentinmit Apr 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion confgenerator/confgenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func (l *Logging) generateFluentbitComponents(userAgent string, hostInfo *host.I
}
components = append(components, processor.Components(tag, strconv.Itoa(i))...)
}
components = append(components, setLogNameComponents(tag, rID)...)
components = append(components, setLogNameComponents(tag, rID, receiver.Type())...)

// Logs ingested using the fluent_forward receiver must add the existing_tag
// on the record to the LogName. This is done with a Lua filter.
Expand Down
11 changes: 11 additions & 0 deletions confgenerator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,14 @@ func (ve validationError) Error() string {
return fmt.Sprintf("%q must start with %q", ve.Field(), ve.Param())
case "url":
return fmt.Sprintf("%q must be a URL", ve.Field())
case "excluded_with":
return fmt.Sprintf("%q cannot be set if one of [%s] is set", ve.Field(), ve.Param())
case "filter":
_, err := filter.NewFilter(ve.Value().(string))
return fmt.Sprintf("%q: %v", ve.Field(), err)
case "field":
_, err := filter.NewMember(ve.Value().(string))
return fmt.Sprintf("%q: %v", ve.Field(), err)
}

return ve.FieldError.Error()
Expand Down Expand Up @@ -172,6 +177,12 @@ func newValidator() *validator.Validate {
_, err := filter.NewFilter(fl.Field().String())
return err == nil
})
// field validates that a Cloud Logging field expression is valid
v.RegisterValidation("field", func(fl validator.FieldLevel) bool {
_, err := filter.NewMember(fl.Field().String())
// TODO: Disallow specific target fields?
return err == nil
})
// multipleof_time validates that the value duration is a multiple of the parameter
v.RegisterValidation("multipleof_time", func(fl validator.FieldLevel) bool {
t, ok := fl.Field().Interface().(time.Duration)
Expand Down
123 changes: 72 additions & 51 deletions confgenerator/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ import (
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/fluentbit"
)

type Member struct {
ast.Target
}

func NewMember(m string) (*Member, error) {
lex := lexer.NewLexer([]byte(m))
p := parser.NewParser()
out, err := p.Parse(lex)
if err != nil {
return nil, err
}
r, ok := out.(ast.Restriction)
if !ok || r.Operator != "GLOBAL" {
return nil, fmt.Errorf("not a field: %#v", out)
}
return &Member{r.LHS}, nil
}

var LuaQuote = ast.LuaQuote

type Filter struct {
expr ast.Expression
}
Expand All @@ -44,31 +64,45 @@ func NewFilter(f string) (*Filter, error) {
return nil, fmt.Errorf("not an expression: %+v", out)
}

// innerComponents returns only the logical modify filters that are intended to be
// positioned between corresponding nest/grep/lift filters.
func (f *Filter) innerComponents(tag string) []fluentbit.Component {
match := fmt.Sprintf("__match_%s", strings.ReplaceAll(tag, ".", "_"))
return f.expr.Components(tag, match)
// innerFluentConfig returns components that are intended to be positioned between corresponding nest/lift filters and a Lua expression to evaluate.
func (f *Filter) innerFluentConfig(tag, prefix string) ([]fluentbit.Component, string) {
return f.expr.FluentConfig(tag, prefix)
}

func (f *Filter) Components(tag string, isExclusionFilter bool) []fluentbit.Component {
return AllComponents(tag, []*Filter{f}, isExclusionFilter)
func (f Filter) String() string {
return f.expr.String()
}

// AllComponents returns a list of FluentBit components for a list of filters.
// As an optimization, only a single set of nest/grep/lift components is
// emitted in total.
func AllComponents(tag string, filters []*Filter, isExclusionFilter bool) []fluentbit.Component {
var parity string
if isExclusionFilter {
parity = "Exclude"
} else {
parity = "Regex"
// MatchesAny returns a single Filter that matches if any of the child filters match.
func MatchesAny(filters []*Filter) *Filter {
d := ast.Disjunction{}
for _, f := range filters {
d = append(d, f.expr)
}
// TODO: Re-implement using Lua once regex is supported. Lua has been shown to perform better
// than the next/modify/grep/lift pattern used here, but we are unable to use Lua for now since
// it does not yet support regex.
c := []fluentbit.Component{{
return &Filter{expr: d}
}

// AllFluentConfig returns components (if any) and Lua code that sets a Boolean local variable for each filter to indicate if that filter matched.
func AllFluentConfig(tag string, filters map[string]*Filter) ([]fluentbit.Component, string) {
var c []fluentbit.Component
var lua strings.Builder
var vars []string
for k := range filters {
vars = append(vars, k)
}

for i, k := range vars {
prefix := fmt.Sprintf("__match_%d", i)
filter := filters[k]
filterComponents, filterExpr := filter.innerFluentConfig(tag, prefix)
c = append(c, filterComponents...)
lua.WriteString(fmt.Sprintf("local %s = %s\n", k, filterExpr))
}
if len(c) == 0 {
// If we didn't need any filters, just return the Lua code.
return nil, lua.String()
}
out := []fluentbit.Component{{
Kind: "FILTER",
Config: map[string]string{
"Name": "nest",
Expand All @@ -78,36 +112,23 @@ func AllComponents(tag string, filters []*Filter, isExclusionFilter bool) []flue
"Wildcard": "*",
},
}}
match := fmt.Sprintf("__match_%s", strings.ReplaceAll(tag, ".", "_"))
for _, filter := range filters {
c = append(c, filter.innerComponents(tag)...)
}
c = append(c,
fluentbit.Component{
Kind: "FILTER",
Config: map[string]string{
"Name": "grep",
"Match": tag,
parity: fmt.Sprintf("%s 1", match),
},
},
fluentbit.Component{
Kind: "FILTER",
Config: map[string]string{
"Name": "modify",
"Match": tag,
"Remove_wildcard": match,
},
},
fluentbit.Component{
Kind: "FILTER",
Config: map[string]string{
"Name": "nest",
"Match": tag,
"Operation": "lift",
"Nested_under": "record",
},
out = append(out, c...)
out = append(out, fluentbit.Component{
Kind: "FILTER",
Config: map[string]string{
"Name": "nest",
"Match": tag,
"Operation": "lift",
"Nested_under": "record",
},
)
return c
})
// Remove match keys
lua.WriteString(`
for k, v in pairs(record) do
if string.match(k, "^__match_.+") then
record[k] = nil
end
end
`)
return out, lua.String()
}
93 changes: 88 additions & 5 deletions confgenerator/filter/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@ import (
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/filter/internal/generated/lexer"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/filter/internal/generated/token"
"github.com/GoogleCloudPlatform/ops-agent/confgenerator/fluentbit"
"github.com/google/go-cmp/cmp"
)

var validFilters = []string{
`severity = "hello"`,
`jsonPayload."bar.baz" = "hello"`,
`jsonPayload.b.c=~"b.*c"`,
`"jsonPayload"."foo" = "bar"`,
`-severity = 1`,
`NOT severity = 3`,
`(jsonPayload.bar = "one" OR jsonPayload.bar = "two") jsonPayload.baz = "three"`,
`jsonPayload.one = 1 jsonPayload.two = 2 AND jsonPayload.three = 3`,
`jsonPayload.int_field:0 OR jsonPayload.int_field:0 AND jsonPayload.int_field:0`,
`jsonPayload.compound.string_field : wal\"rus`,
`severity =~ ERROR AND jsonPayload.message =~ foo AND httpRequest.requestMethod =~ GET`,
`severity =~ "ERROR" AND jsonPayload.message =~ "foo" AND httpRequest.requestMethod =~ "GET"`,
`severity = "AND"`,
`severity = AND`,
`severity = OR`,
`severity = NOT`,
`"json\u0050ayload".foo = bar`,
`jsonPayload.\= = bar`,
`jsonPayload."\=" = bar`,
}

func TestShouldLex(t *testing.T) {
Expand All @@ -59,13 +67,88 @@ func TestShouldParse(t *testing.T) {
if err != nil {
t.Error(err)
}
components := filter.Components("logname", false)
t.Logf("parsed filter = %s", filter)
if filter == nil {
t.Fatal("got nil filter")
}
components, expr := AllFluentConfig("logname", map[string]*Filter{"filter": filter})
t.Logf("components = %+v", components)
files, err := fluentbit.ModularConfig{Components: components}.Generate()
t.Logf("expression =\n%s", expr)
if components != nil {
files, err := fluentbit.ModularConfig{Components: components}.Generate()
if err != nil {
t.Error(err)
}
t.Logf("generated config:\n%v", files)
}
})
}
}

func TestFilterRoundTrip(t *testing.T) {
for _, test := range validFilters {
test := test
t.Run(test, func(t *testing.T) {
filter, err := NewFilter(test)
if err != nil {
t.Error(err)
t.Fatal(err)
}
first := filter.String()
filter2, err := NewFilter(first)
if err != nil {
t.Fatalf("failed to re-parse %q: %v", first, err)
}
second := filter2.String()
if diff := cmp.Diff(second, first); diff != "" {
t.Errorf("filter did not round-trip (second -, first+):\n%s", diff)
}
})
}
}

func TestInvalidFilters(t *testing.T) {
for _, test := range []string{
`"missing operator"`,
`invalid/characters*here`,
`jsonPayload.foo =~ bareword`,
`json\u0050ayload.foo = bar`,
} {
test := test
t.Run(test, func(t *testing.T) {
filter, err := NewFilter(test)
if err != nil {
t.Logf("got expected error %v", err)
return
}
t.Errorf("invalid filter %q unexpectedly parsed: %v", test, filter)
})
}
}

func TestValidMembers(t *testing.T) {
for _, test := range []struct {
in string
want []string
}{
{`jsonPayload.foo`, []string{"jsonPayload", "foo"}},
{`labels."logging.googleapis.com/foo"`, []string{"labels", "logging.googleapis.com/foo"}},
{`severity`, []string{"severity"}},
{`jsonPayload.\=`, []string{"jsonPayload", `\=`}},
{`jsonPayload."\="`, []string{"jsonPayload", `=`}},
} {
test := test
t.Run(test.in, func(t *testing.T) {
member, err := NewMember(test.in)
if err != nil {
t.Fatal(err)
}
got, err := member.Unquote()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(got, test.want); diff != "" {
t.Errorf("incorrect parse (got -/want +):\n%s", diff)
}
t.Logf("generated config:\n%v", files)
})
}
}
Loading