diff --git a/config/config_test.go b/config/config_test.go index fcc3aaa80f..dece4a322b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -306,25 +306,25 @@ func TestReadRulesConfig(t *testing.T) { assert.NoError(t, err) switch r := d.(type) { case *RulesBasedSamplerConfig: - assert.Len(t, r.Rule, 6) + assert.Len(t, r.Rules, 6) var rule *RulesBasedSamplerRule - rule = r.Rule[0] + rule = r.Rules[0] assert.True(t, rule.Drop) assert.Equal(t, 0, rule.SampleRate) - assert.Len(t, rule.Condition, 1) + assert.Len(t, rule.Conditions, 1) - rule = r.Rule[1] + rule = r.Rules[1] assert.Equal(t, 1, rule.SampleRate) assert.Equal(t, "keep slow 500 errors", rule.Name) - assert.Len(t, rule.Condition, 2) + assert.Len(t, rule.Conditions, 2) - rule = r.Rule[4] + rule = r.Rules[4] assert.Equal(t, 5, rule.SampleRate) assert.Equal(t, "span", rule.Scope) - rule = r.Rule[5] + rule = r.Rules[5] assert.Equal(t, 10, rule.SampleRate) assert.Equal(t, "", rule.Scope) diff --git a/config/file_config.go b/config/file_config.go index 4306ebb7ed..0636d392e1 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -172,10 +172,10 @@ func newFileConfig(opts *CmdEnv) (*fileConfig, error) { // TODO: this is temporary while we still conform to the old config format; // once we're fully migrated, we can remove this stuff. - if dryRun, ok := getValueForCaseInsensitiveKey(rulesconf, "dryrun", false); ok { + if dryRun, ok := GetValueForCaseInsensitiveKey(rulesconf, "dryrun", false); ok { mainconf.DryRun = dryRun } - if dryRunFieldName, ok := getValueForCaseInsensitiveKey(rulesconf, "dryrunfieldname", ""); ok && dryRunFieldName != "" { + if dryRunFieldName, ok := GetValueForCaseInsensitiveKey(rulesconf, "dryrunfieldname", ""); ok && dryRunFieldName != "" { mainconf.DryRunFieldName = dryRunFieldName } @@ -443,11 +443,11 @@ func (f *fileConfig) GetAllSamplerRules() (map[string]any, error) { return f.rulesConfig, nil } -// getValueForCaseInsensitiveKey is a generic function that returns the value from a map[string]any +// GetValueForCaseInsensitiveKey is a generic function that returns the value from a map[string]any // for the given key, ignoring case of the key. It returns ok=true only if the key was found // and could be converted to the required type. Otherwise it returns the default value // and ok=false. -func getValueForCaseInsensitiveKey[T any](m map[string]any, key string, def T) (T, bool) { +func GetValueForCaseInsensitiveKey[T any](m map[string]any, key string, def T) (T, bool) { for k, v := range m { if strings.EqualFold(k, key) { if t, ok := v.(T); ok { @@ -473,13 +473,13 @@ func (f *fileConfig) GetSamplerConfigForDestName(destname string) (any, string, // both fail will we return not found. const notfound = "not found" - if v, ok := getValueForCaseInsensitiveKey(config, destname, map[string]any{}); ok { + if v, ok := GetValueForCaseInsensitiveKey(config, destname, map[string]any{}); ok { // we have a specific sampler, so we extract that sampler's config config = v } // now we need the name of the sampler - samplerName, _ := getValueForCaseInsensitiveKey(config, "sampler", "DeterministicSampler") + samplerName, _ := GetValueForCaseInsensitiveKey(config, "sampler", "DeterministicSampler") var i any switch samplerName { diff --git a/config/sampler_config.go b/config/sampler_config.go index e78d43645a..9207351f76 100644 --- a/config/sampler_config.go +++ b/config/sampler_config.go @@ -6,48 +6,92 @@ import ( "strings" ) +// The json tags in this file are used for conversion from the old format (see tools/convert for details). +// They are deliberately all lowercase. +// The yaml tags are used for the new format and are PascalCase. + +type V2SamplerChoice struct { + Name string `json:"name" yaml:"Name,omitempty"` + DeterministicSampler *DeterministicSamplerConfig `json:"deterministicsampler" yaml:"DeterministicSampler,omitempty"` + RulesBasedSampler *RulesBasedSamplerConfig `json:"rulesbasedsampler" yaml:"RulesBasedSampler,omitempty"` + DynamicSampler *DynamicSamplerConfig `json:"dynamicsampler" yaml:"DynamicSampler,omitempty"` + EMADynamicSampler *EMADynamicSamplerConfig `json:"emadynamicsampler" yaml:"EmaDynamicSampler,omitempty"` + TotalThroughputSampler *TotalThroughputSamplerConfig `json:"totalthroughputsampler" yaml:"TotalThroughputSampler,omitempty"` +} + +type V2SamplerConfig struct { + ConfigVersion int `json:"configversion" yaml:"ConfigVersion" validate:"required,ge=2"` + Samplers map[string]*V2SamplerChoice `json:"samplers" yaml:"Samplers,omitempty" validate:"required"` +} + type DeterministicSamplerConfig struct { - SampleRate int `default:"1" validate:"required,gte=1"` + SampleRate int `json:"samplerate" yaml:"SampleRate,omitempty" default:"1" validate:"required,gte=1"` } type DynamicSamplerConfig struct { - SampleRate int64 `validate:"required,gte=1"` - ClearFrequencySec int64 `` - FieldList []string `validate:"required"` - UseTraceLength bool `` - AddSampleRateKeyToTrace bool `` - AddSampleRateKeyToTraceField string `validate:"required_with=AddSampleRateKeyToTrace"` + SampleRate int64 `json:"samplerate" yaml:"SampleRate,omitempty" validate:"required,gte=1"` + ClearFrequencySec int64 `json:"clearfrequencysec" yaml:"ClearFrequencySec,omitempty"` + FieldList []string `json:"fieldlist" yaml:"FieldList,omitempty" validate:"required"` + UseTraceLength bool `json:"usetracelength" yaml:"UseTraceLength,omitempty"` + AddSampleRateKeyToTrace bool `json:"addsampleratekeytotrace" yaml:"AddSampleRateKeyToTrace,omitempty"` + AddSampleRateKeyToTraceField string `json:"addsampleratekeytotracefield" yaml:"AddSampleRateKeyToTraceField,omitempty" validate:"required_with=AddSampleRateKeyToTrace"` } type EMADynamicSamplerConfig struct { - GoalSampleRate int `validate:"gte=1"` - AdjustmentInterval int `` - Weight float64 `validate:"gt=0,lt=1"` - AgeOutValue float64 `` - BurstMultiple float64 `` - BurstDetectionDelay uint `` - MaxKeys int `` - FieldList []string `validate:"required"` - UseTraceLength bool `` - AddSampleRateKeyToTrace bool `` - AddSampleRateKeyToTraceField string `validate:"required_with=AddSampleRateKeyToTrace"` + GoalSampleRate int `json:"goalsamplerate" yaml:"GoalSampleRate,omitempty" validate:"gte=1"` + AdjustmentInterval int `json:"adjustmentinterval" yaml:"AdjustmentInterval,omitempty"` + Weight float64 `json:"weight" yaml:"Weight,omitempty" validate:"gt=0,lt=1"` + AgeOutValue float64 `json:"ageoutvalue" yaml:"AgeOutValue,omitempty"` + BurstMultiple float64 `json:"burstmultiple" yaml:"BurstMultiple,omitempty"` + BurstDetectionDelay uint `json:"burstdetectiondelay" yaml:"BurstDetectionDelay,omitempty"` + MaxKeys int `json:"maxkeys" yaml:"MaxKeys,omitempty"` + FieldList []string `json:"fieldlist" yaml:"FieldList,omitempty" validate:"required"` + UseTraceLength bool `json:"usetracelength" yaml:"UseTraceLength,omitempty"` + AddSampleRateKeyToTrace bool `json:"addsampleratekeytotrace" yaml:"AddSampleRateKeyToTrace,omitempty"` + AddSampleRateKeyToTraceField string `json:"addsampleratekeytotracefield" yaml:"AddSampleRateKeyToTraceField,omitempty" validate:"required_with=AddSampleRateKeyToTrace"` } type TotalThroughputSamplerConfig struct { - GoalThroughputPerSec int64 `validate:"gte=1"` - ClearFrequencySec int64 `` - FieldList []string `validate:"required"` - UseTraceLength bool `` - AddSampleRateKeyToTrace bool `` - AddSampleRateKeyToTraceField string `validate:"required_with=AddSampleRateKeyToTrace"` + GoalThroughputPerSec int64 `json:"goalthroughputpersec" yaml:"GoalThroughputPerSec,omitempty" validate:"gte=1"` + ClearFrequencySec int64 `json:"clearfrequencysec" yaml:"ClearFrequencySec,omitempty"` + FieldList []string `json:"fieldlist" yaml:"FieldList,omitempty" validate:"required"` + UseTraceLength bool `json:"usetracelength" yaml:"UseTraceLength,omitempty"` + AddSampleRateKeyToTrace bool `json:"addsampleratekeytotrace" yaml:"AddSampleRateKeyToTrace,omitempty"` + AddSampleRateKeyToTraceField string `json:"addsampleratekeytotracefield" yaml:"AddSampleRateKeyToTraceField,omitempty" validate:"required_with=AddSampleRateKeyToTrace"` +} + +type RulesBasedSamplerConfig struct { + // Rules has deliberately different names for json and yaml for conversion from old to new format + Rules []*RulesBasedSamplerRule `json:"rule" yaml:"Rules,omitempty"` + CheckNestedFields bool `json:"checknestedfields" yaml:"CheckNestedFields,omitempty"` +} + +type RulesBasedDownstreamSampler struct { + DynamicSampler *DynamicSamplerConfig `json:"dynamicsampler" yaml:"DynamicSampler,omitempty"` + EMADynamicSampler *EMADynamicSamplerConfig `json:"emadynamicsampler" yaml:"EmaDynamicSampler,omitempty"` + TotalThroughputSampler *TotalThroughputSamplerConfig `json:"totalthroughputsampler" yaml:"TotalThroughputSampler,omitempty"` +} + +type RulesBasedSamplerRule struct { + // Conditions has deliberately different names for json and yaml for conversion from old to new format + Name string `json:"name" yaml:"Name,omitempty"` + SampleRate int `json:"samplerate" yaml:"SampleRate,omitempty"` + Drop bool `json:"drop" yaml:"Drop,omitempty"` + Scope string `json:"scope" yaml:"Scope,omitempty" validate:"oneof=span trace"` + Conditions []*RulesBasedSamplerCondition `json:"condition" yaml:"Conditions,omitempty"` + Sampler *RulesBasedDownstreamSampler `json:"sampler" yaml:"Sampler,omitempty"` +} + +func (r *RulesBasedSamplerRule) String() string { + return fmt.Sprintf("%+v", *r) } type RulesBasedSamplerCondition struct { - Field string `validate:"required"` - Operator string `validate:"required"` - Value interface{} `` - Datatype string `` - Matches func(value any, exists bool) bool + Field string `json:"field" yaml:"Field" validate:"required"` + Operator string `json:"operator" yaml:"Operator" validate:"required"` + Value interface{} `json:"value" yaml:"Value" ` + Datatype string `json:"datatype" yaml:"Datatype,omitempty"` + Matches func(value any, exists bool) bool `json:"-" yaml:"-"` } func (r *RulesBasedSamplerCondition) Init() error { @@ -374,30 +418,6 @@ func setMatchStringBasedOperators(r *RulesBasedSamplerCondition, condition strin return nil } -type RulesBasedDownstreamSampler struct { - DynamicSampler *DynamicSamplerConfig - EMADynamicSampler *EMADynamicSamplerConfig - TotalThroughputSampler *TotalThroughputSamplerConfig -} - -type RulesBasedSamplerRule struct { - Name string `` - SampleRate int `` - Sampler *RulesBasedDownstreamSampler `` - Drop bool `` - Scope string `validate:"oneof=span trace"` - Condition []*RulesBasedSamplerCondition `` -} - -func (r *RulesBasedSamplerRule) String() string { - return fmt.Sprintf("%+v", *r) -} - -type RulesBasedSamplerConfig struct { - Rule []*RulesBasedSamplerRule `` - CheckNestedFields bool `` -} - func (r *RulesBasedSamplerConfig) String() string { return fmt.Sprintf("%+v", *r) } diff --git a/sample/rules.go b/sample/rules.go index 7a94661e8f..52556d3725 100644 --- a/sample/rules.go +++ b/sample/rules.go @@ -30,8 +30,8 @@ func (s *RulesBasedSampler) Start() error { s.samplers = make(map[string]Sampler) // Check if any rule has a downstream sampler and create it - for _, rule := range s.Config.Rule { - for _, cond := range rule.Condition { + for _, rule := range s.Config.Rules { + for _, cond := range rule.Conditions { if err := cond.Init(); err != nil { s.Logger.Debug().WithFields(map[string]interface{}{ "rule_name": rule.Name, @@ -73,7 +73,7 @@ func (s *RulesBasedSampler) GetSampleRate(trace *types.Trace) (rate uint, keep b "trace_id": trace.TraceID, }) - for _, rule := range s.Config.Rule { + for _, rule := range s.Config.Rules { var matched bool var reason string @@ -135,13 +135,13 @@ func (s *RulesBasedSampler) GetSampleRate(trace *types.Trace) (rate uint, keep b func ruleMatchesTrace(t *types.Trace, rule *config.RulesBasedSamplerRule, checkNestedFields bool) bool { // We treat a rule with no conditions as a match. - if rule.Condition == nil { + if rule.Conditions == nil { return true } var matched int - for _, condition := range rule.Condition { + for _, condition := range rule.Conditions { span: for _, span := range t.GetSpans() { value, exists := extractValueFromSpan(span, condition, checkNestedFields) @@ -158,18 +158,18 @@ func ruleMatchesTrace(t *types.Trace, rule *config.RulesBasedSamplerRule, checkN } } - return matched == len(rule.Condition) + return matched == len(rule.Conditions) } func ruleMatchesSpanInTrace(trace *types.Trace, rule *config.RulesBasedSamplerRule, checkNestedFields bool) bool { // We treat a rule with no conditions as a match. - if rule.Condition == nil { + if rule.Conditions == nil { return true } for _, span := range trace.GetSpans() { ruleMatched := true - for _, condition := range rule.Condition { + for _, condition := range rule.Conditions { // whether this condition is matched by this span. value, exists := extractValueFromSpan(span, condition, checkNestedFields) if condition.Matches == nil { diff --git a/sample/rules_test.go b/sample/rules_test.go index 67839ae84b..a6c8d3ea61 100644 --- a/sample/rules_test.go +++ b/sample/rules_test.go @@ -22,11 +22,11 @@ func TestRules(t *testing.T) { data := []TestRulesData{ { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "int64equals", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -50,11 +50,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "int64greaterthan", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -78,11 +78,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "int64lessthan", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "<", @@ -106,11 +106,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "int64float64lessthan", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "<", @@ -134,11 +134,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "rule that wont be hit", SampleRate: 0, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -167,11 +167,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "multiple matches", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "<=", @@ -213,11 +213,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "drop", Drop: true, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -241,7 +241,7 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "drop everything", Drop: true, @@ -262,11 +262,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "test multiple rules must all be matched", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "=", @@ -304,11 +304,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "not equal test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "!=", @@ -332,11 +332,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "exists test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "exists", @@ -359,11 +359,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "not exists test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "not-exists", @@ -386,11 +386,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "starts with test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "starts-with", @@ -414,11 +414,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "contains test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "contains", @@ -442,11 +442,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "does not contain test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "first", Operator: "does-not-contain", @@ -470,11 +470,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "YAMLintgeaterthan", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -498,11 +498,11 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "Check root span for span count", SampleRate: 1, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "meta.span_count", Operator: "=", @@ -540,12 +540,12 @@ func TestRules(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "Check root span for span count", Drop: true, SampleRate: 0, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "meta.span_count", Operator: ">=", @@ -611,7 +611,7 @@ func TestRules(t *testing.T) { assert.Equal(t, d.ExpectedRate, rate, d.Rules) name := d.ExpectedName if name == "" { - name = d.Rules.Rule[0].Name + name = d.Rules.Rules[0].Name } assert.Contains(t, reason, name) @@ -626,11 +626,11 @@ func TestRulesWithNestedFields(t *testing.T) { data := []TestRulesData{ { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "nested field", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test.test1", Operator: "=", @@ -657,11 +657,11 @@ func TestRulesWithNestedFields(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "field not nested", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test.test1", Operator: "=", @@ -686,11 +686,11 @@ func TestRulesWithNestedFields(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "not exists test", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test.test1", Operator: "not-exists", @@ -716,11 +716,11 @@ func TestRulesWithNestedFields(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "do not check nested", SampleRate: 4, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test.test1", Operator: "exists", @@ -765,7 +765,7 @@ func TestRulesWithNestedFields(t *testing.T) { assert.Equal(t, d.ExpectedRate, rate, d.Rules) name := d.ExpectedName if name == "" { - name = d.Rules.Rule[0].Name + name = d.Rules.Rules[0].Name } assert.Contains(t, reason, name) @@ -780,10 +780,10 @@ func TestRulesWithDynamicSampler(t *testing.T) { data := []TestRulesData{ { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "downstream-dynamic", - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "rule_test", Operator: "=", @@ -843,7 +843,7 @@ func TestRulesWithDynamicSampler(t *testing.T) { assert.Equal(t, d.ExpectedRate, rate, d.Rules) name := d.ExpectedName if name == "" { - name = d.Rules.Rule[0].Name + name = d.Rules.Rules[0].Name } assert.Contains(t, reason, name) @@ -868,10 +868,10 @@ func TestRulesWithEMADynamicSampler(t *testing.T) { data := []TestRulesData{ { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "downstream-dynamic", - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "rule_test", Operator: "=", @@ -931,7 +931,7 @@ func TestRulesWithEMADynamicSampler(t *testing.T) { assert.Equal(t, d.ExpectedRate, rate, d.Rules) name := d.ExpectedName if name == "" { - name = d.Rules.Rule[0].Name + name = d.Rules.Rules[0].Name } assert.Contains(t, reason, name) @@ -1012,12 +1012,12 @@ func TestRuleMatchesSpanMatchingSpan(t *testing.T) { for _, scope := range []string{"span", "trace"} { sampler := &RulesBasedSampler{ Config: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "Rule to match span", Scope: scope, SampleRate: 1, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "rule_test", Operator: "=", @@ -1065,11 +1065,11 @@ func TestRulesDatatypes(t *testing.T) { data := []TestRulesData{ { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "int64Unchanged", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1094,11 +1094,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "floatUnchanged", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1123,11 +1123,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "stringUnchanged", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1152,11 +1152,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "boolUnchanged", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1181,11 +1181,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "bool", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1210,11 +1210,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "boolShouldChangeToFalse", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "contains", @@ -1239,11 +1239,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "intToFloat", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1268,11 +1268,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "floatToInt", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "<", @@ -1297,11 +1297,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "invalidConfigComparesStringWithInt", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1326,11 +1326,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "stringToInt", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "=", @@ -1355,11 +1355,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "intToString", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -1384,11 +1384,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "floatToString", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">", @@ -1413,11 +1413,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "stringToFloat", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "<=", @@ -1442,11 +1442,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "stringToFloatInvalid", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: ">=", @@ -1471,11 +1471,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "stringNotEqual", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "!=", @@ -1500,11 +1500,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "toStringNotEqual", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "!=", @@ -1529,11 +1529,11 @@ func TestRulesDatatypes(t *testing.T) { }, { Rules: &config.RulesBasedSamplerConfig{ - Rule: []*config.RulesBasedSamplerRule{ + Rules: []*config.RulesBasedSamplerRule{ { Name: "intsNotEqual", SampleRate: 10, - Condition: []*config.RulesBasedSamplerCondition{ + Conditions: []*config.RulesBasedSamplerCondition{ { Field: "test", Operator: "!=", @@ -1559,7 +1559,7 @@ func TestRulesDatatypes(t *testing.T) { } for _, d := range data { - t.Run(d.Rules.Rule[0].Name, func(t *testing.T) { + t.Run(d.Rules.Rules[0].Name, func(t *testing.T) { sampler := &RulesBasedSampler{ Config: d.Rules, Logger: &logger.NullLogger{}, diff --git a/tools/convert/helpers.go b/tools/convert/helpers.go index 68d6697a3e..cfc40ac16c 100644 --- a/tools/convert/helpers.go +++ b/tools/convert/helpers.go @@ -221,8 +221,15 @@ func renderMap(data map[string]any, key, oldkey string, example string) string { func renderStringarray(data map[string]any, key, oldkey string, example string) string { var sa []string comment := "" - if value, ok := data[key]; ok { - sa = value.([]string) + if v, ok := data[key]; ok { + switch value := v.(type) { + case []interface{}: + for _, s := range value { + sa = append(sa, s.(string)) + } + case []string: + sa = value + } } if len(sa) == 0 { @@ -239,8 +246,15 @@ func renderStringarray(data map[string]any, key, oldkey string, example string) // secondsToDuration takes a number of seconds (if the previous value had it) and returns a string duration func secondsToDuration(data map[string]any, key, oldkey string, example string) string { + i64 := int64(0) if value, ok := _fetch(data, oldkey); ok && value != "" { - dur := time.Duration(value.(int64)) * time.Second + switch i := value.(type) { + case int64: + i64 = i + case int: + i64 = int64(i) + } + dur := time.Duration(i64) * time.Second return fmt.Sprintf("%s: %v", key, yamlf(dur)) } return fmt.Sprintf(`# %s: %v`, key, yamlf(example)) @@ -252,8 +266,13 @@ func split(s, sep string) []string { // Prints a nicely-formatted string array; if the incoming string array doesn't exist, or // exactly matches the default, then it's commented out. -func stringArray(data map[string]any, key, oldkey string, indent int, examples ...string) string { - var keys []string = examples +func stringArray(data map[string]any, key, oldkey string, indent int, examples ...any) string { + var keys []string + for _, e := range examples { + if s, ok := e.(string); ok { + keys = append(keys, s) + } + } var userdata []string comment := "# " diff --git a/tools/convert/helpers_test.go b/tools/convert/helpers_test.go index 88a1a11108..081f37cee6 100644 --- a/tools/convert/helpers_test.go +++ b/tools/convert/helpers_test.go @@ -1,6 +1,8 @@ package main -import "testing" +import ( + "testing" +) func Test_conditional(t *testing.T) { tests := []struct { diff --git a/tools/convert/main.go b/tools/convert/main.go index 937ea6d5f3..76e660c50d 100644 --- a/tools/convert/main.go +++ b/tools/convert/main.go @@ -106,16 +106,22 @@ func main() { switch args[0] { case "template": GenerateTemplate(output) + os.Exit(0) case "names": PrintNames(output) + os.Exit(0) case "sample": GenerateMinimalSample(output) + os.Exit(0) case "doc": GenerateMarkdown(output) + os.Exit(0) + case "config", "rules": + // do nothing yet because we need to parse the input file default: - fmt.Fprintf(os.Stderr, "unknown subcommand %s; valid commands are template, names, and sample\n", args[0]) + fmt.Fprintf(os.Stderr, "unknown subcommand %s; valid commands are template, names, sample, doc, config, rules\n", args[0]) + os.Exit(1) } - os.Exit(0) } rdr, err := os.Open(opts.Input) @@ -150,19 +156,27 @@ func main() { Data: data, } - tmpl := template.New("configV2.tmpl") - tmpl.Funcs(helpers()) - tmpl, err = tmpl.ParseFS(filesystem, "templates/configV2.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "template error %v\n", err) - os.Exit(1) - } + switch args[0] { + case "config": + tmpl := template.New("configV2.tmpl") + tmpl.Funcs(helpers()) + tmpl, err = tmpl.ParseFS(filesystem, "templates/configV2.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "template error %v\n", err) + os.Exit(1) + } - err = tmpl.Execute(output, tmplData) - if err != nil { - fmt.Fprintf(os.Stderr, "template error %v\n", err) - os.Exit(1) + err = tmpl.Execute(output, tmplData) + if err != nil { + fmt.Fprintf(os.Stderr, "template error %v\n", err) + os.Exit(1) + } + case "rules": + ConvertRules(data, output) + default: + fmt.Fprintf(os.Stderr, "unknown subcommand %s; valid commands are template, names, and sample\n", args[0]) } + } // All of the code below is used when building and debugging this tool. diff --git a/tools/convert/ruleconvert.go b/tools/convert/ruleconvert.go new file mode 100644 index 0000000000..d6343d3994 --- /dev/null +++ b/tools/convert/ruleconvert.go @@ -0,0 +1,135 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + + "github.com/creasty/defaults" + "github.com/honeycombio/refinery/config" + "gopkg.in/yaml.v3" +) + +func _keysToLowercase(m map[string]any) map[string]any { + newmap := make(map[string]any) + for k, v := range m { + switch val := v.(type) { + case map[string]any: + v = _keysToLowercase(val) + } + newmap[strings.ToLower(k)] = v + } + return newmap +} + +func readV1RulesIntoV2Sampler(samplerType string, rulesmap map[string]any) (*config.V2SamplerChoice, string, error) { + // construct a sampler of the appropriate type that we can treat as "any" for unmarshalling + var sampler any + switch samplerType { + case "DeterministicSampler": + sampler = &config.DeterministicSamplerConfig{} + case "DynamicSampler": + sampler = &config.DynamicSamplerConfig{} + case "EMADynamicSampler": + sampler = &config.EMADynamicSamplerConfig{} + case "RulesBasedSampler": + sampler = &config.RulesBasedSamplerConfig{} + case "TotalThroughputSampler": + sampler = &config.TotalThroughputSamplerConfig{} + default: + return nil, "not found", errors.New("no sampler found") + } + + // We use a little trick here -- we have read the rules into a generic map. + // First we convert the generic map into all lowercase keys, then marshal + // them into a bytestream using JSON, then finally unmarshal them into their + // final form. This lets us use the JSON tags to do the mapping of old field + // names onto new names, while we use the YAML tags to render the new names + // in the final output. So it's real important to have both tags on any + // field that gets renamed! + + // convert all the keys to lowercase + lowermap := _keysToLowercase(rulesmap) + + // marshal the rules into a bytestream + b, err := json.Marshal(lowermap) + if err != nil { + return nil, "", fmt.Errorf("getV1RulesForSampler unable to marshal config: %w", err) + } + + // and unmarshal them back into the sampler + err = json.Unmarshal(b, sampler) + if err != nil { + return nil, "", fmt.Errorf("getV1RulesForSampler unable to unmarshal config: %w", err) + } + // now we've got the config, apply defaults to zero values + if err := defaults.Set(sampler); err != nil { + return nil, "", fmt.Errorf("getV1RulesForSampler unable to apply defaults: %w", err) + } + + // and now put it into the V2 sampler config + newSampler := &config.V2SamplerChoice{} + switch samplerType { + case "DeterministicSampler": + newSampler.DeterministicSampler = sampler.(*config.DeterministicSamplerConfig) + case "DynamicSampler": + newSampler.DynamicSampler = sampler.(*config.DynamicSamplerConfig) + case "EMADynamicSampler": + newSampler.EMADynamicSampler = sampler.(*config.EMADynamicSamplerConfig) + case "RulesBasedSampler": + newSampler.RulesBasedSampler = sampler.(*config.RulesBasedSamplerConfig) + case "TotalThroughputSampler": + newSampler.TotalThroughputSampler = sampler.(*config.TotalThroughputSamplerConfig) + } + + return newSampler, samplerType, nil +} + +func ConvertRules(rules map[string]any, w io.Writer) { + // this writes the rules to w as a YAML file for debugging + // yaml.NewEncoder(w).Encode(rules) + + // get the sampler type for the default rule + defaultSamplerType, _ := config.GetValueForCaseInsensitiveKey(rules, "sampler", "DeterministicSampler") + + newConfig := &config.V2SamplerConfig{ + ConfigVersion: 2, + Samplers: make(map[string]*config.V2SamplerChoice), + } + sampler, _, err := readV1RulesIntoV2Sampler(defaultSamplerType, rules) + if err != nil { + panic(err) + } + + newConfig.Samplers["__default__"] = sampler + + for k, v := range rules { + // if it's not a map, skip it + if _, ok := v.(map[string]any); !ok { + continue + } + sub := v.(map[string]any) + + // make sure it's a sampler destination key by checking for the presence + // of a "sampler" key or a "samplerate" key; having either one is good + // enough + if _, ok := config.GetValueForCaseInsensitiveKey(sub, "sampler", ""); !ok { + if _, ok := config.GetValueForCaseInsensitiveKey(sub, "samplerate", ""); !ok { + continue + } + } + + // get the sampler type for the rule + samplerType, _ := config.GetValueForCaseInsensitiveKey(sub, "sampler", "DeterministicSampler") + sampler, _, err := readV1RulesIntoV2Sampler(samplerType, sub) + if err != nil { + panic(err) + } + + newConfig.Samplers[k] = sampler + } + + yaml.NewEncoder(w).Encode(newConfig) +} diff --git a/tools/convert/ruleconvert_test.go b/tools/convert/ruleconvert_test.go new file mode 100644 index 0000000000..11d1132a22 --- /dev/null +++ b/tools/convert/ruleconvert_test.go @@ -0,0 +1,26 @@ +package main + +import ( + "reflect" + "testing" +) + +func Test_keysToLowercase(t *testing.T) { + tests := []struct { + name string + m map[string]any + want map[string]any + }{ + {"empty", map[string]any{}, map[string]any{}}, + {"one", map[string]any{"A": "b"}, map[string]any{"a": "b"}}, + {"two", map[string]any{"A": "b", "C": "d"}, map[string]any{"a": "b", "c": "d"}}, + {"recursive", map[string]any{"A": "b", "C": map[string]any{"D": "e"}}, map[string]any{"a": "b", "c": map[string]any{"d": "e"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := _keysToLowercase(tt.m); !reflect.DeepEqual(got, tt.want) { + t.Errorf("_keysToLowercase() = %v, want %v", got, tt.want) + } + }) + } +}