diff --git a/CHANGELOG.md b/CHANGELOG.md index 45154704f1f..530db264e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Add AppendEmpty and EnsureCapacity method to primitive pdata slices (#6060) - Expose `AsRaw` and `FromRaw` `pcommon.Value` methods (#6090) +- Add config marshaler (#5566) ## v0.60.0 Beta diff --git a/config/configtelemetry/configtelemetry.go b/config/configtelemetry/configtelemetry.go index a363db1e54c..a4a8d541daa 100644 --- a/config/configtelemetry/configtelemetry.go +++ b/config/configtelemetry/configtelemetry.go @@ -41,6 +41,7 @@ const ( // that every component should generate. type Level int32 +var _ encoding.TextMarshaler = (*Level)(nil) var _ encoding.TextUnmarshaler = (*Level)(nil) func (l Level) String() string { @@ -57,6 +58,11 @@ func (l Level) String() string { return "unknown" } +// MarshalText marshals Level to text. +func (l Level) MarshalText() (text []byte, err error) { + return []byte(l.String()), nil +} + // UnmarshalText unmarshalls text to a Level. func (l *Level) UnmarshalText(text []byte) error { if l == nil { diff --git a/config/configtelemetry/configtelemetry_test.go b/config/configtelemetry/configtelemetry_test.go index 7eb8b84af11..6fda8eb4321 100644 --- a/config/configtelemetry/configtelemetry_test.go +++ b/config/configtelemetry/configtelemetry_test.go @@ -103,6 +103,9 @@ func TestLevelString(t *testing.T) { for _, test := range tests { t.Run(test.str, func(t *testing.T) { assert.Equal(t, test.str, test.level.String()) + got, err := test.level.MarshalText() + assert.NoError(t, err) + assert.Equal(t, test.str, string(got)) }) } } diff --git a/config/identifiable.go b/config/identifiable.go index b4482d7ca21..10637e2c800 100644 --- a/config/identifiable.go +++ b/config/identifiable.go @@ -69,6 +69,12 @@ func (id ComponentID) Name() string { return id.nameVal } +// MarshalText implements the encoding.TextMarshaler interface. +// This marshals the type and name as one string in the config. +func (id ComponentID) MarshalText() (text []byte, err error) { + return []byte(id.String()), nil +} + // UnmarshalText implements the encoding.TextUnmarshaler interface. func (id *ComponentID) UnmarshalText(text []byte) error { idStr := string(text) diff --git a/config/identifiable_test.go b/config/identifiable_test.go index fe342346d4d..09d6be986c7 100644 --- a/config/identifiable_test.go +++ b/config/identifiable_test.go @@ -76,3 +76,10 @@ func TestIDFromString(t *testing.T) { }) } } + +func TestMarshalText(t *testing.T) { + id := NewComponentIDWithName("test", "name") + got, err := id.MarshalText() + assert.NoError(t, err) + assert.Equal(t, id.String(), string(got)) +} diff --git a/confmap/confmap.go b/confmap/confmap.go index a7bf9bd4e5c..b7b46da4e70 100644 --- a/confmap/confmap.go +++ b/confmap/confmap.go @@ -23,6 +23,8 @@ import ( "github.com/knadh/koanf/maps" "github.com/knadh/koanf/providers/confmap" "github.com/mitchellh/mapstructure" + + encoder "go.opentelemetry.io/collector/confmap/internal/mapstructure" ) const ( @@ -66,6 +68,20 @@ func (l *Conf) UnmarshalExact(result interface{}) error { return decodeConfig(l, result, true) } +// Marshal encodes the config and merges it into the Conf. +func (l *Conf) Marshal(rawVal interface{}) error { + enc := encoder.New(encoderConfig(rawVal)) + data, err := enc.Encode(rawVal) + if err != nil { + return err + } + out, ok := data.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid config encoding") + } + return l.Merge(NewFromStringMap(out)) +} + // Get can retrieve any value given the key to use. func (l *Conf) Get(key string) interface{} { return l.k.Get(key) @@ -133,6 +149,18 @@ func decodeConfig(m *Conf, result interface{}, errorUnused bool) error { return decoder.Decode(m.ToStringMap()) } +// encoderConfig returns a default encoder.EncoderConfig that includes +// an EncodeHook that handles both TextMarshaller and Marshaler +// interfaces. +func encoderConfig(rawVal interface{}) *encoder.EncoderConfig { + return &encoder.EncoderConfig{ + EncodeHook: mapstructure.ComposeDecodeHookFunc( + encoder.TextMarshalerHookFunc(), + marshalerHookFunc(rawVal), + ), + } +} + // In cases where a config has a mapping of something to a struct pointers // we want nil values to resolve to a pointer to the zero value of the // underlying struct just as we want nil values of a mapping of something @@ -239,9 +267,43 @@ func unmarshalerHookFunc(result interface{}) mapstructure.DecodeHookFuncValue { } } +// marshalerHookFunc returns a DecodeHookFuncValue that checks structs that aren't +// the top level struct to see if they implement the Marshaler interface. +func marshalerHookFunc(top interface{}) mapstructure.DecodeHookFuncValue { + topValue := reflect.ValueOf(top) + return func(from reflect.Value, _ reflect.Value) (interface{}, error) { + if from.Kind() != reflect.Struct { + return from.Interface(), nil + } + + // ignore top structure to avoid infinite loop. + if from == topValue { + return from.Interface(), nil + } + marshaler, ok := from.Interface().(Marshaler) + if !ok { + return from.Interface(), nil + } + conf := New() + if err := marshaler.Marshal(conf); err != nil { + return nil, err + } + return conf.ToStringMap(), nil + } +} + // Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf. type Unmarshaler interface { // Unmarshal a Conf into the struct in a custom way. // The Conf for this specific component may be nil or empty if no config available. Unmarshal(component *Conf) error } + +// Marshaler defines an optional interface for custom configuration marshalling. +// A configuration struct can implement this interface to override the default +// marshalling. +type Marshaler interface { + // Marshal the config into a Conf in a custom way. + // The Conf will be empty and can be merged into. + Marshal(component *Conf) error +} diff --git a/confmap/confmap_test.go b/confmap/confmap_test.go index f7abb928619..7625f1bfbd8 100644 --- a/confmap/confmap_test.go +++ b/confmap/confmap_test.go @@ -160,6 +160,18 @@ type TestConfig struct { MapStruct map[string]*Struct `mapstructure:"map_struct"` } +func (t TestConfig) Marshal(conf *Conf) error { + if t.Boolean != nil && !*t.Boolean { + return errors.New("unable to marshal") + } + if err := conf.Marshal(t); err != nil { + return err + } + return conf.Merge(NewFromStringMap(map[string]interface{}{ + "additional": "field", + })) +} + type Struct struct { Name string } @@ -174,6 +186,14 @@ func (tID *TestID) UnmarshalText(text []byte) error { return nil } +func (tID TestID) MarshalText() (text []byte, err error) { + out := string(tID) + if !strings.HasSuffix(out, "_") { + out += "_" + } + return []byte(out), nil +} + type TestIDConfig struct { Boolean bool `mapstructure:"bool"` Map map[TestID]string `mapstructure:"map"` @@ -232,6 +252,63 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T) assert.Error(t, conf.Unmarshal(cfg)) } +func TestMarshal(t *testing.T) { + conf := New() + cfg := &TestIDConfig{ + Boolean: true, + Map: map[TestID]string{ + "string": "this is a string", + }, + } + assert.NoError(t, conf.Marshal(cfg)) + assert.Equal(t, true, conf.Get("bool")) + assert.Equal(t, map[string]interface{}{"string_": "this is a string"}, conf.Get("map")) +} + +func TestMarshalDuplicateID(t *testing.T) { + conf := New() + cfg := &TestIDConfig{ + Boolean: true, + Map: map[TestID]string{ + "string": "this is a string", + "string_": "this is another string", + }, + } + assert.Error(t, conf.Marshal(cfg)) +} + +func TestMarshalError(t *testing.T) { + conf := New() + assert.Error(t, conf.Marshal(nil)) +} + +func TestMarshaler(t *testing.T) { + conf := New() + cfg := &TestConfig{ + Struct: &Struct{ + Name: "StructName", + }, + } + assert.NoError(t, conf.Marshal(cfg)) + assert.Equal(t, "field", conf.Get("additional")) + + conf = New() + type NestedMarshaler struct { + TestConfig *TestConfig + } + nmCfg := &NestedMarshaler{ + TestConfig: cfg, + } + assert.NoError(t, conf.Marshal(nmCfg)) + sub, err := conf.Sub("testconfig") + assert.NoError(t, err) + assert.True(t, sub.IsSet("additional")) + assert.Equal(t, "field", sub.Get("additional")) + varBool := false + nmCfg.TestConfig.Boolean = &varBool + assert.Error(t, conf.Marshal(nmCfg)) +} + // newConfFromFile creates a new Conf by reading the given file. func newConfFromFile(t testing.TB, fileName string) map[string]interface{} { content, err := os.ReadFile(filepath.Clean(fileName)) diff --git a/confmap/internal/mapstructure/encoder.go b/confmap/internal/mapstructure/encoder.go new file mode 100644 index 00000000000..94accf8e161 --- /dev/null +++ b/confmap/internal/mapstructure/encoder.go @@ -0,0 +1,234 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapstructure // import "go.opentelemetry.io/collector/confmap/internal/mapstructure" + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" +) + +const ( + tagNameMapStructure = "mapstructure" + optionSeparator = "," + optionOmitEmpty = "omitempty" + optionSquash = "squash" + optionRemain = "remain" + optionSkip = "-" +) + +var ( + errNonStringEncodedKey = errors.New("non string-encoded key") +) + +// tagInfo stores the mapstructure tag details. +type tagInfo struct { + name string + omitEmpty bool + squash bool +} + +// An Encoder takes structured data and converts it into an +// interface following the mapstructure tags. +type Encoder struct { + config *EncoderConfig +} + +// EncoderConfig is the configuration used to create a new encoder. +type EncoderConfig struct { + // EncodeHook, if set, is a way to provide custom encoding. It + // will be called before structs and primitive types. + EncodeHook mapstructure.DecodeHookFunc +} + +// New returns a new encoder for the configuration. +func New(cfg *EncoderConfig) *Encoder { + return &Encoder{config: cfg} +} + +// Encode takes the input and uses reflection to encode it to +// an interface based on the mapstructure spec. +func (e *Encoder) Encode(input interface{}) (interface{}, error) { + return e.encode(reflect.ValueOf(input)) +} + +// encode processes the value based on the reflect.Kind. +func (e *Encoder) encode(value reflect.Value) (interface{}, error) { + if value.IsValid() { + switch value.Kind() { + case reflect.Interface, reflect.Ptr: + return e.encode(value.Elem()) + case reflect.Map: + return e.encodeMap(value) + case reflect.Slice: + return e.encodeSlice(value) + case reflect.Struct: + return e.encodeStruct(value) + default: + return e.encodeHook(value) + } + } + return nil, nil +} + +// encodeHook calls the EncodeHook in the EncoderConfig with the value passed in. +// This is called before processing structs and for primitive data types. +func (e *Encoder) encodeHook(value reflect.Value) (interface{}, error) { + if e.config != nil && e.config.EncodeHook != nil { + out, err := mapstructure.DecodeHookExec(e.config.EncodeHook, value, value) + if err != nil { + return nil, fmt.Errorf("error running encode hook: %w", err) + } + return out, nil + } + return value.Interface(), nil +} + +// encodeStruct encodes the struct by iterating over the fields, getting the +// mapstructure tagInfo for each exported field, and encoding the value. +func (e *Encoder) encodeStruct(value reflect.Value) (interface{}, error) { + if value.Kind() != reflect.Struct { + return nil, &reflect.ValueError{ + Method: "encodeStruct", + Kind: value.Kind(), + } + } + out, err := e.encodeHook(value) + if err != nil { + return nil, err + } + value = reflect.ValueOf(out) + // if the output of encodeHook is no longer a struct, + // call encode against it. + if value.Kind() != reflect.Struct { + return e.encode(value) + } + result := make(map[string]interface{}) + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + if field.CanInterface() { + info := getTagInfo(value.Type().Field(i)) + if (info.omitEmpty && field.IsZero()) || info.name == optionSkip { + continue + } + encoded, err := e.encode(field) + if err != nil { + return nil, fmt.Errorf("error encoding field %q: %w", info.name, err) + } + if info.squash { + if m, ok := encoded.(map[string]interface{}); ok { + for k, v := range m { + result[k] = v + } + } + } else { + result[info.name] = encoded + } + } + } + return result, nil +} + +// encodeSlice iterates over the slice and encodes each of the elements. +func (e *Encoder) encodeSlice(value reflect.Value) (interface{}, error) { + if value.Kind() != reflect.Slice { + return nil, &reflect.ValueError{ + Method: "encodeSlice", + Kind: value.Kind(), + } + } + result := make([]interface{}, value.Len()) + for i := 0; i < value.Len(); i++ { + var err error + if result[i], err = e.encode(value.Index(i)); err != nil { + return nil, fmt.Errorf("error encoding element in slice at index %d: %w", i, err) + } + } + return result, nil +} + +// encodeMap encodes a map by encoding the key and value. Returns errNonStringEncodedKey +// if the key is not encoded into a string. +func (e *Encoder) encodeMap(value reflect.Value) (interface{}, error) { + if value.Kind() != reflect.Map { + return nil, &reflect.ValueError{ + Method: "encodeMap", + Kind: value.Kind(), + } + } + result := make(map[string]interface{}) + iterator := value.MapRange() + for iterator.Next() { + encoded, err := e.encode(iterator.Key()) + if err != nil { + return nil, fmt.Errorf("error encoding key: %w", err) + } + key, ok := encoded.(string) + if !ok { + return nil, fmt.Errorf("%w key %q, kind: %v", errNonStringEncodedKey, iterator.Key().Interface(), iterator.Key().Kind()) + } + if _, ok = result[key]; ok { + return nil, fmt.Errorf("duplicate key %q while encoding", key) + } + if result[key], err = e.encode(iterator.Value()); err != nil { + return nil, fmt.Errorf("error encoding map value for key %q: %w", key, err) + } + } + return result, nil +} + +// getTagInfo looks up the mapstructure tag and uses that if available. +// Uses the lowercase field if not found. Checks for omitempty and squash. +func getTagInfo(field reflect.StructField) *tagInfo { + info := tagInfo{} + if tag, ok := field.Tag.Lookup(tagNameMapStructure); ok { + options := strings.Split(tag, optionSeparator) + info.name = options[0] + if len(options) > 1 { + for _, option := range options[1:] { + switch option { + case optionOmitEmpty: + info.omitEmpty = true + case optionSquash, optionRemain: + info.squash = true + } + } + } + } else { + info.name = strings.ToLower(field.Name) + } + return &info +} + +// TextMarshalerHookFunc returns a DecodeHookFuncValue that checks +// for the encoding.TextMarshaler interface and calls the MarshalText +// function if found. +func TextMarshalerHookFunc() mapstructure.DecodeHookFuncValue { + return func(from reflect.Value, _ reflect.Value) (interface{}, error) { + marshaler, ok := from.Interface().(encoding.TextMarshaler) + if !ok { + return from.Interface(), nil + } + out, err := marshaler.MarshalText() + if err != nil { + return nil, err + } + return string(out), nil + } +} diff --git a/confmap/internal/mapstructure/encoder_test.go b/confmap/internal/mapstructure/encoder_test.go new file mode 100644 index 00000000000..5c65532512f --- /dev/null +++ b/confmap/internal/mapstructure/encoder_test.go @@ -0,0 +1,313 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mapstructure + +import ( + "encoding" + "errors" + "reflect" + "strings" + "testing" + + "github.com/mitchellh/mapstructure" + "github.com/stretchr/testify/require" +) + +type TestComplexStruct struct { + Skipped TestEmptyStruct `mapstructure:",squash"` + Nested TestSimpleStruct `mapstructure:",squash"` + Slice []TestSimpleStruct `mapstructure:"slice,omitempty"` + Pointer *TestSimpleStruct `mapstructure:"ptr"` + Map map[string]TestSimpleStruct `mapstructure:"map,omitempty"` + Remain map[string]interface{} `mapstructure:",remain"` + Interface encoding.TextMarshaler +} + +type TestSimpleStruct struct { + Value string `mapstructure:"value"` + skipped string + err error +} + +type TestEmptyStruct struct { + Value string `mapstructure:"-"` +} + +type TestID string + +func (tID TestID) MarshalText() (text []byte, err error) { + out := string(tID) + if out == "error" { + return nil, errors.New("parsing error") + } + if !strings.HasSuffix(out, "_") { + out += "_" + } + return []byte(out), nil +} + +func TestEncode(t *testing.T) { + enc := New(&EncoderConfig{ + EncodeHook: TextMarshalerHookFunc(), + }) + testCases := map[string]struct { + input interface{} + want interface{} + }{ + "WithString": { + input: "test", + want: "test", + }, + "WithTextMarshaler": { + input: TestID("type"), + want: "type_", + }, + "WithSlice": { + input: []TestID{ + TestID("nop"), + TestID("type_"), + }, + want: []interface{}{"nop_", "type_"}, + }, + "WithSimpleStruct": { + input: TestSimpleStruct{Value: "test", skipped: "skipped"}, + want: map[string]interface{}{ + "value": "test", + }, + }, + "WithComplexStruct": { + input: &TestComplexStruct{ + Skipped: TestEmptyStruct{ + Value: "omitted", + }, + Nested: TestSimpleStruct{ + Value: "nested", + }, + Slice: []TestSimpleStruct{ + {Value: "slice"}, + }, + Map: map[string]TestSimpleStruct{ + "Key": {Value: "map"}, + }, + Pointer: &TestSimpleStruct{ + Value: "pointer", + }, + Remain: map[string]interface{}{ + "remain1": 23, + "remain2": "value", + }, + Interface: TestID("value"), + }, + want: map[string]interface{}{ + "value": "nested", + "slice": []interface{}{map[string]interface{}{"value": "slice"}}, + "map": map[string]interface{}{ + "Key": map[string]interface{}{"value": "map"}, + }, + "ptr": map[string]interface{}{"value": "pointer"}, + "interface": "value_", + "remain1": 23, + "remain2": "value", + }, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + got, err := enc.Encode(testCase.input) + require.NoError(t, err) + require.Equal(t, testCase.want, got) + }) + } + // without the TextMarshalerHookFunc + enc.config.EncodeHook = nil + testCase := TestID("test") + got, err := enc.Encode(testCase) + require.NoError(t, err) + require.Equal(t, testCase, got) +} + +func TestGetTagInfo(t *testing.T) { + testCases := map[string]struct { + field reflect.StructField + wantName string + wantOmit bool + wantSquash bool + }{ + "WithoutTags": { + field: reflect.StructField{ + Name: "Test", + }, + wantName: "test", + }, + "WithoutMapStructureTag": { + field: reflect.StructField{ + Tag: `yaml:"hello,inline"`, + Name: "YAML", + }, + wantName: "yaml", + }, + "WithRename": { + field: reflect.StructField{ + Tag: `mapstructure:"hello"`, + Name: "Test", + }, + wantName: "hello", + }, + "WithOmitEmpty": { + field: reflect.StructField{ + Tag: `mapstructure:"hello,omitempty"`, + Name: "Test", + }, + wantName: "hello", + wantOmit: true, + }, + "WithSquash": { + field: reflect.StructField{ + Tag: `mapstructure:",squash"`, + Name: "Test", + }, + wantSquash: true, + }, + "WithRemain": { + field: reflect.StructField{ + Tag: `mapstructure:",remain"`, + Name: "Test", + }, + wantSquash: true, + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + got := getTagInfo(testCase.field) + require.Equal(t, testCase.wantName, got.name) + require.Equal(t, testCase.wantOmit, got.omitEmpty) + require.Equal(t, testCase.wantSquash, got.squash) + }) + } +} + +func TestEncodeValueError(t *testing.T) { + enc := New(nil) + testValue := reflect.ValueOf("") + testCases := []struct { + encodeFn func(value reflect.Value) (interface{}, error) + wantErr error + }{ + {encodeFn: enc.encodeMap, wantErr: &reflect.ValueError{Method: "encodeMap", Kind: reflect.String}}, + {encodeFn: enc.encodeStruct, wantErr: &reflect.ValueError{Method: "encodeStruct", Kind: reflect.String}}, + {encodeFn: enc.encodeSlice, wantErr: &reflect.ValueError{Method: "encodeSlice", Kind: reflect.String}}, + } + for _, testCase := range testCases { + got, err := testCase.encodeFn(testValue) + require.Error(t, err) + require.Equal(t, testCase.wantErr, err) + require.Nil(t, got) + } +} + +func TestEncodeNonStringEncodedKey(t *testing.T) { + enc := New(nil) + testCase := []struct { + Test map[string]interface{} + }{ + { + Test: map[string]interface{}{ + "test": map[TestEmptyStruct]TestSimpleStruct{ + {Value: "key"}: {Value: "value"}, + }, + }, + }, + } + got, err := enc.Encode(testCase) + require.Error(t, err) + require.True(t, errors.Is(err, errNonStringEncodedKey)) + require.Nil(t, got) +} + +func TestDuplicateKey(t *testing.T) { + enc := New(&EncoderConfig{ + EncodeHook: TextMarshalerHookFunc(), + }) + testCase := map[TestID]string{ + "test": "value", + "test_": "other value", + } + got, err := enc.Encode(testCase) + require.Error(t, err) + require.Nil(t, got) +} + +func TestTextMarshalerError(t *testing.T) { + enc := New(&EncoderConfig{ + EncodeHook: TextMarshalerHookFunc(), + }) + testCase := map[TestID]string{ + "error": "value", + } + got, err := enc.Encode(testCase) + require.Error(t, err) + require.Nil(t, got) +} + +func TestEncodeStruct(t *testing.T) { + enc := New(&EncoderConfig{ + EncodeHook: testHookFunc(), + }) + testCase := TestSimpleStruct{ + Value: "original", + skipped: "final", + } + got, err := enc.Encode(testCase) + require.NoError(t, err) + require.Equal(t, "final", got) +} + +func TestEncodeStructError(t *testing.T) { + enc := New(&EncoderConfig{ + EncodeHook: testHookFunc(), + }) + wantErr := errors.New("test") + testCase := map[TestSimpleStruct]string{ + {err: wantErr}: "value", + } + got, err := enc.Encode(testCase) + require.Error(t, err) + require.True(t, errors.Is(err, wantErr)) + require.Nil(t, got) +} + +func TestEncodeNil(t *testing.T) { + enc := New(nil) + got, err := enc.Encode(nil) + require.NoError(t, err) + require.Nil(t, got) +} + +func testHookFunc() mapstructure.DecodeHookFuncValue { + return func(from reflect.Value, _ reflect.Value) (interface{}, error) { + if from.Kind() != reflect.Struct { + return from.Interface(), nil + } + + got, ok := from.Interface().(TestSimpleStruct) + if !ok { + return from.Interface(), nil + } + if got.err != nil { + return nil, got.err + } + return got.skipped, nil + } +} diff --git a/receiver/otlpreceiver/config.go b/receiver/otlpreceiver/config.go index 68602551762..2fef4e1446e 100644 --- a/receiver/otlpreceiver/config.go +++ b/receiver/otlpreceiver/config.go @@ -32,8 +32,8 @@ const ( // Protocols is the configuration for the supported protocols. type Protocols struct { - GRPC *configgrpc.GRPCServerSettings `mapstructure:"grpc"` - HTTP *confighttp.HTTPServerSettings `mapstructure:"http"` + GRPC *configgrpc.GRPCServerSettings `mapstructure:"grpc,omitempty"` + HTTP *confighttp.HTTPServerSettings `mapstructure:"http,omitempty"` } // Config defines configuration for OTLP receiver. diff --git a/service/telemetry/config.go b/service/telemetry/config.go index 198dbef218f..696a808668b 100644 --- a/service/telemetry/config.go +++ b/service/telemetry/config.go @@ -88,7 +88,7 @@ type LogsConfig struct { // foo: "bar" // // By default, there is no initial field. - InitialFields map[string]interface{} `mapstructure:"initial_fields"` + InitialFields map[string]interface{} `mapstructure:"initial_fields,omitempty"` } // MetricsConfig exposes the common Telemetry configuration for one component.