Skip to content

Commit

Permalink
Add config marshaler.
Browse files Browse the repository at this point in the history
  • Loading branch information
jefchien committed Sep 7, 2022
1 parent f4e6175 commit 92822b6
Show file tree
Hide file tree
Showing 12 changed files with 726 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
### 💡 Enhancements 💡

- Add `skip-get-modules` builder flag to support isolated environment executions (#6009)
- Add config marshaler (#5566)

## v0.59.0 Beta

Expand Down
6 changes: 6 additions & 0 deletions config/configtelemetry/configtelemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions config/configtelemetry/configtelemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
5 changes: 5 additions & 0 deletions config/identifiable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions config/identifiable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
53 changes: 53 additions & 0 deletions confmap/confmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/service/configmarshaler/encoder/mapstructure"
)

const (
Expand Down Expand Up @@ -76,6 +78,20 @@ func (l *Conf) UnmarshalExact(rawVal interface{}) error {
return decoder.Decode(l.ToStringMap())
}

// 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)
Expand Down Expand Up @@ -135,6 +151,18 @@ func decoderConfig(result interface{}) *mapstructure.DecoderConfig {
}
}

// 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
Expand Down Expand Up @@ -209,3 +237,28 @@ func mapKeyStringToMapKeyTextUnmarshalerHookFunc() mapstructure.DecodeHookFuncTy
return data, nil
}
}

// 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
}
}
77 changes: 77 additions & 0 deletions confmap/confmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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"`
Expand Down Expand Up @@ -218,6 +238,63 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T)
assert.Error(t, conf.UnmarshalExact(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))
Expand Down
24 changes: 24 additions & 0 deletions confmap/marshaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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 confmap

// 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
}
4 changes: 2 additions & 2 deletions receiver/otlpreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 92822b6

Please sign in to comment.