opentelemetry-collector/confmap/internal/conf.go

236 lines
6.9 KiB
Go

// 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)
}