Skip to content

Commit 1d620bf

Browse files
authored
[pkg/stanza] Allow 'parse_to' fields to accept 'attributes' and 'resource' (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.
1 parent 1434604 commit 1d620bf

File tree

19 files changed

+346
-183
lines changed

19 files changed

+346
-183
lines changed

pkg/stanza/entry/field.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ type Field struct {
3333
FieldInterface
3434
}
3535

36+
// RootableField is a Field that may refer directly to "attributes" or "resource"
37+
type RootableField struct {
38+
Field
39+
}
40+
3641
// FieldInterface is a field on an entry.
3742
type FieldInterface interface {
3843
Get(*Entry) (interface{}, bool)
@@ -52,6 +57,18 @@ func (f *Field) UnmarshalJSON(raw []byte) error {
5257
return err
5358
}
5459

60+
// UnmarshalJSON will unmarshal a field from JSON
61+
func (r *RootableField) UnmarshalJSON(raw []byte) error {
62+
var s string
63+
err := json.Unmarshal(raw, &s)
64+
if err != nil {
65+
return err
66+
}
67+
field, err := newField(s, true)
68+
*r = RootableField{Field: field}
69+
return err
70+
}
71+
5572
// UnmarshalYAML will unmarshal a field from YAML
5673
func (f *Field) UnmarshalYAML(unmarshal func(interface{}) error) error {
5774
var s string
@@ -63,27 +80,50 @@ func (f *Field) UnmarshalYAML(unmarshal func(interface{}) error) error {
6380
return err
6481
}
6582

83+
// UnmarshalYAML will unmarshal a field from YAML
84+
func (r *RootableField) UnmarshalYAML(unmarshal func(interface{}) error) error {
85+
var s string
86+
err := unmarshal(&s)
87+
if err != nil {
88+
return err
89+
}
90+
field, err := newField(s, true)
91+
*r = RootableField{Field: field}
92+
return err
93+
}
94+
6695
// UnmarshalText will unmarshal a field from text
6796
func (f *Field) UnmarshalText(text []byte) error {
6897
field, err := NewField(string(text))
6998
*f = field
7099
return err
71100
}
72101

102+
// UnmarshalText will unmarshal a field from text
103+
func (r *RootableField) UnmarshalText(text []byte) error {
104+
field, err := newField(string(text), true)
105+
*r = RootableField{Field: field}
106+
return err
107+
}
108+
73109
func NewField(s string) (Field, error) {
110+
return newField(s, false)
111+
}
112+
113+
func newField(s string, rootable bool) (Field, error) {
74114
keys, err := fromJSONDot(s)
75115
if err != nil {
76116
return Field{}, fmt.Errorf("splitting field: %w", err)
77117
}
78118

79119
switch keys[0] {
80120
case AttributesPrefix:
81-
if len(keys) == 1 {
121+
if !rootable && len(keys) == 1 {
82122
return Field{}, fmt.Errorf("attributes cannot be referenced without subfield")
83123
}
84124
return NewAttributeField(keys[1:]...), nil
85125
case ResourcePrefix:
86-
if len(keys) == 1 {
126+
if !rootable && len(keys) == 1 {
87127
return Field{}, fmt.Errorf("resource cannot be referenced without subfield")
88128
}
89129
return NewResourceField(keys[1:]...), nil

pkg/stanza/entry/field_test.go

Lines changed: 96 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -24,147 +24,151 @@ import (
2424

2525
func TestFieldUnmarshalJSON(t *testing.T) {
2626
cases := []struct {
27-
name string
28-
input []byte
29-
expected Field
27+
name string
28+
input []byte
29+
expected Field
30+
expectedErr string
31+
expectedErrRootable string
3032
}{
3133
{
32-
"BodyLong",
33-
[]byte(`"body"`),
34-
NewBodyField(),
34+
name: "BodyLong",
35+
input: []byte(`"body"`),
36+
expected: NewBodyField(),
3537
},
3638
{
37-
"SimpleField",
38-
[]byte(`"body.test1"`),
39-
NewBodyField("test1"),
39+
name: "SimpleField",
40+
input: []byte(`"body.test1"`),
41+
expected: NewBodyField("test1"),
4042
},
4143
{
42-
"ComplexField",
43-
[]byte(`"body.test1.test2"`),
44-
NewBodyField("test1", "test2"),
44+
name: "ComplexField",
45+
input: []byte(`"body.test1.test2"`),
46+
expected: NewBodyField("test1", "test2"),
4547
},
4648
{
47-
"BracketedField",
48-
[]byte(`"body.test1['file.name']"`),
49-
NewBodyField("test1", "file.name"),
49+
name: "BracketedField",
50+
input: []byte(`"body.test1['file.name']"`),
51+
expected: NewBodyField("test1", "file.name"),
5052
},
5153
{
52-
"DoubleBracketedField",
53-
[]byte(`"body.test1['file.details']['file.name']"`),
54-
NewBodyField("test1", "file.details", "file.name"),
54+
name: "DoubleBracketedField",
55+
input: []byte(`"body.test1['file.details']['file.name']"`),
56+
expected: NewBodyField("test1", "file.details", "file.name"),
5557
},
5658
{
57-
"PostBracketField",
58-
[]byte(`"body.test1['file.details'].name"`),
59-
NewBodyField("test1", "file.details", "name"),
59+
name: "PostBracketField",
60+
input: []byte(`"body.test1['file.details'].name"`),
61+
expected: NewBodyField("test1", "file.details", "name"),
6062
},
6163
{
62-
"AttributesSimpleField",
63-
[]byte(`"attributes.test1"`),
64-
NewAttributeField("test1"),
64+
name: "AttributesSimpleField",
65+
input: []byte(`"attributes.test1"`),
66+
expected: NewAttributeField("test1"),
6567
},
6668
{
67-
"AttributesComplexField",
68-
[]byte(`"attributes.test1.test2"`),
69-
NewAttributeField("test1", "test2"),
69+
name: "AttributesComplexField",
70+
input: []byte(`"attributes.test1.test2"`),
71+
expected: NewAttributeField("test1", "test2"),
7072
},
7173
{
72-
"AttributesBracketedField",
73-
[]byte(`"attributes.test1['file.name']"`),
74-
NewAttributeField("test1", "file.name"),
74+
name: "AttributesBracketedField",
75+
input: []byte(`"attributes.test1['file.name']"`),
76+
expected: NewAttributeField("test1", "file.name"),
7577
},
7678
{
77-
"AttributesDoubleBracketedField",
78-
[]byte(`"attributes.test1['file.details']['file.name']"`),
79-
NewAttributeField("test1", "file.details", "file.name"),
79+
name: "AttributesDoubleBracketedField",
80+
input: []byte(`"attributes.test1['file.details']['file.name']"`),
81+
expected: NewAttributeField("test1", "file.details", "file.name"),
8082
},
8183
{
82-
"AttributesPostBracketField",
83-
[]byte(`"attributes.test1['file.details'].name"`),
84-
NewAttributeField("test1", "file.details", "name"),
84+
name: "AttributesPostBracketField",
85+
input: []byte(`"attributes.test1['file.details'].name"`),
86+
expected: NewAttributeField("test1", "file.details", "name"),
8587
},
8688
{
87-
"AttributesSimpleField",
88-
[]byte(`"attributes.test1"`),
89-
NewAttributeField("test1"),
89+
name: "AttributesSimpleField",
90+
input: []byte(`"attributes.test1"`),
91+
expected: NewAttributeField("test1"),
9092
},
91-
9293
{
93-
"ResourceSimpleField",
94-
[]byte(`"resource.test1"`),
95-
NewResourceField("test1"),
94+
name: "ResourceSimpleField",
95+
input: []byte(`"resource.test1"`),
96+
expected: NewResourceField("test1"),
9697
},
9798
{
98-
"ResourceComplexField",
99-
[]byte(`"resource.test1.test2"`),
100-
NewResourceField("test1", "test2"),
99+
name: "ResourceComplexField",
100+
input: []byte(`"resource.test1.test2"`),
101+
expected: NewResourceField("test1", "test2"),
101102
},
102103
{
103-
"ResourceBracketedField",
104-
[]byte(`"resource.test1['file.name']"`),
105-
NewResourceField("test1", "file.name"),
104+
name: "ResourceBracketedField",
105+
input: []byte(`"resource.test1['file.name']"`),
106+
expected: NewResourceField("test1", "file.name"),
106107
},
107108
{
108-
"ResourceDoubleBracketedField",
109-
[]byte(`"resource.test1['file.details']['file.name']"`),
110-
NewResourceField("test1", "file.details", "file.name"),
109+
name: "ResourceDoubleBracketedField",
110+
input: []byte(`"resource.test1['file.details']['file.name']"`),
111+
expected: NewResourceField("test1", "file.details", "file.name"),
111112
},
112113
{
113-
"ResourcePostBracketField",
114-
[]byte(`"resource.test1['file.details'].name"`),
115-
NewResourceField("test1", "file.details", "name"),
114+
name: "ResourcePostBracketField",
115+
input: []byte(`"resource.test1['file.details'].name"`),
116+
expected: NewResourceField("test1", "file.details", "name"),
116117
},
117118
{
118-
"ResourceSimpleField",
119-
[]byte(`"resource.test1"`),
120-
NewResourceField("test1"),
119+
name: "ResourceSimpleField",
120+
input: []byte(`"resource.test1"`),
121+
expected: NewResourceField("test1"),
121122
},
122-
}
123-
124-
for _, tc := range cases {
125-
t.Run(tc.name, func(t *testing.T) {
126-
var f Field
127-
err := json.Unmarshal(tc.input, &f)
128-
require.NoError(t, err)
129-
require.Equal(t, tc.expected, f)
130-
})
131-
}
132-
}
133-
134-
func TestFieldUnmarshalJSONFailure(t *testing.T) {
135-
cases := []struct {
136-
name string
137-
input []byte
138-
expected string
139-
}{
140123
{
141-
"Bool",
142-
[]byte(`"bool"`),
143-
"unrecognized prefix",
124+
name: "AttributesRoot",
125+
input: []byte(`"attributes"`),
126+
expectedErr: "attributes cannot be referenced without subfield",
127+
expected: NewAttributeField(),
144128
},
145129
{
146-
"Object",
147-
[]byte(`{"key":"value"}`),
148-
"cannot unmarshal object into Go value of type string",
130+
name: "ResourceRoot",
131+
input: []byte(`"resource"`),
132+
expectedErr: "resource cannot be referenced without subfield",
133+
expected: NewResourceField(),
149134
},
150135
{
151-
"AttributesRoot",
152-
[]byte(`"attributes"`),
153-
"attributes cannot be referenced without subfield",
136+
name: "Bool",
137+
input: []byte(`"bool"`),
138+
expectedErrRootable: "unrecognized prefix",
154139
},
155140
{
156-
"ResourceRoot",
157-
[]byte(`"resource"`),
158-
"resource cannot be referenced without subfield",
141+
name: "Object",
142+
input: []byte(`{"key":"value"}`),
143+
expectedErrRootable: "cannot unmarshal object into Go value of type string",
159144
},
160145
}
161146

162147
for _, tc := range cases {
163148
t.Run(tc.name, func(t *testing.T) {
164-
var f Field
165-
err := json.Unmarshal(tc.input, &f)
166-
require.Error(t, err)
167-
require.Contains(t, err.Error(), tc.expected)
149+
var field Field
150+
err := json.Unmarshal(tc.input, &field)
151+
152+
var rootableField RootableField
153+
errRootable := json.Unmarshal(tc.input, &rootableField)
154+
155+
switch {
156+
case tc.expectedErrRootable != "":
157+
require.Error(t, err)
158+
require.Contains(t, err.Error(), tc.expectedErr)
159+
require.Error(t, errRootable)
160+
require.Contains(t, errRootable.Error(), tc.expectedErrRootable)
161+
case tc.expectedErr != "":
162+
require.Error(t, err)
163+
require.Contains(t, err.Error(), tc.expectedErr)
164+
require.NoError(t, errRootable)
165+
require.Equal(t, tc.expected, rootableField.Field)
166+
default:
167+
require.NoError(t, err)
168+
require.Equal(t, tc.expected, field)
169+
require.NoError(t, errRootable)
170+
require.Equal(t, tc.expected, rootableField.Field)
171+
}
168172
})
169173
}
170174
}

pkg/stanza/operator/helper/parser.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,20 @@ func NewParserConfig(operatorID, operatorType string) ParserConfig {
2929
return ParserConfig{
3030
TransformerConfig: NewTransformerConfig(operatorID, operatorType),
3131
ParseFrom: entry.NewBodyField(),
32-
ParseTo: entry.NewAttributeField(),
32+
ParseTo: entry.RootableField{Field: entry.NewAttributeField()},
3333
}
3434
}
3535

3636
// ParserConfig provides the basic implementation of a parser config.
3737
type ParserConfig struct {
3838
TransformerConfig `mapstructure:",squash" yaml:",inline"`
39-
40-
ParseFrom entry.Field `mapstructure:"parse_from" json:"parse_from" yaml:"parse_from"`
41-
ParseTo entry.Field `mapstructure:"parse_to" json:"parse_to" yaml:"parse_to"`
42-
BodyField *entry.Field `mapstructure:"body" json:"body" yaml:"body"`
43-
TimeParser *TimeParser `mapstructure:"timestamp,omitempty" json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
44-
SeverityConfig *SeverityConfig `mapstructure:"severity,omitempty" json:"severity,omitempty" yaml:"severity,omitempty"`
45-
TraceParser *TraceParser `mapstructure:"trace,omitempty" json:"trace,omitempty" yaml:"trace,omitempty"`
46-
ScopeNameParser *ScopeNameParser `mapstructure:"scope_name,omitempty" json:"scope_name,omitempty" yaml:"scope_name,omitempty"`
39+
ParseFrom entry.Field `mapstructure:"parse_from" json:"parse_from" yaml:"parse_from"`
40+
ParseTo entry.RootableField `mapstructure:"parse_to" json:"parse_to" yaml:"parse_to"`
41+
BodyField *entry.Field `mapstructure:"body" json:"body" yaml:"body"`
42+
TimeParser *TimeParser `mapstructure:"timestamp,omitempty" json:"timestamp,omitempty" yaml:"timestamp,omitempty"`
43+
SeverityConfig *SeverityConfig `mapstructure:"severity,omitempty" json:"severity,omitempty" yaml:"severity,omitempty"`
44+
TraceParser *TraceParser `mapstructure:"trace,omitempty" json:"trace,omitempty" yaml:"trace,omitempty"`
45+
ScopeNameParser *ScopeNameParser `mapstructure:"scope_name,omitempty" json:"scope_name,omitempty" yaml:"scope_name,omitempty"`
4746
}
4847

4948
// Build will build a parser operator.
@@ -60,7 +59,7 @@ func (c ParserConfig) Build(logger *zap.SugaredLogger) (ParserOperator, error) {
6059
parserOperator := ParserOperator{
6160
TransformerOperator: transformerOperator,
6261
ParseFrom: c.ParseFrom,
63-
ParseTo: c.ParseTo,
62+
ParseTo: c.ParseTo.Field,
6463
BodyField: c.BodyField,
6564
}
6665

0 commit comments

Comments
 (0)