Skip to content

Commit

Permalink
[pkg/stanza] Allow 'parse_to' fields to accept 'attributes' and 'reso…
Browse files Browse the repository at this point in the history
…urce' (open-telemetry#14089)

* [pkg/stanza] Allow 'parse_to' fields to accept 'attributes' and 'resource'

This change enables parsers to parse directly to 'attributes' or 'resource'.
In order to allow alternate validation behavior while unmarshaling, a new
RootableField struct was introduced, which wraps the Field struct.
  • Loading branch information
djaglowski authored Sep 27, 2022
1 parent 1434604 commit 1d620bf
Show file tree
Hide file tree
Showing 19 changed files with 346 additions and 183 deletions.
44 changes: 42 additions & 2 deletions pkg/stanza/entry/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ type Field struct {
FieldInterface
}

// RootableField is a Field that may refer directly to "attributes" or "resource"
type RootableField struct {
Field
}

// FieldInterface is a field on an entry.
type FieldInterface interface {
Get(*Entry) (interface{}, bool)
Expand All @@ -52,6 +57,18 @@ func (f *Field) UnmarshalJSON(raw []byte) error {
return err
}

// UnmarshalJSON will unmarshal a field from JSON
func (r *RootableField) UnmarshalJSON(raw []byte) error {
var s string
err := json.Unmarshal(raw, &s)
if err != nil {
return err
}
field, err := newField(s, true)
*r = RootableField{Field: field}
return err
}

// UnmarshalYAML will unmarshal a field from YAML
func (f *Field) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
Expand All @@ -63,27 +80,50 @@ func (f *Field) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err
}

// UnmarshalYAML will unmarshal a field from YAML
func (r *RootableField) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
err := unmarshal(&s)
if err != nil {
return err
}
field, err := newField(s, true)
*r = RootableField{Field: field}
return err
}

// UnmarshalText will unmarshal a field from text
func (f *Field) UnmarshalText(text []byte) error {
field, err := NewField(string(text))
*f = field
return err
}

// UnmarshalText will unmarshal a field from text
func (r *RootableField) UnmarshalText(text []byte) error {
field, err := newField(string(text), true)
*r = RootableField{Field: field}
return err
}

func NewField(s string) (Field, error) {
return newField(s, false)
}

func newField(s string, rootable bool) (Field, error) {
keys, err := fromJSONDot(s)
if err != nil {
return Field{}, fmt.Errorf("splitting field: %w", err)
}

switch keys[0] {
case AttributesPrefix:
if len(keys) == 1 {
if !rootable && len(keys) == 1 {
return Field{}, fmt.Errorf("attributes cannot be referenced without subfield")
}
return NewAttributeField(keys[1:]...), nil
case ResourcePrefix:
if len(keys) == 1 {
if !rootable && len(keys) == 1 {
return Field{}, fmt.Errorf("resource cannot be referenced without subfield")
}
return NewResourceField(keys[1:]...), nil
Expand Down
188 changes: 96 additions & 92 deletions pkg/stanza/entry/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,147 +24,151 @@ import (

func TestFieldUnmarshalJSON(t *testing.T) {
cases := []struct {
name string
input []byte
expected Field
name string
input []byte
expected Field
expectedErr string
expectedErrRootable string
}{
{
"BodyLong",
[]byte(`"body"`),
NewBodyField(),
name: "BodyLong",
input: []byte(`"body"`),
expected: NewBodyField(),
},
{
"SimpleField",
[]byte(`"body.test1"`),
NewBodyField("test1"),
name: "SimpleField",
input: []byte(`"body.test1"`),
expected: NewBodyField("test1"),
},
{
"ComplexField",
[]byte(`"body.test1.test2"`),
NewBodyField("test1", "test2"),
name: "ComplexField",
input: []byte(`"body.test1.test2"`),
expected: NewBodyField("test1", "test2"),
},
{
"BracketedField",
[]byte(`"body.test1['file.name']"`),
NewBodyField("test1", "file.name"),
name: "BracketedField",
input: []byte(`"body.test1['file.name']"`),
expected: NewBodyField("test1", "file.name"),
},
{
"DoubleBracketedField",
[]byte(`"body.test1['file.details']['file.name']"`),
NewBodyField("test1", "file.details", "file.name"),
name: "DoubleBracketedField",
input: []byte(`"body.test1['file.details']['file.name']"`),
expected: NewBodyField("test1", "file.details", "file.name"),
},
{
"PostBracketField",
[]byte(`"body.test1['file.details'].name"`),
NewBodyField("test1", "file.details", "name"),
name: "PostBracketField",
input: []byte(`"body.test1['file.details'].name"`),
expected: NewBodyField("test1", "file.details", "name"),
},
{
"AttributesSimpleField",
[]byte(`"attributes.test1"`),
NewAttributeField("test1"),
name: "AttributesSimpleField",
input: []byte(`"attributes.test1"`),
expected: NewAttributeField("test1"),
},
{
"AttributesComplexField",
[]byte(`"attributes.test1.test2"`),
NewAttributeField("test1", "test2"),
name: "AttributesComplexField",
input: []byte(`"attributes.test1.test2"`),
expected: NewAttributeField("test1", "test2"),
},
{
"AttributesBracketedField",
[]byte(`"attributes.test1['file.name']"`),
NewAttributeField("test1", "file.name"),
name: "AttributesBracketedField",
input: []byte(`"attributes.test1['file.name']"`),
expected: NewAttributeField("test1", "file.name"),
},
{
"AttributesDoubleBracketedField",
[]byte(`"attributes.test1['file.details']['file.name']"`),
NewAttributeField("test1", "file.details", "file.name"),
name: "AttributesDoubleBracketedField",
input: []byte(`"attributes.test1['file.details']['file.name']"`),
expected: NewAttributeField("test1", "file.details", "file.name"),
},
{
"AttributesPostBracketField",
[]byte(`"attributes.test1['file.details'].name"`),
NewAttributeField("test1", "file.details", "name"),
name: "AttributesPostBracketField",
input: []byte(`"attributes.test1['file.details'].name"`),
expected: NewAttributeField("test1", "file.details", "name"),
},
{
"AttributesSimpleField",
[]byte(`"attributes.test1"`),
NewAttributeField("test1"),
name: "AttributesSimpleField",
input: []byte(`"attributes.test1"`),
expected: NewAttributeField("test1"),
},

{
"ResourceSimpleField",
[]byte(`"resource.test1"`),
NewResourceField("test1"),
name: "ResourceSimpleField",
input: []byte(`"resource.test1"`),
expected: NewResourceField("test1"),
},
{
"ResourceComplexField",
[]byte(`"resource.test1.test2"`),
NewResourceField("test1", "test2"),
name: "ResourceComplexField",
input: []byte(`"resource.test1.test2"`),
expected: NewResourceField("test1", "test2"),
},
{
"ResourceBracketedField",
[]byte(`"resource.test1['file.name']"`),
NewResourceField("test1", "file.name"),
name: "ResourceBracketedField",
input: []byte(`"resource.test1['file.name']"`),
expected: NewResourceField("test1", "file.name"),
},
{
"ResourceDoubleBracketedField",
[]byte(`"resource.test1['file.details']['file.name']"`),
NewResourceField("test1", "file.details", "file.name"),
name: "ResourceDoubleBracketedField",
input: []byte(`"resource.test1['file.details']['file.name']"`),
expected: NewResourceField("test1", "file.details", "file.name"),
},
{
"ResourcePostBracketField",
[]byte(`"resource.test1['file.details'].name"`),
NewResourceField("test1", "file.details", "name"),
name: "ResourcePostBracketField",
input: []byte(`"resource.test1['file.details'].name"`),
expected: NewResourceField("test1", "file.details", "name"),
},
{
"ResourceSimpleField",
[]byte(`"resource.test1"`),
NewResourceField("test1"),
name: "ResourceSimpleField",
input: []byte(`"resource.test1"`),
expected: NewResourceField("test1"),
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var f Field
err := json.Unmarshal(tc.input, &f)
require.NoError(t, err)
require.Equal(t, tc.expected, f)
})
}
}

func TestFieldUnmarshalJSONFailure(t *testing.T) {
cases := []struct {
name string
input []byte
expected string
}{
{
"Bool",
[]byte(`"bool"`),
"unrecognized prefix",
name: "AttributesRoot",
input: []byte(`"attributes"`),
expectedErr: "attributes cannot be referenced without subfield",
expected: NewAttributeField(),
},
{
"Object",
[]byte(`{"key":"value"}`),
"cannot unmarshal object into Go value of type string",
name: "ResourceRoot",
input: []byte(`"resource"`),
expectedErr: "resource cannot be referenced without subfield",
expected: NewResourceField(),
},
{
"AttributesRoot",
[]byte(`"attributes"`),
"attributes cannot be referenced without subfield",
name: "Bool",
input: []byte(`"bool"`),
expectedErrRootable: "unrecognized prefix",
},
{
"ResourceRoot",
[]byte(`"resource"`),
"resource cannot be referenced without subfield",
name: "Object",
input: []byte(`{"key":"value"}`),
expectedErrRootable: "cannot unmarshal object into Go value of type string",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var f Field
err := json.Unmarshal(tc.input, &f)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expected)
var field Field
err := json.Unmarshal(tc.input, &field)

var rootableField RootableField
errRootable := json.Unmarshal(tc.input, &rootableField)

switch {
case tc.expectedErrRootable != "":
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
require.Error(t, errRootable)
require.Contains(t, errRootable.Error(), tc.expectedErrRootable)
case tc.expectedErr != "":
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
require.NoError(t, errRootable)
require.Equal(t, tc.expected, rootableField.Field)
default:
require.NoError(t, err)
require.Equal(t, tc.expected, field)
require.NoError(t, errRootable)
require.Equal(t, tc.expected, rootableField.Field)
}
})
}
}
Expand Down
19 changes: 9 additions & 10 deletions pkg/stanza/operator/helper/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,20 @@ func NewParserConfig(operatorID, operatorType string) ParserConfig {
return ParserConfig{
TransformerConfig: NewTransformerConfig(operatorID, operatorType),
ParseFrom: entry.NewBodyField(),
ParseTo: entry.NewAttributeField(),
ParseTo: entry.RootableField{Field: entry.NewAttributeField()},
}
}

// ParserConfig provides the basic implementation of a parser config.
type ParserConfig struct {
TransformerConfig `mapstructure:",squash" yaml:",inline"`

ParseFrom entry.Field `mapstructure:"parse_from" json:"parse_from" yaml:"parse_from"`
ParseTo entry.Field `mapstructure:"parse_to" json:"parse_to" yaml:"parse_to"`
BodyField *entry.Field `mapstructure:"body" json:"body" yaml:"body"`
TimeParser *TimeParser `mapstructure:"timestamp,omitempty" json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
SeverityConfig *SeverityConfig `mapstructure:"severity,omitempty" json:"severity,omitempty" yaml:"severity,omitempty"`
TraceParser *TraceParser `mapstructure:"trace,omitempty" json:"trace,omitempty" yaml:"trace,omitempty"`
ScopeNameParser *ScopeNameParser `mapstructure:"scope_name,omitempty" json:"scope_name,omitempty" yaml:"scope_name,omitempty"`
ParseFrom entry.Field `mapstructure:"parse_from" json:"parse_from" yaml:"parse_from"`
ParseTo entry.RootableField `mapstructure:"parse_to" json:"parse_to" yaml:"parse_to"`
BodyField *entry.Field `mapstructure:"body" json:"body" yaml:"body"`
TimeParser *TimeParser `mapstructure:"timestamp,omitempty" json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
SeverityConfig *SeverityConfig `mapstructure:"severity,omitempty" json:"severity,omitempty" yaml:"severity,omitempty"`
TraceParser *TraceParser `mapstructure:"trace,omitempty" json:"trace,omitempty" yaml:"trace,omitempty"`
ScopeNameParser *ScopeNameParser `mapstructure:"scope_name,omitempty" json:"scope_name,omitempty" yaml:"scope_name,omitempty"`
}

// Build will build a parser operator.
Expand All @@ -60,7 +59,7 @@ func (c ParserConfig) Build(logger *zap.SugaredLogger) (ParserOperator, error) {
parserOperator := ParserOperator{
TransformerOperator: transformerOperator,
ParseFrom: c.ParseFrom,
ParseTo: c.ParseTo,
ParseTo: c.ParseTo.Field,
BodyField: c.BodyField,
}

Expand Down
Loading

0 comments on commit 1d620bf

Please sign in to comment.