internal/merge/confmap/merge.go (124 lines of code) (raw):

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT package confmap import ( "fmt" "reflect" "strings" "github.com/knadh/koanf/maps" ) const ( keyReceivers = "receivers" keyProcessors = "processors" keyExporters = "exporters" keyExtensions = "extensions" keyService = "service" keyPipelines = "pipelines" ) var ( // restrictedSections in the OTEL configuration that cannot have duplicate keys restrictedSections = [][]string{ {keyReceivers}, {keyProcessors}, {keyExporters}, {keyExtensions}, {keyService, keyPipelines}, } ) type mergeConflict struct { // section where conflict occurs section string // keys in the section that have conflicts keys []string } type MergeConflictError struct { conflicts []mergeConflict } func (e *MergeConflictError) Error() string { var conflictStrs []string for _, conflict := range e.conflicts { conflictStrs = append(conflictStrs, fmt.Sprintf("%s: %s", conflict.section, conflict.keys)) } return fmt.Sprintf("merge conflict in %s", strings.Join(conflictStrs, ", ")) } // mergeMaps checks for conflicts and merges the service before merging the rest of the maps. func mergeMaps(src, dest map[string]any) error { mce := &MergeConflictError{} for _, section := range restrictedSections { if mc := checkConflicts(src, dest, section); mc != nil { mce.conflicts = append(mce.conflicts, *mc) } } if len(mce.conflicts) > 0 { return mce } mergeServices(src, dest) maps.Merge(src, dest) return nil } // checkConflicts for overlapping keys in the maps at the path. func checkConflicts(src, dest map[string]any, path []string) *mergeConflict { srcMap, srcOK := getMapValue[map[string]any](src, path) destMap, destOK := getMapValue[map[string]any](dest, path) if !srcOK || !destOK { return nil } var keys []string for key := range destMap { if _, ok := srcMap[key]; ok && !reflect.DeepEqual(srcMap[key], destMap[key]) { keys = append(keys, key) } } if len(keys) > 0 { return &mergeConflict{section: strings.Join(path, KeyDelimiter), keys: keys} } return nil } // mergeServices overwrites the source service::extensions with the merged results. This is because the default // maps.Merge just sets the destination to the source for slices. func mergeServices(src, dest map[string]any) { srcMap, srcOK := getMapValue[map[string]any](src, []string{keyService}) destMap, destOK := getMapValue[map[string]any](dest, []string{keyService}) if !srcOK || !destOK { return } results := mergeSlices(srcMap[keyExtensions], destMap[keyExtensions]) if results != nil { srcMap[keyExtensions] = results } } // mergeSlices appends the deduplicated items in the destination to the source slice. func mergeSlices(src, dest any) any { if src == nil || dest == nil { return nil } srcVal := reflect.ValueOf(src) destVal := reflect.ValueOf(dest) if srcVal.Kind() != reflect.Slice || destVal.Kind() != reflect.Slice { return nil } result := reflect.MakeSlice(srcVal.Type(), 0, srcVal.Len()+destVal.Len()) for i := 0; i < srcVal.Len(); i++ { result = reflect.Append(result, srcVal.Index(i)) } for i := 0; i < destVal.Len(); i++ { item := destVal.Index(i) if !containsInSlice(result, item) { result = reflect.Append(result, item) } } return result.Interface() } func containsInSlice(slice, item reflect.Value) bool { if slice.Kind() != reflect.Slice { return false } for i := 0; i < slice.Len(); i++ { if slice.Index(i).Equal(item) { return true } } return false } // getMapValue uses maps.Search to find the value at the path and casts it. func getMapValue[T any](m map[string]any, path []string) (T, bool) { var zeroValue T found := maps.Search(m, path) if found == nil { return zeroValue, false } cast, ok := found.(T) if !ok { return zeroValue, false } return cast, true }