// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package internal // import "go.opentelemetry.io/collector/confmap/internal" import ( "errors" "fmt" "reflect" "github.com/knadh/koanf/maps" "github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/v2" encoder "go.opentelemetry.io/collector/confmap/internal/mapstructure" ) const ( // KeyDelimiter is used as the default key delimiter in the default koanf instance. KeyDelimiter = "::" ) // Conf represents the raw configuration map for the OpenTelemetry Collector. // The confmap.Conf can be unmarshalled into the Collector's config using the "service" package. type Conf struct { k *koanf.Koanf // If true, upon unmarshaling do not call the Unmarshal function on the struct // if it implements Unmarshaler and is the top-level struct. // This avoids running into an infinite recursion where Unmarshaler.Unmarshal and // Conf.Unmarshal would call each other. skipTopLevelUnmarshaler bool // isNil is true if this Conf was created from a nil field, as opposed to an empty map. // AllKeys must return an empty slice if this is true. isNil bool } // New creates a new empty confmap.Conf instance. func New() *Conf { return &Conf{k: koanf.New(KeyDelimiter), isNil: false} } // NewFromStringMap creates a confmap.Conf from a map[string]any. func NewFromStringMap(data map[string]any) *Conf { p := New() if data == nil { p.isNil = true } else { // Cannot return error because the koanf instance is empty. _ = p.k.Load(confmap.Provider(data, KeyDelimiter), nil) } return p } // Unmarshal unmarshalls the config into a struct using the given options. // Tags on the fields of the structure must be properly set. func (l *Conf) Unmarshal(result any, opts ...UnmarshalOption) error { set := UnmarshalOptions{} for _, opt := range opts { opt.apply(&set) } return Decode(l.toStringMapWithExpand(), result, set, l.skipTopLevelUnmarshaler) } // Marshal encodes the config and merges it into the Conf. func (l *Conf) Marshal(rawVal any, opts ...MarshalOption) error { set := MarshalOptions{} for _, opt := range opts { opt.apply(&set) } enc := encoder.New(EncoderConfig(rawVal, set)) data, err := enc.Encode(rawVal) if err != nil { return err } out, ok := data.(map[string]any) if !ok { return errors.New("invalid config encoding") } return l.Merge(NewFromStringMap(out)) } // AllKeys returns all keys holding a value, regardless of where they are set. // Nested keys are returned with a KeyDelimiter separator. func (l *Conf) AllKeys() []string { return l.k.Keys() } // Get can retrieve any value given the key to use. func (l *Conf) Get(key string) any { val := l.unsanitizedGet(key) return sanitizeExpanded(val, false) } // IsSet checks to see if the key has been set in any of the data locations. func (l *Conf) IsSet(key string) bool { return l.k.Exists(key) } // Merge merges the input given configuration into the existing config. // Note that the given map may be modified. func (l *Conf) Merge(in *Conf) error { if EnableMergeAppendOption.IsEnabled() { // only use MergeAppend when EnableMergeAppendOption featuregate is enabled. return l.mergeAppend(in) } l.isNil = l.isNil && in.isNil return l.k.Merge(in.k) } // Delete a path from the Conf. // If the path exists, deletes it and returns true. // If the path does not exist, does nothing and returns false. func (l *Conf) Delete(key string) bool { wasSet := l.IsSet(key) l.k.Delete(key) return wasSet } // mergeAppend merges the input given configuration into the existing config. // Note that the given map may be modified. // Additionally, mergeAppend performs deduplication when merging lists. // For example, if listA = [extension1, extension2] and listB = [extension1, extension3], // the resulting list will be [extension1, extension2, extension3]. func (l *Conf) mergeAppend(in *Conf) error { err := l.k.Load(confmap.Provider(in.ToStringMap(), ""), nil, koanf.WithMergeFunc(mergeAppend)) if err != nil { return err } l.isNil = l.isNil && in.isNil return nil } // Sub returns new Conf instance representing a sub-config of this instance. // It returns an error is the sub-config is not a map[string]any (use Get()), and an empty Map if none exists. func (l *Conf) Sub(key string) (*Conf, error) { // Code inspired by the koanf "Cut" func, but returns an error instead of empty map for unsupported sub-config type. data := l.unsanitizedGet(key) if data == nil { c := New() c.isNil = true return c, nil } switch v := data.(type) { case map[string]any: return NewFromStringMap(v), nil case ExpandedValue: if m, ok := v.Value.(map[string]any); ok { return NewFromStringMap(m), nil } else if v.Value == nil { // If the value is nil, return a new empty Conf. c := New() c.isNil = true return c, nil } // override data with the original value to make the error message more informative. data = v.Value } return nil, fmt.Errorf("unexpected sub-config value kind for key:%s value:%v kind:%v", key, data, reflect.TypeOf(data).Kind()) } func (l *Conf) toStringMapWithExpand() map[string]any { if l.isNil { return nil } m := maps.Unflatten(l.k.All(), KeyDelimiter) return m } // ToStringMap creates a map[string]any from a Conf. // Values with multiple representations // are normalized with the YAML parsed representation. // // For example, for a Conf created from `foo: ${env:FOO}` and `FOO=123` // ToStringMap will return `map[string]any{"foo": 123}`. // // For any map `m`, `NewFromStringMap(m).ToStringMap() == m`. // In particular, if the Conf was created from a nil value, // ToStringMap will return map[string]any(nil). func (l *Conf) ToStringMap() map[string]any { return sanitize(l.toStringMapWithExpand()).(map[string]any) } func (l *Conf) unsanitizedGet(key string) any { return l.k.Get(key) } // sanitize recursively removes expandedValue references from the given data. // It uses the expandedValue.Value field to replace the expandedValue references. func sanitize(a any) any { return sanitizeExpanded(a, false) } // sanitizeToStringMap recursively removes expandedValue references from the given data. // It uses the expandedValue.Original field to replace the expandedValue references. func sanitizeToStr(a any) any { return sanitizeExpanded(a, true) } func sanitizeExpanded(a any, useOriginal bool) any { switch m := a.(type) { case map[string]any: c := maps.Copy(m) for k, v := range m { c[k] = sanitizeExpanded(v, useOriginal) } return c case []any: // If the value is nil, return nil. var newSlice []any if m == nil { return newSlice } newSlice = make([]any, 0, len(m)) for _, e := range m { newSlice = append(newSlice, sanitizeExpanded(e, useOriginal)) } return newSlice case ExpandedValue: if useOriginal { return m.Original } return m.Value } return a } type UnsanitizedGetter struct { Conf *Conf } func (ug *UnsanitizedGetter) UnsanitizedGet(key string) any { return ug.Conf.unsanitizedGet(key) }