diff --git a/config/common.go b/config/common.go index c02e38264ca..d39bee8fd65 100644 --- a/config/common.go +++ b/config/common.go @@ -15,6 +15,7 @@ package config // import "go.opentelemetry.io/collector/config" import ( "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder" ) // Type is the component type as it is used in the config. @@ -32,6 +33,9 @@ type Unmarshallable interface { // Unmarshal is a function that unmarshalls a confmap.Conf into the unmarshable struct in a custom way. // The confmap.Conf for this specific component may be nil or empty if no config available. Unmarshal(component *confmap.Conf) error + // Marshal is a function that marshals the config into an interface to be encoded by + // the YAML encoder. Provides the configencoder.Encoder. + Marshal(encoder configencoder.Encoder) (interface{}, error) } // DataType is a special Type that represents the data types supported by the collector. We currently support diff --git a/config/configcompression/compressionType.go b/config/configcompression/compressionType.go index d38435c8702..66c91623bd9 100644 --- a/config/configcompression/compressionType.go +++ b/config/configcompression/compressionType.go @@ -32,6 +32,10 @@ func IsCompressed(compressionType CompressionType) bool { return compressionType != empty && compressionType != none } +func (ct CompressionType) MarshalText() (out []byte, err error) { + return []byte(ct), nil +} + func (ct *CompressionType) UnmarshalText(in []byte) error { switch typ := CompressionType(in); typ { case Gzip, 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/identifiable.go b/config/identifiable.go index b4482d7ca21..5474f71d394 100644 --- a/config/identifiable.go +++ b/config/identifiable.go @@ -69,6 +69,11 @@ func (id ComponentID) Name() string { return id.nameVal } +// MarshalText implements the encoding.TextMarshaler interface. +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/receiver/otlpreceiver/config.go b/receiver/otlpreceiver/config.go index 6a4552db25a..6b785e0fcf9 100644 --- a/receiver/otlpreceiver/config.go +++ b/receiver/otlpreceiver/config.go @@ -21,6 +21,7 @@ import ( "go.opentelemetry.io/collector/config/configgrpc" "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder" ) const ( @@ -32,8 +33,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. @@ -82,3 +83,7 @@ func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error { return nil } + +func (cfg *Config) Marshal(enc configencoder.Encoder) (interface{}, error) { + return enc.Encode(cfg) +} diff --git a/service/configmarshaler/configencoder/encoder.go b/service/configmarshaler/configencoder/encoder.go new file mode 100644 index 00000000000..46e2fa500dd --- /dev/null +++ b/service/configmarshaler/configencoder/encoder.go @@ -0,0 +1,20 @@ +// 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 configencoder // import "go.opentelemetry.io/collector/service/configmarshaler/configencoder" + +type Encoder interface { + // Encode encodes the input into the output. + Encode(in interface{}) (out interface{}, err error) +} diff --git a/service/configmarshaler/configencoder/encodertest/encoder.go b/service/configmarshaler/configencoder/encodertest/encoder.go new file mode 100644 index 00000000000..148de428240 --- /dev/null +++ b/service/configmarshaler/configencoder/encodertest/encoder.go @@ -0,0 +1,40 @@ +// 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 encodertest // import "go.opentelemetry.io/collector/service/configmarshaler/configencoder/encodertest" + +import "go.opentelemetry.io/collector/service/configmarshaler/configencoder" + +// nopEncoder mocks a configencoder.Encoder for test purposes. +type nopEncoder struct { + err error +} + +var _ configencoder.Encoder = (*nopEncoder)(nil) + +// NewNop returns a new instance of nopEncoder. +func NewNop() configencoder.Encoder { + return &nopEncoder{} +} + +// NewNopWithErr returns a new instance of nopEncoder with +// an error set. +func NewNopWithErr(err error) configencoder.Encoder { + return &nopEncoder{err} +} + +// Encode returns nil and the error if it is set. +func (ne nopEncoder) Encode(interface{}) (interface{}, error) { + return nil, ne.err +} diff --git a/service/configmarshaler/configencoder/encodertest/encoder_test.go b/service/configmarshaler/configencoder/encodertest/encoder_test.go new file mode 100644 index 00000000000..fd7e53e2b26 --- /dev/null +++ b/service/configmarshaler/configencoder/encodertest/encoder_test.go @@ -0,0 +1,36 @@ +// 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 encodertest + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewNop(t *testing.T) { + encoder := NewNop() + got, err := encoder.Encode(nil) + require.NoError(t, err) + require.Nil(t, got) +} + +func TestNewNopWithErr(t *testing.T) { + wantErr := errors.New("test") + encoder := NewNopWithErr(wantErr) + _, err := encoder.Encode(nil) + require.Equal(t, wantErr, err) +} diff --git a/service/configmarshaler/configencoder/mapstructure/encoder.go b/service/configmarshaler/configencoder/mapstructure/encoder.go new file mode 100644 index 00000000000..cb8b5548b34 --- /dev/null +++ b/service/configmarshaler/configencoder/mapstructure/encoder.go @@ -0,0 +1,214 @@ +// 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/service/configmarshaler/configencoder/mapstructure" + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strings" + + "gopkg.in/yaml.v3" + + "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder" +) + +const ( + tagNameMapStructure = "mapstructure" + optionSeparator = "," + optionOmitEmpty = "omitempty" + optionSquash = "squash" + optionRemain = "remain" + optionSkip = "-" +) + +var ( + ErrNonStringEncodedKey = errors.New("non string-encoded key") + // emptyNode is a workaround to encode nil as empty + emptyNode = &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "", + Style: yaml.FlowStyle, + } +) + +// tagInfo stores the mapstructure tag details. +type tagInfo struct { + name string + omitEmpty bool + squash bool +} + +type mapStructureEncoder struct { +} + +var _ configencoder.Encoder = (*mapStructureEncoder)(nil) + +// NewEncoder returns an encoder.Encoder that supports mapstructure tags. +func NewEncoder() configencoder.Encoder { + return &mapStructureEncoder{} +} + +// Encode encodes the input following the mapstructure tag spec. +func (mse *mapStructureEncoder) Encode(input interface{}) (interface{}, error) { + return mse.encode(reflect.ValueOf(input)) +} + +// encode processes the value based on the reflect.Kind. +func (mse *mapStructureEncoder) encode(value reflect.Value) (interface{}, error) { + if value.IsValid() { + switch value.Kind() { + case reflect.Interface, reflect.Ptr: + return mse.encode(value.Elem()) + case reflect.Map: + return mse.encodeMap(value) + case reflect.Slice: + return mse.encodeSlice(value) + case reflect.Struct: + return mse.encodeStruct(value) + default: + return value.Interface(), nil + } + } + return emptyNode, nil +} + +// encodeStruct encodes the struct by iterating over the fields, getting the +// mapstructure tagInfo for each exported field, and encoding the value. +func (mse *mapStructureEncoder) encodeStruct(value reflect.Value) (interface{}, error) { + if value.Kind() != reflect.Struct { + return nil, &reflect.ValueError{ + Method: "encodeStruct", + Kind: value.Kind(), + } + } + switch marshaler := value.Interface().(type) { + case encoding.TextMarshaler: + out, err := marshaler.MarshalText() + if err != nil { + return nil, err + } + return string(out), nil + case config.Unmarshallable: + out, err := marshaler.Marshal(mse) + if err != nil { + return nil, err + } + return out, nil + } + result := make(map[string]interface{}) + for i := 0; i < value.NumField(); i++ { + field := value.Field(i) + if field.CanInterface() { + info := mse.getTagInfo(value.Type().Field(i)) + if (info.omitEmpty && field.IsZero()) || info.name == optionSkip { + continue + } + encoded, err := mse.encode(field) + if err != nil { + return nil, 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 + } + } + } + if len(result) == 0 { + return emptyNode, nil + } + return result, nil +} + +// encodeSlice iterates over the slice and encodes each of the elements. +func (mse *mapStructureEncoder) 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 = mse.encode(value.Index(i)); err != nil { + return nil, err + } + } + if len(result) == 0 { + return emptyNode, nil + } + 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 (mse *mapStructureEncoder) 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 := mse.encode(iterator.Key()) + if err != nil { + return nil, err + } + key, ok := encoded.(string) + if !ok { + return nil, fmt.Errorf("%w | input: %#+v, output: %#+v", ErrNonStringEncodedKey, iterator.Key().Interface(), encoded) + } + if result[key], err = mse.encode(iterator.Value()); err != nil { + return nil, err + } + } + if len(result) == 0 { + return emptyNode, nil + } + 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 (mse *mapStructureEncoder) 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 +} diff --git a/service/configmarshaler/configencoder/mapstructure/encoder_test.go b/service/configmarshaler/configencoder/mapstructure/encoder_test.go new file mode 100644 index 00000000000..740a2dc10e7 --- /dev/null +++ b/service/configmarshaler/configencoder/mapstructure/encoder_test.go @@ -0,0 +1,257 @@ +// 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 ( + "errors" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/collector/config" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder" +) + +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 config.Unmarshallable +} + +type TestSimpleStruct struct { + Value string `mapstructure:"value"` + skipped string +} + +type TestEmptyStruct struct { + Value string `mapstructure:"-"` +} + +type TestUnmarshallableStruct struct { + result interface{} + err error +} + +var _ config.Unmarshallable = (*TestUnmarshallableStruct)(nil) + +func (tus TestUnmarshallableStruct) Unmarshal(*confmap.Conf) error { + panic("unused in test") +} + +func (tus TestUnmarshallableStruct) Marshal(configencoder.Encoder) (interface{}, error) { + return tus.result, tus.err +} + +func TestEncode(t *testing.T) { + enc := NewEncoder().(*mapStructureEncoder) + testCases := map[string]struct { + input interface{} + want interface{} + }{ + "WithString": { + input: "test", + want: "test", + }, + "WithComponentID": { + input: config.NewComponentIDWithName("type", "name"), + want: "type/name", + }, + "WithSlice": { + input: []config.ComponentID{ + config.NewComponentID("nop"), + config.NewComponentIDWithName("type", "name"), + }, + want: []interface{}{"nop", "type/name"}, + }, + "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: &TestUnmarshallableStruct{ + result: "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) + }) + } +} + +func TestGetTagInfo(t *testing.T) { + enc := NewEncoder().(*mapStructureEncoder) + 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 := enc.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 := NewEncoder().(*mapStructureEncoder) + 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) { + testCase := []struct { + Test map[TestEmptyStruct]TestSimpleStruct + }{ + { + Test: map[TestEmptyStruct]TestSimpleStruct{ + {Value: "key"}: {Value: "value"}, + }, + }, + } + enc := NewEncoder() + got, err := enc.Encode(testCase) + require.Error(t, err) + require.True(t, errors.Is(err, ErrNonStringEncodedKey)) + require.Nil(t, got) +} + +func TestEmptyNode(t *testing.T) { + enc := NewEncoder() + testCases := []struct { + input interface{} + }{ + {input: nil}, + {input: []TestEmptyStruct{}}, + {input: map[string]TestEmptyStruct{}}, + {input: TestEmptyStruct{}}, + } + for _, testCase := range testCases { + got, err := enc.Encode(testCase.input) + require.NoError(t, err) + require.Equal(t, emptyNode, got) + } +} + +func TestStructMarshalerError(t *testing.T) { + wantErr := errors.New("test") + enc := NewEncoder().(*mapStructureEncoder) + got, err := enc.encodeStruct(reflect.ValueOf(TestUnmarshallableStruct{err: wantErr})) + require.Nil(t, got) + require.Equal(t, wantErr, err) +} diff --git a/service/configmarshaler/defaultmarshaler.go b/service/configmarshaler/defaultmarshaler.go new file mode 100644 index 00000000000..1d51587a9b5 --- /dev/null +++ b/service/configmarshaler/defaultmarshaler.go @@ -0,0 +1,63 @@ +// 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 configmarshaler // import "go.opentelemetry.io/collector/service/configmarshaler" + +import ( + "bytes" + + "gopkg.in/yaml.v3" + + "go.opentelemetry.io/collector/service" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder/mapstructure" +) + +const ( + defaultIndent = 2 +) + +type ConfigMarshaler struct { + // encoder is the encoder.Encoder to use to convert the service.Config + // into a form usable by the yaml.Encoder. + encoder configencoder.Encoder +} + +// New creates a new default ConfigMarshaler using the mapstructure.NewEncoder. +func New() *ConfigMarshaler { + return NewWithEncoder(mapstructure.NewEncoder()) +} + +// NewWithEncoder creates a new default ConfigMarshaler with the provided encoder.Encoder. +func NewWithEncoder(encoder configencoder.Encoder) *ConfigMarshaler { + return &ConfigMarshaler{encoder} +} + +// Marshal converts a service.Config into YAML. +func (cm *ConfigMarshaler) Marshal(cfg *service.Config) ([]byte, error) { + output, err := cm.encoder.Encode(cfg) + if err != nil { + return nil, err + } + var buffer bytes.Buffer + enc := yaml.NewEncoder(&buffer) + enc.SetIndent(defaultIndent) + if err = enc.Encode(output); err != nil { + return nil, err + } + if err = enc.Close(); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} diff --git a/service/configmarshaler/defaultmarshaler_test.go b/service/configmarshaler/defaultmarshaler_test.go new file mode 100644 index 00000000000..c6257de3f1e --- /dev/null +++ b/service/configmarshaler/defaultmarshaler_test.go @@ -0,0 +1,75 @@ +// 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 configmarshaler + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/internal/testcomponents" + "go.opentelemetry.io/collector/service" + "go.opentelemetry.io/collector/service/configmarshaler/configencoder/encodertest" + "go.opentelemetry.io/collector/service/internal/configunmarshaler" +) + +func TestMarshalCycle(t *testing.T) { + factories, err := testcomponents.ExampleComponents() + require.NoError(t, err) + testCases := map[string]struct { + filename string + }{ + "WithEmptyConfig": { + filename: "empty.yaml", + }, + "WithValidConfig": { + filename: "config.yaml", + }, + } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + expectedContent, err := os.ReadFile(filepath.Join("testdata", testCase.filename)) + require.NoError(t, err) + + var data map[string]interface{} + require.NoError(t, yaml.Unmarshal(expectedContent, &data)) + + configMap := confmap.NewFromStringMap(data) + + unmarshaler := configunmarshaler.New() + marshaler := New() + + actualConfig, err := unmarshaler.Unmarshal(configMap, factories) + require.NoError(t, err) + + actualContent, err := marshaler.Marshal(actualConfig) + require.NoError(t, err) + + require.Equal(t, string(expectedContent), string(actualContent)) + }) + } +} + +func TestMarshalError(t *testing.T) { + cfg := &service.Config{} + marshaler := NewWithEncoder(encodertest.NewNopWithErr(errors.New("test"))) + _, err := marshaler.Marshal(cfg) + require.Error(t, err) +} diff --git a/service/configmarshaler/testdata/config.yaml b/service/configmarshaler/testdata/config.yaml new file mode 100644 index 00000000000..a8982133c32 --- /dev/null +++ b/service/configmarshaler/testdata/config.yaml @@ -0,0 +1,33 @@ +exporters: + exampleexporter: +extensions: +processors: + exampleprocessor: +receivers: + examplereceiver: +service: + extensions: + pipelines: + metrics: + exporters: + - exampleexporter + processors: + - exampleprocessor + receivers: + - examplereceiver + telemetry: + logs: + development: true + disable_caller: false + disable_stacktrace: false + encoding: console + error_output_paths: + - stderr + initial_fields: + level: debug + output_paths: + - stderr + metrics: + address: :8080 + level: normal + resource: diff --git a/service/configmarshaler/testdata/empty.yaml b/service/configmarshaler/testdata/empty.yaml new file mode 100644 index 00000000000..d82100538f0 --- /dev/null +++ b/service/configmarshaler/testdata/empty.yaml @@ -0,0 +1,23 @@ +exporters: +extensions: +processors: +receivers: +service: + extensions: + pipelines: + telemetry: + logs: + development: false + disable_caller: false + disable_stacktrace: false + encoding: console + error_output_paths: + - stderr + initial_fields: + level: info + output_paths: + - stderr + metrics: + address: :8888 + level: basic + resource: