Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config marshaler. #5566

Merged
merged 1 commit into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Deprecate `p[metric|log|trace]otlp.RegiserServer` in favor of `p[metric|log|trace]otlp.RegiserGRPCServer` (#6180)

### 💡 Enhancements 💡

- Add config marshaler (#5566)

## v0.61.0 Beta

### 🛑 Breaking changes 🛑
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
}
jefchien marked this conversation as resolved.
Show resolved Hide resolved

// 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))
})
}
}
6 changes: 6 additions & 0 deletions config/identifiable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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))
}
62 changes: 62 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/confmap/internal/mapstructure"
)

const (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -239,9 +267,43 @@ func unmarshalerHookFunc(result interface{}) mapstructure.DecodeHookFuncValue {
}
}

// marshalerHookFunc returns a DecodeHookFuncValue that checks structs that aren't
// the original to see if they implement the Marshaler interface.
func marshalerHookFunc(orig interface{}) mapstructure.DecodeHookFuncValue {
origType := reflect.TypeOf(orig)
return func(from reflect.Value, _ reflect.Value) (interface{}, error) {
if from.Kind() != reflect.Struct {
return from.Interface(), nil
}

// ignore original to avoid infinite loop.
if from.Type() == origType && reflect.DeepEqual(from.Interface(), orig) {
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 marshaling.
// A configuration struct can implement this interface to override the default
// marshaling.
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
}
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 @@ -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))
Expand Down
Loading