confgenerator/feature_tracking.go (393 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package confgenerator
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
var (
ErrTrackingInlineStruct = errors.New("cannot have tracking on inline struct")
ErrTrackingOverrideStruct = errors.New("struct that has tracking tag must not be empty")
)
type TrackingOverrideMapError struct {
FieldName string
}
func (e *TrackingOverrideMapError) Error() string {
return fmt.Sprintf("map type for a field is not supported: %s", e.FieldName)
}
type Feature struct {
// Module defines the sub-agent: metrics or logging
Module string
// Kind defines the kind: receivers or processors
Kind string
// Type from Component.Type()
Type string
// Key: set of keys that will be joined together for feature tracking metrics
Key []string
// Value defined from fields of UnifiedConfig.
Value string
}
type CustomFeature struct {
// Key: set of keys that will be joined together for feature tracking metrics
Key []string
// Value defined from fields of UnifiedConfig.
Value string
}
// CustomFeatures is the interface that components must implement to be able to
// track features not captured by the `tracking` struct tag.
type CustomFeatures interface {
// ExtractFeatures returns a list of features that will be tracked for this component.
ExtractFeatures() ([]CustomFeature, error)
// ListAllFeatures returns a list of all features that could be tracked for this component.
// This lists all the possible features that could be tracked for this component, but some of these
// features may not be tracked when not used by the component.
ListAllFeatures() []string
}
// ExtractFeatures fields that containing a tracking tag will be tracked.
// Automatic collection of bool or int fields. Any value that exists on tracking
// tag will be used instead of value from UnifiedConfig.
func ExtractFeatures(ctx context.Context, userUc, mergedUc *UnifiedConfig) ([]Feature, error) {
allFeatures := getOverriddenDefaultPipelines(userUc)
allFeatures = append(allFeatures, getSelfLogCollection(userUc))
allFeatures = append(allFeatures, getOTelLoggingSupportedConfig(ctx, mergedUc))
var err error
var tempTrackedFeatures []Feature
if userUc.HasMetrics() {
tempTrackedFeatures, err = trackedMappedComponents("metrics", "receivers", userUc.Metrics.Receivers)
if err != nil {
return nil, err
}
allFeatures = append(allFeatures, tempTrackedFeatures...)
tempTrackedFeatures, err = trackedMappedComponents("metrics", "processors", userUc.Metrics.Processors)
if err != nil {
return nil, err
}
allFeatures = append(allFeatures, tempTrackedFeatures...)
}
if userUc.HasLogging() {
tempTrackedFeatures, err = trackedMappedComponents("logging", "receivers", userUc.Logging.Receivers)
if err != nil {
return nil, err
}
allFeatures = append(allFeatures, tempTrackedFeatures...)
tempTrackedFeatures, err = trackedMappedComponents("logging", "processors", userUc.Logging.Processors)
if err != nil {
return nil, err
}
allFeatures = append(allFeatures, tempTrackedFeatures...)
}
if userUc.HasCombined() {
tempTrackedFeatures, err = trackedMappedComponents("combined", "receivers", userUc.Combined.Receivers)
if err != nil {
return nil, err
}
allFeatures = append(allFeatures, tempTrackedFeatures...)
}
return allFeatures, nil
}
func GetSortedKeys[V any](m map[string]V) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func trackedMappedComponents[C Component](module string, kind string, m map[string]C) ([]Feature, error) {
if m == nil {
return nil, nil
}
var features []Feature
for i, k := range GetSortedKeys(m) {
c := m[k]
feature := Feature{
Module: module,
Kind: kind,
Type: c.Type(),
Key: []string{fmt.Sprintf("[%d]", i)},
}
trackedFeatures, err := trackingFeatures(reflect.ValueOf(c), metadata{}, feature)
if err != nil {
return nil, err
}
features = append(features, trackedFeatures...)
}
return features, nil
}
// TODO(b/272536539): add time.Duration to auto tracking
func trackingFeatures(c reflect.Value, md metadata, feature Feature) ([]Feature, error) {
if customFeatures, ok := c.Interface().(CustomFeatures); ok {
cfs, err := customFeatures.ExtractFeatures()
if err != nil {
return nil, err
}
var features []Feature
for _, cf := range cfs {
features = append(features, Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, cf.Key...),
Value: cf.Value,
})
}
return features, nil
}
if md.isExcluded {
return nil, nil
}
t := c.Type()
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if c.IsZero() {
return nil, nil
}
v := reflect.Indirect(c)
if v.Kind() == reflect.Invalid {
return nil, nil
}
var features []Feature
switch kind := t.Kind(); {
case kind == reflect.Struct:
// If struct has tracking it must have a value
if md.hasTracking && !md.hasOverride {
return nil, ErrTrackingOverrideStruct
}
if md.yamlTag != "" {
feature.Key = append(feature.Key, md.yamlTag)
}
if md.hasTracking {
// If struct is inline there is no associated name for key generation
// By default inline structs of a tracked field are also tracked
if md.isInline {
return nil, ErrTrackingInlineStruct
} else {
// For structs that are in a Component. An extra metric is added with
// the value being the override value from the yaml tag
ftr := feature
ftr.Value = md.overrideValue
features = append(features, ftr)
}
}
// Iterate over all available fields and read the tag value
for i := 0; i < t.NumField(); i++ {
// Get the field, returns https://golang.org/pkg/reflect/#StructField
field := t.Field(i)
if !field.IsExported() {
continue
}
// Type field name is part of the ConfigComponent definition.
// All user visible component inlines that component, this field can help
// us assert that a certain component is enabled.
// Capture special metrics for enabled receiver or processor
if field.Name == "Type" {
f := feature
f.Key = append(f.Key, "enabled")
f.Value = "true"
features = append(features, f)
continue
}
f := Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append([]string{}, feature.Key...),
Value: feature.Value,
}
tf, err := trackingFeatures(v.Field(i), getMetadata(field), f)
if err != nil {
return nil, err
}
features = append(features, tf...)
}
case kind == reflect.Map:
// Create map length metric
features = append(features, Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, md.yamlTag, "__length"),
Value: fmt.Sprintf("%d", v.Len()),
})
keys := make([]string, 0)
for _, k := range v.MapKeys() {
keys = append(keys, k.String())
}
sort.Strings(keys)
for i, key := range keys {
f := Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, md.yamlTag),
}
vAtKey := v.MapIndex(reflect.ValueOf(key))
t := vAtKey.Type()
fs := make([]Feature, 0)
k := fmt.Sprintf("[%d]", i)
if md.keepKeys {
features = append(features, Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, md.yamlTag, k, "__key"),
Value: key,
})
}
mdCopy := md.deepCopy()
var err error
if t.Kind() == reflect.Struct {
f.Key = append(f.Key, k)
mdCopy.yamlTag = ""
fs, err = trackingFeatures(vAtKey, mdCopy, f)
} else {
mdCopy.yamlTag = k
fs, err = trackingFeatures(vAtKey, mdCopy, f)
}
if err != nil {
return nil, err
}
features = append(features, fs...)
}
case kind == reflect.Slice || kind == reflect.Array:
// Create array length metric
features = append(features, Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, md.yamlTag, "__length"),
Value: fmt.Sprintf("%d", v.Len()),
})
for i := 0; i < v.Len(); i++ {
f := Feature{
Module: feature.Module,
Kind: feature.Kind,
Type: feature.Type,
Key: append(feature.Key, md.yamlTag),
}
v := v.Index(i)
t := v.Type()
fs := make([]Feature, 0)
m2 := md.deepCopy()
var err error
if t.Kind() == reflect.Struct {
f.Key = append(f.Key, fmt.Sprintf("[%d]", i))
m2.yamlTag = ""
fs, err = trackingFeatures(v, m2, f)
} else {
m2.yamlTag = fmt.Sprintf("[%d]", i)
fs, err = trackingFeatures(v, m2, f)
}
if err != nil {
return nil, err
}
features = append(features, fs...)
}
default:
if skipField(v, md) {
return nil, nil
}
feature.Key = append(feature.Key, md.yamlTag)
if md.hasOverride {
feature.Value = md.overrideValue
} else {
feature.Value = fmt.Sprintf("%v", v.Interface())
}
features = append(features, feature)
}
return features, nil
}
func skipField(value reflect.Value, m metadata) bool {
if m.hasTracking {
return false
}
if m.isExcluded {
return true
}
switch value.Type().Kind() {
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int32:
return false
default:
return true
}
}
type metadata struct {
isExcluded bool
isInline bool
hasTracking bool
hasOverride bool
keepKeys bool
yamlTag string
overrideValue string
componentIndex int
}
func (m metadata) deepCopy() metadata {
return metadata{
isExcluded: m.isExcluded,
isInline: m.isInline,
hasTracking: m.hasTracking,
hasOverride: m.hasOverride,
keepKeys: m.keepKeys,
yamlTag: m.yamlTag,
overrideValue: m.overrideValue,
componentIndex: m.componentIndex,
}
}
func getMetadata(field reflect.StructField) metadata {
trackingTag, hasTracking := field.Tag.Lookup("tracking")
hasOverride := false
hasKeepKeys := false
trackingTags := strings.Split(trackingTag, ",")
if trackingTags[0] != "" {
hasOverride = true
}
if len(trackingTags) > 1 && trackingTags[1] == "keys" {
hasKeepKeys = true
}
isExcluded := trackingTag == "-"
yamlTag, ok := field.Tag.Lookup("yaml")
if !ok {
panic("field must have a yaml tag")
}
hasInline := false
yamlTags := strings.Split(yamlTag, ",")
for _, tag := range yamlTags {
if tag == "inline" {
hasInline = true
}
}
return metadata{
hasTracking: hasTracking,
hasOverride: hasOverride,
keepKeys: hasKeepKeys,
isExcluded: isExcluded,
isInline: hasInline,
overrideValue: trackingTags[0],
// The first tag is the field identifier
// See this for more details: https://pkg.go.dev/gopkg.in/yaml.v2#Unmarshal
yamlTag: yamlTags[0],
}
}
// TODO: b/399354366 - Cleanup when OTel Logging Support is fully released.
func getOTelLoggingSupportedConfig(ctx context.Context, mergedUc *UnifiedConfig) Feature {
feature := Feature{
Module: "logging",
Kind: "service",
Type: "otel_logging",
Key: []string{"otel_logging_supported_config"},
Value: "false",
}
if mergedUc.OTelLoggingSupported(ctx) {
feature.Value = "true"
}
return feature
}
func getSelfLogCollection(uc *UnifiedConfig) Feature {
feature := Feature{
Module: "global",
Kind: "default",
Type: "self_log",
Key: []string{"default_self_log_file_collection"},
Value: "true",
}
if uc.Global != nil {
feature.Value = strconv.FormatBool(uc.Global.GetDefaultSelfLogFileCollection())
}
return feature
}
func getOverriddenDefaultPipelines(uc *UnifiedConfig) []Feature {
features := []Feature{
{
Module: "logging",
Kind: "service",
Type: "pipelines",
Key: []string{"default_pipeline_overridden"},
Value: "false",
},
{
Module: "metrics",
Kind: "service",
Type: "pipelines",
Key: []string{"default_pipeline_overridden"},
Value: "false",
},
}
if uc.Logging != nil && uc.Logging.Service != nil && uc.Logging.Service.Pipelines["default_pipeline"] != nil {
features[0].Value = "true"
}
if uc.Metrics != nil && uc.Metrics.Service != nil && uc.Metrics.Service.Pipelines["default_pipeline"] != nil {
features[1].Value = "true"
}
return features
}