Skip to content

Commit e9311f6

Browse files
authored
Add config marshaler. (open-telemetry#5566)
1 parent b73cd13 commit e9311f6

File tree

9 files changed

+712
-0
lines changed

9 files changed

+712
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

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

9+
### 💡 Enhancements 💡
10+
11+
- Add config marshaler (#5566)
12+
913
## v0.61.0 Beta
1014

1115
### 🛑 Breaking changes 🛑

config/configtelemetry/configtelemetry.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
// that every component should generate.
4242
type Level int32
4343

44+
var _ encoding.TextMarshaler = (*Level)(nil)
4445
var _ encoding.TextUnmarshaler = (*Level)(nil)
4546

4647
func (l Level) String() string {
@@ -57,6 +58,11 @@ func (l Level) String() string {
5758
return "unknown"
5859
}
5960

61+
// MarshalText marshals Level to text.
62+
func (l Level) MarshalText() (text []byte, err error) {
63+
return []byte(l.String()), nil
64+
}
65+
6066
// UnmarshalText unmarshalls text to a Level.
6167
func (l *Level) UnmarshalText(text []byte) error {
6268
if l == nil {

config/configtelemetry/configtelemetry_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func TestLevelString(t *testing.T) {
103103
for _, test := range tests {
104104
t.Run(test.str, func(t *testing.T) {
105105
assert.Equal(t, test.str, test.level.String())
106+
got, err := test.level.MarshalText()
107+
assert.NoError(t, err)
108+
assert.Equal(t, test.str, string(got))
106109
})
107110
}
108111
}

config/identifiable.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ func (id ComponentID) Name() string {
6969
return id.nameVal
7070
}
7171

72+
// MarshalText implements the encoding.TextMarshaler interface.
73+
// This marshals the type and name as one string in the config.
74+
func (id ComponentID) MarshalText() (text []byte, err error) {
75+
return []byte(id.String()), nil
76+
}
77+
7278
// UnmarshalText implements the encoding.TextUnmarshaler interface.
7379
func (id *ComponentID) UnmarshalText(text []byte) error {
7480
idStr := string(text)

config/identifiable_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,10 @@ func TestIDFromString(t *testing.T) {
7676
})
7777
}
7878
}
79+
80+
func TestMarshalText(t *testing.T) {
81+
id := NewComponentIDWithName("test", "name")
82+
got, err := id.MarshalText()
83+
assert.NoError(t, err)
84+
assert.Equal(t, id.String(), string(got))
85+
}

confmap/confmap.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"github.com/knadh/koanf/maps"
2424
"github.com/knadh/koanf/providers/confmap"
2525
"github.com/mitchellh/mapstructure"
26+
27+
encoder "go.opentelemetry.io/collector/confmap/internal/mapstructure"
2628
)
2729

2830
const (
@@ -66,6 +68,20 @@ func (l *Conf) UnmarshalExact(result interface{}) error {
6668
return decodeConfig(l, result, true)
6769
}
6870

71+
// Marshal encodes the config and merges it into the Conf.
72+
func (l *Conf) Marshal(rawVal interface{}) error {
73+
enc := encoder.New(encoderConfig(rawVal))
74+
data, err := enc.Encode(rawVal)
75+
if err != nil {
76+
return err
77+
}
78+
out, ok := data.(map[string]interface{})
79+
if !ok {
80+
return fmt.Errorf("invalid config encoding")
81+
}
82+
return l.Merge(NewFromStringMap(out))
83+
}
84+
6985
// Get can retrieve any value given the key to use.
7086
func (l *Conf) Get(key string) interface{} {
7187
return l.k.Get(key)
@@ -133,6 +149,18 @@ func decodeConfig(m *Conf, result interface{}, errorUnused bool) error {
133149
return decoder.Decode(m.ToStringMap())
134150
}
135151

152+
// encoderConfig returns a default encoder.EncoderConfig that includes
153+
// an EncodeHook that handles both TextMarshaller and Marshaler
154+
// interfaces.
155+
func encoderConfig(rawVal interface{}) *encoder.EncoderConfig {
156+
return &encoder.EncoderConfig{
157+
EncodeHook: mapstructure.ComposeDecodeHookFunc(
158+
encoder.TextMarshalerHookFunc(),
159+
marshalerHookFunc(rawVal),
160+
),
161+
}
162+
}
163+
136164
// In cases where a config has a mapping of something to a struct pointers
137165
// we want nil values to resolve to a pointer to the zero value of the
138166
// underlying struct just as we want nil values of a mapping of something
@@ -239,9 +267,43 @@ func unmarshalerHookFunc(result interface{}) mapstructure.DecodeHookFuncValue {
239267
}
240268
}
241269

270+
// marshalerHookFunc returns a DecodeHookFuncValue that checks structs that aren't
271+
// the original to see if they implement the Marshaler interface.
272+
func marshalerHookFunc(orig interface{}) mapstructure.DecodeHookFuncValue {
273+
origType := reflect.TypeOf(orig)
274+
return func(from reflect.Value, _ reflect.Value) (interface{}, error) {
275+
if from.Kind() != reflect.Struct {
276+
return from.Interface(), nil
277+
}
278+
279+
// ignore original to avoid infinite loop.
280+
if from.Type() == origType && reflect.DeepEqual(from.Interface(), orig) {
281+
return from.Interface(), nil
282+
}
283+
marshaler, ok := from.Interface().(Marshaler)
284+
if !ok {
285+
return from.Interface(), nil
286+
}
287+
conf := New()
288+
if err := marshaler.Marshal(conf); err != nil {
289+
return nil, err
290+
}
291+
return conf.ToStringMap(), nil
292+
}
293+
}
294+
242295
// Unmarshaler interface may be implemented by types to customize their behavior when being unmarshaled from a Conf.
243296
type Unmarshaler interface {
244297
// Unmarshal a Conf into the struct in a custom way.
245298
// The Conf for this specific component may be nil or empty if no config available.
246299
Unmarshal(component *Conf) error
247300
}
301+
302+
// Marshaler defines an optional interface for custom configuration marshaling.
303+
// A configuration struct can implement this interface to override the default
304+
// marshaling.
305+
type Marshaler interface {
306+
// Marshal the config into a Conf in a custom way.
307+
// The Conf will be empty and can be merged into.
308+
Marshal(component *Conf) error
309+
}

confmap/confmap_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ type TestConfig struct {
160160
MapStruct map[string]*Struct `mapstructure:"map_struct"`
161161
}
162162

163+
func (t TestConfig) Marshal(conf *Conf) error {
164+
if t.Boolean != nil && !*t.Boolean {
165+
return errors.New("unable to marshal")
166+
}
167+
if err := conf.Marshal(t); err != nil {
168+
return err
169+
}
170+
return conf.Merge(NewFromStringMap(map[string]interface{}{
171+
"additional": "field",
172+
}))
173+
}
174+
163175
type Struct struct {
164176
Name string
165177
}
@@ -174,6 +186,14 @@ func (tID *TestID) UnmarshalText(text []byte) error {
174186
return nil
175187
}
176188

189+
func (tID TestID) MarshalText() (text []byte, err error) {
190+
out := string(tID)
191+
if !strings.HasSuffix(out, "_") {
192+
out += "_"
193+
}
194+
return []byte(out), nil
195+
}
196+
177197
type TestIDConfig struct {
178198
Boolean bool `mapstructure:"bool"`
179199
Map map[TestID]string `mapstructure:"map"`
@@ -232,6 +252,63 @@ func TestMapKeyStringToMapKeyTextUnmarshalerHookFuncErrorUnmarshal(t *testing.T)
232252
assert.Error(t, conf.Unmarshal(cfg))
233253
}
234254

255+
func TestMarshal(t *testing.T) {
256+
conf := New()
257+
cfg := &TestIDConfig{
258+
Boolean: true,
259+
Map: map[TestID]string{
260+
"string": "this is a string",
261+
},
262+
}
263+
assert.NoError(t, conf.Marshal(cfg))
264+
assert.Equal(t, true, conf.Get("bool"))
265+
assert.Equal(t, map[string]interface{}{"string_": "this is a string"}, conf.Get("map"))
266+
}
267+
268+
func TestMarshalDuplicateID(t *testing.T) {
269+
conf := New()
270+
cfg := &TestIDConfig{
271+
Boolean: true,
272+
Map: map[TestID]string{
273+
"string": "this is a string",
274+
"string_": "this is another string",
275+
},
276+
}
277+
assert.Error(t, conf.Marshal(cfg))
278+
}
279+
280+
func TestMarshalError(t *testing.T) {
281+
conf := New()
282+
assert.Error(t, conf.Marshal(nil))
283+
}
284+
285+
func TestMarshaler(t *testing.T) {
286+
conf := New()
287+
cfg := &TestConfig{
288+
Struct: &Struct{
289+
Name: "StructName",
290+
},
291+
}
292+
assert.NoError(t, conf.Marshal(cfg))
293+
assert.Equal(t, "field", conf.Get("additional"))
294+
295+
conf = New()
296+
type NestedMarshaler struct {
297+
TestConfig *TestConfig
298+
}
299+
nmCfg := &NestedMarshaler{
300+
TestConfig: cfg,
301+
}
302+
assert.NoError(t, conf.Marshal(nmCfg))
303+
sub, err := conf.Sub("testconfig")
304+
assert.NoError(t, err)
305+
assert.True(t, sub.IsSet("additional"))
306+
assert.Equal(t, "field", sub.Get("additional"))
307+
varBool := false
308+
nmCfg.TestConfig.Boolean = &varBool
309+
assert.Error(t, conf.Marshal(nmCfg))
310+
}
311+
235312
// newConfFromFile creates a new Conf by reading the given file.
236313
func newConfFromFile(t testing.TB, fileName string) map[string]interface{} {
237314
content, err := os.ReadFile(filepath.Clean(fileName))

0 commit comments

Comments
 (0)