From efb9eaf5e03af8a18ac518a4633a5cee3a846eae Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Wed, 4 Mar 2026 22:45:02 -0800 Subject: [PATCH 01/35] feat: Support configuring custom span counters to aid trace insights The goal of this feature is to enrich traces leaving refinery with more information about what happened in the trace and ease querying on that information in honeycomb. Scenario: Finding endpoints with lots of database interactions Challenge: In honeycomb (and most trace providers) querying is done on span events themselves, and there's only a single layer of aggregation. You can query for a count of events where db.statement is set and group that by resolver name. You'll end up with the total number of database interactions per endpoint. Useful, but you can't take the avg or 99th% sum of database interactions and see what endpoints are having issues. I'll note that it is possible to create a calculated field to count the interactions, compute a sum of that, and group by `trace.trace_id` to see what endpoints are the worst offenders. If you export this table, you can throw it into a sheet and do a second aggregation to get the avg of those sums. That said, it's really expensive to do these querys and I've found them to be really slow. Thus the goal of these changes is to make it dramatically easier to query and monitor these values in widgets. For example: I might setup a filter for spans with `db.statement` to count the number of database interactions in a trace. Then in honeycomb I can take the avg of a new database_interactions attribute now set on root spans. --- collect/collect.go | 68 ++++++- collect/collect_test.go | 214 ++++++++++++++++++++ config/config.go | 2 + config/file_config.go | 16 +- config/mock.go | 8 + config/span_counter_config.go | 217 +++++++++++++++++++++ config/span_counter_config_test.go | 302 +++++++++++++++++++++++++++++ sample/rules.go | 156 +-------------- 8 files changed, 825 insertions(+), 158 deletions(-) create mode 100644 config/span_counter_config.go create mode 100644 config/span_counter_config_test.go diff --git a/collect/collect.go b/collect/collect.go index ea430892f1..da7f3a282d 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -113,7 +113,8 @@ type InMemCollector struct { hostname string - memMetricSample []rtmetrics.Sample // Memory monitoring using runtime/metrics + memMetricSample []rtmetrics.Sample // Memory monitoring using runtime/metrics + spanCounterConfigs []config.SpanCounterConfig } // These are the names of the metrics we use to track the number of events sent to peers through the router. @@ -171,6 +172,7 @@ func (i *InMemCollector) Start() error { i.Logger.Info().WithField("num_workers", numWorkers).Logf("Starting InMemCollector with %d workers", numWorkers) i.StressRelief.UpdateFromConfig() + i.initSpanCounterConfigs() // Set queue capacity metrics for stress relief calculations i.Metrics.Store(DENOMINATOR_INCOMING_CAP, float64(imcConfig.IncomingQueueSize)) i.Metrics.Store(DENOMINATOR_PEER_CAP, float64(imcConfig.PeerQueueSize)) @@ -240,6 +242,7 @@ func (i *InMemCollector) reloadConfigs() { i.SamplerFactory.ClearDynsamplers() i.StressRelief.UpdateFromConfig() + i.initSpanCounterConfigs() // Send reload signals to all workers to clear their local samplers // so that the new configuration will be propagated @@ -691,6 +694,60 @@ func (i *InMemCollector) addAdditionalAttributes(sp *types.Span) { } } +// initSpanCounterConfigs loads and initializes span counter configs from the current config. +// Must be called at startup and on config reload. +func (i *InMemCollector) initSpanCounterConfigs() { + cfgs := i.Config.GetSpanCounterConfig() + for j := range cfgs { + if err := cfgs[j].Init(); err != nil { + i.Logger.Error().WithField("error", err).Logf("failed to initialize span counter config entry %q", cfgs[j].Key) + } + } + i.mutex.Lock() + i.spanCounterConfigs = cfgs + i.mutex.Unlock() +} + +// computeCustomCounts computes each counter's value by iterating all spans in the trace +// and attaches the results to the root span. +// Returns nil, nil if there are no counters configured or no root span. +// +// Stress relief note: this runs inside sendTraces(), the sole consumer of the +// tracesToSend channel. Work is O(N×M) — N spans × M counters — so large +// traces with many counters slow the consumer, which deepens the outgoing +// queue. The stress relief system monitors queue depth as one of its stress +// inputs, so heavy custom-count configurations can raise the measured stress +// level and trigger earlier activation of stress relief. Additionally, spans +// processed via ProcessSpanImmediately (the stress-relief fast path) bypass the +// trace buffer entirely and never reach sendTraces, so custom counts are not +// computed or attached to stress-sampled traces. +func (i *InMemCollector) computeCustomCounts(t sendableTrace) (*types.Span, map[string]int64) { + i.mutex.RLock() + counters := i.spanCounterConfigs + i.mutex.RUnlock() + + if len(counters) == 0 { + return nil, nil + } + + targetSpan := t.RootSpan + if targetSpan == nil { + return nil, nil + } + + var rootData config.SpanData = &targetSpan.Data + counts := make(map[string]int64, len(counters)) + for _, sp := range t.GetSpans() { + for _, counter := range counters { + if counter.MatchesSpan(&sp.Data, rootData) { + counts[counter.Key]++ + } + } + } + + return targetSpan, counts +} + func (i *InMemCollector) sendTraces() { defer i.sendTracesWG.Done() @@ -698,6 +755,8 @@ func (i *InMemCollector) sendTraces() { i.Metrics.Histogram("collector_outgoing_queue", float64(len(i.tracesToSend))) _, span := otelutil.StartSpanMulti(context.Background(), i.Tracer, "sendTrace", map[string]interface{}{"num_spans": t.DescendantCount(), "tracesToSend_size": len(i.tracesToSend)}) + customCountTarget, customCounts := i.computeCustomCounts(t) + for _, sp := range t.GetSpans() { if i.Config.GetAddRuleReasonToTrace() { @@ -721,6 +780,13 @@ func (i *InMemCollector) sendTraces() { } } + // set custom span counts on the target span (root if present, else best fallback) + if sp == customCountTarget { + for k, v := range customCounts { + sp.Data.Set(k, v) + } + } + isDryRun := i.Config.GetIsDryRun() if isDryRun { sp.Data.Set(config.DryRunFieldName, t.shouldSend) diff --git a/collect/collect_test.go b/collect/collect_test.go index ffa97cab8b..fcc950a189 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -1901,6 +1901,220 @@ func TestWorkerHealthReporting(t *testing.T) { }, 2*time.Second, 50*time.Millisecond, "InMemCollector should be healthy again after worker resumes") } +// customCountConf returns a base MockConfig suitable for custom span count tests. +func customCountConf(counters []config.SpanCounterConfig) *config.MockConfig { + return &config.MockConfig{ + GetTracesConfigVal: config.TracesConfig{ + SendTicker: config.Duration(2 * time.Millisecond), + SendDelay: config.Duration(1 * time.Millisecond), + TraceTimeout: config.Duration(60 * time.Second), + MaxBatchSize: 500, + }, + SampleCache: config.SampleCacheConfig{ + KeptSize: 100, + DroppedSize: 100, + SizeCheckInterval: config.Duration(1 * time.Second), + }, + GetSamplerTypeVal: &config.DeterministicSamplerConfig{SampleRate: 1}, + TraceIdFieldNames: []string{"trace.trace_id", "traceId"}, + ParentIdFieldNames: []string{"trace.parent_id", "parentId"}, + GetCollectionConfigVal: config.CollectionConfig{ + WorkerCount: 2, + ShutdownDelay: config.Duration(1 * time.Millisecond), + IncomingQueueSize: 10, + PeerQueueSize: 10, + }, + SpanCounterConfigs: counters, + } +} + +// TestCustomSpanCounts_NoCounters verifies that when no counters are configured +// no custom fields are added to any span. +func TestCustomSpanCounts_NoCounters(t *testing.T) { + coll := newTestCollector(t, customCountConf(nil)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "no-counters" + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x"}), + APIKey: legacyAPIKey, + }, + }) + coll.AddSpan(&types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{Dataset: "test", Data: types.NewPayload(coll.Config, nil), APIKey: legacyAPIKey}, + }) + + events := transmission.GetBlock(2) + for _, ev := range events { + assert.Nil(t, ev.Data.Get("my.count"), "no custom count fields should be set when no counters are configured") + } +} + +// TestCustomSpanCounts_CountsLandOnRoot verifies that a counter with no +// conditions counts all spans and attaches the result to the root span only. +func TestCustomSpanCounts_CountsLandOnRoot(t *testing.T) { + counters := []config.SpanCounterConfig{ + {Key: "all_spans"}, + } + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "root-target" + for i := 0; i < 3; i++ { + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x"}), + APIKey: legacyAPIKey, + }, + }) + } + coll.AddSpan(&types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{Dataset: "test", Data: types.NewPayload(coll.Config, nil), APIKey: legacyAPIKey}, + }) + + events := transmission.GetBlock(4) + require.Equal(t, 4, len(events)) + + var rootEvent *types.Event + var childEvents []*types.Event + for _, ev := range events { + if ev.Data.Get("trace.parent_id") == nil { + rootEvent = ev + } else { + childEvents = append(childEvents, ev) + } + } + + require.NotNil(t, rootEvent) + // all 4 spans counted (3 children + root) + assert.Equal(t, int64(4), rootEvent.Data.Get("all_spans")) + for _, child := range childEvents { + assert.Nil(t, child.Data.Get("all_spans"), "custom count should not be set on child spans") + } +} + +// TestCustomSpanCounts_ConditionalCounting verifies that only spans matching +// a condition are counted. +func TestCustomSpanCounts_ConditionalCounting(t *testing.T) { + counters := []config.SpanCounterConfig{ + { + Key: "error_spans", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "error", Operator: config.EQ, Value: true}, + }, + }, + } + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "conditional" + // 2 error spans + for i := 0; i < 2; i++ { + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x", "error": true}), + APIKey: legacyAPIKey, + }, + }) + } + // 2 non-error spans + for i := 0; i < 2; i++ { + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x"}), + APIKey: legacyAPIKey, + }, + }) + } + coll.AddSpan(&types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{Dataset: "test", Data: types.NewPayload(coll.Config, nil), APIKey: legacyAPIKey}, + }) + + events := transmission.GetBlock(5) + require.Equal(t, 5, len(events)) + + var rootEvent *types.Event + for _, ev := range events { + if ev.Data.Get("trace.parent_id") == nil { + rootEvent = ev + } + } + require.NotNil(t, rootEvent) + assert.Equal(t, int64(2), rootEvent.Data.Get("error_spans")) +} + +// TestCustomSpanCounts_MultipleCounters verifies that multiple counters with +// different conditions produce independent counts on the root span. +func TestCustomSpanCounts_MultipleCounters(t *testing.T) { + counters := []config.SpanCounterConfig{ + { + Key: "db_spans", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "db.system", Operator: config.Exists}, + }, + }, + { + Key: "error_spans", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "error", Operator: config.EQ, Value: true}, + }, + }, + } + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "multi-counter" + spans := []map[string]interface{}{ + {"trace.parent_id": "x", "db.system": "postgresql"}, + {"trace.parent_id": "x", "db.system": "postgresql", "error": true}, + {"trace.parent_id": "x", "error": true}, + {"trace.parent_id": "x"}, + } + for _, data := range spans { + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, data), + APIKey: legacyAPIKey, + }, + }) + } + coll.AddSpan(&types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{Dataset: "test", Data: types.NewPayload(coll.Config, nil), APIKey: legacyAPIKey}, + }) + + events := transmission.GetBlock(5) + require.Equal(t, 5, len(events)) + + var rootEvent *types.Event + for _, ev := range events { + if ev.Data.Get("trace.parent_id") == nil { + rootEvent = ev + } + } + require.NotNil(t, rootEvent) + assert.Equal(t, int64(2), rootEvent.Data.Get("db_spans"), "2 spans have db.system") + assert.Equal(t, int64(2), rootEvent.Data.Get("error_spans"), "2 spans have error=true") +} + // BenchmarkCollectorWithSamplers runs benchmarks for different sampler configurations. // This is a tricky benchmark to interpret because just setting up the input data // can easily be more expensive than the collector's routing code. The goal is to diff --git a/config/config.go b/config/config.go index 224fe07d76..cbbd211265 100644 --- a/config/config.go +++ b/config/config.go @@ -151,6 +151,8 @@ type Config interface { GetAddCountsToRoot() bool + GetSpanCounterConfig() []SpanCounterConfig + GetConfigMetadata() []ConfigMetadata GetSampleCacheConfig() SampleCacheConfig diff --git a/config/file_config.go b/config/file_config.go index 43206dae90..dbcf87a7b7 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -189,10 +189,11 @@ func (dt *DefaultTrue) UnmarshalText(text []byte) error { } type RefineryTelemetryConfig struct { - AddRuleReasonToTrace bool `yaml:"AddRuleReasonToTrace"` - AddSpanCountToRoot *DefaultTrue `yaml:"AddSpanCountToRoot" default:"true"` // Avoid pointer woe on access, use GetAddSpanCountToRoot() instead. - AddCountsToRoot bool `yaml:"AddCountsToRoot"` - AddHostMetadataToTrace *DefaultTrue `yaml:"AddHostMetadataToTrace" default:"true"` // Avoid pointer woe on access, use GetAddHostMetadataToTrace() instead. + AddRuleReasonToTrace bool `yaml:"AddRuleReasonToTrace"` + AddSpanCountToRoot *DefaultTrue `yaml:"AddSpanCountToRoot" default:"true"` // Avoid pointer woe on access, use GetAddSpanCountToRoot() instead. + AddCountsToRoot bool `yaml:"AddCountsToRoot"` + AddHostMetadataToTrace *DefaultTrue `yaml:"AddHostMetadataToTrace" default:"true"` // Avoid pointer woe on access, use GetAddHostMetadataToTrace() instead. + CustomSpanCounts []SpanCounterConfig `yaml:"CustomSpanCounts,omitempty"` } type TracesConfig struct { @@ -1116,6 +1117,13 @@ func (f *fileConfig) GetAddCountsToRoot() bool { return f.mainConfig.Telemetry.AddCountsToRoot } +func (f *fileConfig) GetSpanCounterConfig() []SpanCounterConfig { + f.mux.RLock() + defer f.mux.RUnlock() + + return f.mainConfig.Telemetry.CustomSpanCounts +} + func (f *fileConfig) GetSampleCacheConfig() SampleCacheConfig { f.mux.RLock() defer f.mux.RUnlock() diff --git a/config/mock.go b/config/mock.go index 785197a795..57eef3af73 100644 --- a/config/mock.go +++ b/config/mock.go @@ -52,6 +52,7 @@ type MockConfig struct { AdditionalErrorFields []string AddSpanCountToRoot bool AddCountsToRoot bool + SpanCounterConfigs []SpanCounterConfig CacheOverrunStrategy string SampleCache SampleCacheConfig StressRelief StressReliefConfig @@ -415,6 +416,13 @@ func (f *MockConfig) GetAddCountsToRoot() bool { return f.AddSpanCountToRoot } +func (f *MockConfig) GetSpanCounterConfig() []SpanCounterConfig { + f.Mux.RLock() + defer f.Mux.RUnlock() + + return f.SpanCounterConfigs +} + func (f *MockConfig) GetSampleCacheConfig() SampleCacheConfig { f.Mux.RLock() defer f.Mux.RUnlock() diff --git a/config/span_counter_config.go b/config/span_counter_config.go new file mode 100644 index 0000000000..6985e9203d --- /dev/null +++ b/config/span_counter_config.go @@ -0,0 +1,217 @@ +package config + +import "strings" + +// SpanData is the interface required for matching span fields in a SpanCounterConfig. +// It is satisfied by *types.Payload. +type SpanData interface { + Get(key string) any + Exists(key string) bool +} + +// SpanCounterConfig defines a custom span count to be computed and added to +// the root span under Key. Spans are counted if they satisfy all Conditions. +type SpanCounterConfig struct { + Key string `yaml:"Key"` + Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` +} + +// Init initializes all conditions. Must be called before MatchesSpan. +func (c *SpanCounterConfig) Init() error { + for _, cond := range c.Conditions { + if err := cond.Init(); err != nil { + return err + } + } + return nil +} + +// MatchesSpan returns true if the span satisfies all conditions. +// span is the span being tested; root is the root span's data (may be nil). +func (c *SpanCounterConfig) MatchesSpan(span SpanData, root SpanData) bool { + for _, cond := range c.Conditions { + var value any + var exists bool + for _, field := range cond.Fields { + if strings.HasPrefix(field, RootPrefix) { + if root != nil { + f := field[len(RootPrefix):] + if root.Exists(f) { + value = root.Get(f) + exists = true + break + } + } + } else { + if span.Exists(field) { + value = span.Get(field) + exists = true + break + } + } + } + + if cond.Matches != nil { + if !cond.Matches(value, exists) { + return false + } + } else { + if !ConditionMatchesValue(cond, value, exists) { + return false + } + } + } + return true +} + +// ConditionMatchesValue evaluates a condition against a value when the +// condition's Matches function has not been set (i.e. Datatype is unspecified). +// This is exported so that sample/rules.go can share the implementation. +func ConditionMatchesValue(condition *RulesBasedSamplerCondition, value interface{}, exists bool) bool { + var match bool + switch exists { + case true: + switch condition.Operator { + case Exists: + match = exists + case NEQ: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison != equal + } + case EQ: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison == equal + } + case GT: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison == more + } + case GTE: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison == more || comparison == equal + } + case LT: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison == less + } + case LTE: + if comparison, ok := compareValues(value, condition.Value); ok { + match = comparison == less || comparison == equal + } + } + case false: + switch condition.Operator { + case NotExists: + match = !exists + } + } + return match +} + +const ( + less = -1 + equal = 0 + more = 1 +) + +// compareValues compares two values of potentially mixed numeric types. +// a is the span field value (float64, int64, bool, or string). +// b is the condition value (float64, int64, int, bool, or string). +func compareValues(a, b interface{}) (int, bool) { + if a == nil { + if b == nil { + return equal, true + } + return less, true + } + + if b == nil { + return more, true + } + + switch at := a.(type) { + case int64: + switch bt := b.(type) { + case int: + i := int(at) + switch { + case i < bt: + return less, true + case i > bt: + return more, true + default: + return equal, true + } + case int64: + switch { + case at < bt: + return less, true + case at > bt: + return more, true + default: + return equal, true + } + case float64: + f := float64(at) + switch { + case f < bt: + return less, true + case f > bt: + return more, true + default: + return equal, true + } + } + case float64: + switch bt := b.(type) { + case int: + f := float64(bt) + switch { + case at < f: + return less, true + case at > f: + return more, true + default: + return equal, true + } + case int64: + f := float64(bt) + switch { + case at < f: + return less, true + case at > f: + return more, true + default: + return equal, true + } + case float64: + switch { + case at < bt: + return less, true + case at > bt: + return more, true + default: + return equal, true + } + } + case bool: + switch bt := b.(type) { + case bool: + switch { + case !at && bt: + return less, true + case at && !bt: + return more, true + default: + return equal, true + } + } + case string: + switch bt := b.(type) { + case string: + return strings.Compare(at, bt), true + } + } + + return equal, false +} diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go new file mode 100644 index 0000000000..366a95803b --- /dev/null +++ b/config/span_counter_config_test.go @@ -0,0 +1,302 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// spanData is a simple map-backed implementation of SpanData for tests. +type spanData map[string]any + +func (s spanData) Get(key string) any { return s[key] } +func (s spanData) Exists(key string) bool { _, ok := s[key]; return ok } + +// cond builds an initialized RulesBasedSamplerCondition from a field name, +// operator, and optional value. It calls Init() so that the Matches function +// is set when Datatype is empty (the ConditionMatchesValue path). +func cond(field, operator string, value any) *RulesBasedSamplerCondition { + c := &RulesBasedSamplerCondition{ + Field: field, + Operator: operator, + Value: value, + } + if err := c.Init(); err != nil { + panic("cond Init: " + err.Error()) + } + return c +} + +// condTyped builds an initialized condition with an explicit Datatype, which +// causes Init to set a type-coercing Matches function instead of falling +// through to ConditionMatchesValue. +func condTyped(field, operator string, value any, datatype string) *RulesBasedSamplerCondition { + c := &RulesBasedSamplerCondition{ + Field: field, + Operator: operator, + Value: value, + Datatype: datatype, + } + if err := c.Init(); err != nil { + panic("condTyped Init: " + err.Error()) + } + return c +} + +// ---------------------------------------------------------------------------- +// compareValues +// ---------------------------------------------------------------------------- + +func TestCompareValues(t *testing.T) { + tests := []struct { + name string + a, b any + want int + wantOK bool + }{ + // nil handling + {"nil==nil", nil, nil, equal, true}, + {"nilnil", int64(1), nil, more, true}, + + // int64 vs int64 + {"i64 less", int64(1), int64(2), less, true}, + {"i64 equal", int64(3), int64(3), equal, true}, + {"i64 more", int64(5), int64(4), more, true}, + + // int64 vs int + {"i64 vs int less", int64(1), int(2), less, true}, + {"i64 vs int equal", int64(3), int(3), equal, true}, + {"i64 vs int more", int64(5), int(4), more, true}, + + // int64 vs float64 + {"i64 vs f64 less", int64(1), float64(1.5), less, true}, + {"i64 vs f64 equal", int64(2), float64(2.0), equal, true}, + {"i64 vs f64 more", int64(3), float64(2.9), more, true}, + + // float64 vs float64 + {"f64 less", float64(1.1), float64(1.2), less, true}, + {"f64 equal", float64(2.5), float64(2.5), equal, true}, + {"f64 more", float64(3.0), float64(2.0), more, true}, + + // float64 vs int + {"f64 vs int less", float64(0.5), int(1), less, true}, + {"f64 vs int equal", float64(2.0), int(2), equal, true}, + {"f64 vs int more", float64(2.1), int(2), more, true}, + + // float64 vs int64 + {"f64 vs i64 less", float64(0.5), int64(1), less, true}, + {"f64 vs i64 equal", float64(2.0), int64(2), equal, true}, + {"f64 vs i64 more", float64(3.0), int64(2), more, true}, + + // bool + {"bool falsefalse", true, false, more, true}, + {"bool equal", true, true, equal, true}, + + // string + {"str less", "apple", "banana", less, true}, + {"str equal", "foo", "foo", equal, true}, + {"str more", "zoo", "ant", more, true}, + + // type mismatch → ok=false + {"mismatch int64 str", int64(1), "1", equal, false}, + {"mismatch f64 str", float64(1.0), "1.0", equal, false}, + {"mismatch bool str", true, "true", equal, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, ok := compareValues(tc.a, tc.b) + assert.Equal(t, tc.wantOK, ok, "ok") + if tc.wantOK { + assert.Equal(t, tc.want, got, "comparison result") + } + }) + } +} + +// ---------------------------------------------------------------------------- +// ConditionMatchesValue +// ---------------------------------------------------------------------------- + +func TestConditionMatchesValue(t *testing.T) { + tests := []struct { + name string + operator string + condVal any + spanVal any + exists bool + want bool + }{ + // Exists / NotExists + {"exists true", Exists, nil, "anything", true, true}, + {"exists false", Exists, nil, nil, false, false}, + {"not-exists true", NotExists, nil, nil, false, true}, + {"not-exists false", NotExists, nil, "x", true, false}, + + // EQ + {"eq string match", EQ, "foo", "foo", true, true}, + {"eq string no-match", EQ, "foo", "bar", true, false}, + {"eq int64 match", EQ, int64(42), int64(42), true, true}, + {"eq int64 no-match", EQ, int64(42), int64(0), true, false}, + {"eq type mismatch", EQ, "1", int64(1), true, false}, // compareValues returns ok=false → no match + + // NEQ + {"neq match", NEQ, "foo", "bar", true, true}, + {"neq no-match", NEQ, "foo", "foo", true, false}, + + // GT / GTE / LT / LTE + {"gt true", GT, int64(1), int64(2), true, true}, + {"gt false eq", GT, int64(1), int64(1), true, false}, + {"gte equal", GTE, int64(1), int64(1), true, true}, + {"gte more", GTE, int64(1), int64(2), true, true}, + {"gte less", GTE, int64(2), int64(1), true, false}, + {"lt true", LT, int64(2), int64(1), true, true}, + {"lt false", LT, int64(1), int64(2), true, false}, + {"lte equal", LTE, int64(2), int64(2), true, true}, + {"lte less", LTE, int64(3), int64(2), true, true}, + {"lte more", LTE, int64(1), int64(2), true, false}, + + // field does not exist with non-NotExists operator → no match + {"eq field missing", EQ, "foo", nil, false, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c := &RulesBasedSamplerCondition{ + Operator: tc.operator, + Value: tc.condVal, + } + got := ConditionMatchesValue(c, tc.spanVal, tc.exists) + assert.Equal(t, tc.want, got) + }) + } +} + +// ---------------------------------------------------------------------------- +// SpanCounterConfig.MatchesSpan +// ---------------------------------------------------------------------------- + +func TestMatchesSpan_NoConditions(t *testing.T) { + // A counter with no conditions matches every span. + counter := SpanCounterConfig{Key: "all"} + assert.True(t, counter.MatchesSpan(spanData{"foo": "bar"}, nil)) + assert.True(t, counter.MatchesSpan(spanData{}, nil)) +} + +func TestMatchesSpan_SingleCondition(t *testing.T) { + counter := SpanCounterConfig{ + Key: "errors", + Conditions: []*RulesBasedSamplerCondition{cond("error", EQ, true)}, + } + + assert.True(t, counter.MatchesSpan(spanData{"error": true}, nil)) + assert.False(t, counter.MatchesSpan(spanData{"error": false}, nil)) + assert.False(t, counter.MatchesSpan(spanData{}, nil)) +} + +func TestMatchesSpan_MultipleConditionsAllMustMatch(t *testing.T) { + counter := SpanCounterConfig{ + Key: "slow-errors", + Conditions: []*RulesBasedSamplerCondition{ + cond("error", EQ, true), + cond("duration_ms", GT, int64(500)), + }, + } + + assert.True(t, counter.MatchesSpan(spanData{"error": true, "duration_ms": int64(1000)}, nil)) + assert.False(t, counter.MatchesSpan(spanData{"error": true, "duration_ms": int64(100)}, nil)) + assert.False(t, counter.MatchesSpan(spanData{"error": false, "duration_ms": int64(1000)}, nil)) + assert.False(t, counter.MatchesSpan(spanData{}, nil)) +} + +func TestMatchesSpan_RootPrefixedField(t *testing.T) { + // "root.service.name" reads from the root span data, not the span itself. + counter := SpanCounterConfig{ + Key: "svc-db", + Conditions: []*RulesBasedSamplerCondition{cond("root.service.name", EQ, "database")}, + } + + root := spanData{"service.name": "database"} + span := spanData{"duration_ms": int64(5)} + + assert.True(t, counter.MatchesSpan(span, root)) + assert.False(t, counter.MatchesSpan(span, spanData{"service.name": "api"})) +} + +func TestMatchesSpan_RootPrefixedField_NilRoot(t *testing.T) { + // When root is nil a root-prefixed field is never found → field is absent. + counter := SpanCounterConfig{ + Key: "svc", + Conditions: []*RulesBasedSamplerCondition{cond("root.service.name", EQ, "database")}, + } + assert.False(t, counter.MatchesSpan(spanData{}, nil)) +} + +func TestMatchesSpan_MultiFieldFallback(t *testing.T) { + // When multiple fields are listed, the first one found is used. + c := &RulesBasedSamplerCondition{ + Fields: []string{"trace.trace_id", "traceId"}, + Operator: Exists, + } + if err := c.Init(); err != nil { + t.Fatal(err) + } + counter := SpanCounterConfig{Key: "has-trace", Conditions: []*RulesBasedSamplerCondition{c}} + + assert.True(t, counter.MatchesSpan(spanData{"trace.trace_id": "abc"}, nil)) + assert.True(t, counter.MatchesSpan(spanData{"traceId": "abc"}, nil)) + assert.False(t, counter.MatchesSpan(spanData{}, nil)) +} + +func TestMatchesSpan_MultiFieldFallback_FirstWins(t *testing.T) { + // If the first field exists but evaluates to a non-match, the second field + // is not consulted — only the first found field is used. + c := &RulesBasedSamplerCondition{ + Fields: []string{"a", "b"}, + Operator: EQ, + Value: "yes", + } + if err := c.Init(); err != nil { + t.Fatal(err) + } + counter := SpanCounterConfig{Key: "k", Conditions: []*RulesBasedSamplerCondition{c}} + + // "a" is found with wrong value; "b" has the right value but is not checked. + assert.False(t, counter.MatchesSpan(spanData{"a": "no", "b": "yes"}, nil)) + // Only "b" exists → fallback to "b" → match. + assert.True(t, counter.MatchesSpan(spanData{"b": "yes"}, nil)) +} + +func TestMatchesSpan_TypedCondition(t *testing.T) { + // When Datatype is set, Init wires up a type-coercing Matches function. + // Verify that MatchesSpan delegates to it correctly. + counter := SpanCounterConfig{ + Key: "count-int", + Conditions: []*RulesBasedSamplerCondition{condTyped("code", EQ, 200, "int")}, + } + + // span value arrives as string "200"; the typed matcher coerces it. + assert.True(t, counter.MatchesSpan(spanData{"code": "200"}, nil)) + assert.False(t, counter.MatchesSpan(spanData{"code": "404"}, nil)) +} + +func TestMatchesSpan_ExistsAndNotExists(t *testing.T) { + exists := SpanCounterConfig{ + Key: "has-field", + Conditions: []*RulesBasedSamplerCondition{cond("db.query", Exists, nil)}, + } + notExists := SpanCounterConfig{ + Key: "no-field", + Conditions: []*RulesBasedSamplerCondition{cond("db.query", NotExists, nil)}, + } + + withField := spanData{"db.query": "SELECT 1"} + without := spanData{} + + assert.True(t, exists.MatchesSpan(withField, nil)) + assert.False(t, exists.MatchesSpan(without, nil)) + assert.False(t, notExists.MatchesSpan(withField, nil)) + assert.True(t, notExists.MatchesSpan(without, nil)) +} diff --git a/sample/rules.go b/sample/rules.go index 3f907f3ad1..cf25d547dd 100644 --- a/sample/rules.go +++ b/sample/rules.go @@ -308,158 +308,8 @@ func extractValueFromSpan( return nil, false, false } -// This only gets called when we're using one of the basic operators, and -// there is no datatype specified (meaning that the Matches function has not -// been set). In this case, we need to do some type conversion and comparison -// to determine whether the condition matches the value. +// conditionMatchesValue delegates to config.ConditionMatchesValue. +// It is called when condition.Matches is nil (Datatype was not specified). func conditionMatchesValue(condition *config.RulesBasedSamplerCondition, value interface{}, exists bool) bool { - var match bool - switch exists { - case true: - switch condition.Operator { - case config.Exists: - match = exists - case config.NEQ: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison != equal - } - case config.EQ: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison == equal - } - case config.GT: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison == more - } - case config.GTE: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison == more || comparison == equal - } - case config.LT: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison == less - } - case config.LTE: - if comparison, ok := compare(value, condition.Value); ok { - match = comparison == less || comparison == equal - } - } - case false: - switch condition.Operator { - case config.NotExists: - match = !exists - } - } - return match -} - -const ( - less = -1 - equal = 0 - more = 1 -) - -func compare(a, b interface{}) (int, bool) { - // a is the tracing data field value. This can be: float64, int64, bool, or string - // b is the Rule condition value. This can be: float64, int64, int, bool, or string - // Note: in YAML config parsing, the Value may be returned as int - // When comparing numeric values, we need to check across the 3 types: float64, int64, and int - - if a == nil { - if b == nil { - return equal, true - } - - return less, true - } - - if b == nil { - return more, true - } - - switch at := a.(type) { - case int64: - switch bt := b.(type) { - case int: - i := int(at) - switch { - case i < bt: - return less, true - case i > bt: - return more, true - default: - return equal, true - } - case int64: - switch { - case at < bt: - return less, true - case at > bt: - return more, true - default: - return equal, true - } - case float64: - f := float64(at) - switch { - case f < bt: - return less, true - case f > bt: - return more, true - default: - return equal, true - } - } - case float64: - switch bt := b.(type) { - case int: - f := float64(bt) - switch { - case at < f: - return less, true - case at > f: - return more, true - default: - return equal, true - } - case int64: - f := float64(bt) - switch { - case at < f: - return less, true - case at > f: - return more, true - default: - return equal, true - } - case float64: - switch { - case at < bt: - return less, true - case at > bt: - return more, true - default: - return equal, true - } - } - case bool: - switch bt := b.(type) { - case bool: - switch { - case !at && bt: - return less, true - case at && !bt: - return more, true - default: - return equal, true - } - } - case string: - switch bt := b.(type) { - case string: - return strings.Compare(at, bt), true - } - } - - return equal, false + return config.ConditionMatchesValue(condition, value, exists) } From d46a60fc2da4d968994c01fc8eaac8ac16c65228 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 9 Mar 2026 13:59:12 -0700 Subject: [PATCH 02/35] update docker builder to support khan gcp --- build-docker.sh | 117 ++++++++++++++---------------------------------- 1 file changed, 34 insertions(+), 83 deletions(-) diff --git a/build-docker.sh b/build-docker.sh index e47c388573..8a5cfb2631 100755 --- a/build-docker.sh +++ b/build-docker.sh @@ -3,101 +3,52 @@ set -o nounset set -o pipefail set -o xtrace -### Versioning and image tagging ### -# -# Three build scenarios: -# 1. CI release build: triggered by git tag -# - Stable (vX.Y.Z): tagged with major, minor, patch, and "latest" -# - Pre-release (vX.Y.Z-suffix): tagged only with exact version -# 2. CI branch build: version + CI job ID, tagged with branch name (+ "latest" if main) -# 3. Local build: version from git describe, tagged with that version - -# Get version info from git (used by branch and local builds) -# --tags: use any tag, not just annotated ones -# --match='v[0-9]*': only version tags (starts with v and a digit) -# --always: fall back to commit ID if no tag found -# e.g., v2.1.1-45-ga1b2c3d means commit a1b2c3d, 45 commits ahead of tag v2.1.1 -VERSION_FROM_GIT=$(git describe --tags --match='v[0-9]*' --always) - -if [[ -n "${CIRCLE_TAG:-}" ]]; then - # Release build (triggered by git tag) - VERSION=${CIRCLE_TAG#"v"} - - if [[ "${CIRCLE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - # Stable release: tag with major, minor, patch, and latest - # e.g., v2.1.1 -> "2", "2.1", "2.1.1", "latest" - MAJOR_VERSION=${VERSION%%.*} - MINOR_VERSION=${VERSION%.*} - TAGS="$MAJOR_VERSION,$MINOR_VERSION,$VERSION,latest" - else - # Pre-release: only the exact version tag - # e.g., v3.0.0-rc1 -> "3.0.0-rc1" - TAGS="$VERSION" - fi - -elif [[ -n "${CIRCLE_BRANCH:-}" ]]; then - # CI branch build - # Version from git describe + CI job ID - # e.g., 2.1.1-45-ga1b2c3d-ci8675309 - VERSION="${VERSION_FROM_GIT#'v'}-ci${CIRCLE_BUILD_NUM}" - BRANCH_TAG=${CIRCLE_BRANCH//\//-} - TAGS="${VERSION},branch-${BRANCH_TAG}" - - # Main branch builds are tagged "latest" in the private registry - if [[ "${CIRCLE_BRANCH}" == "main" ]]; then - TAGS+=",latest" - fi - -else - # Local build - # Version from git describe only - # e.g., 2.1.1-45-ga1b2c3d - VERSION=${VERSION_FROM_GIT#'v'} - TAGS="${VERSION}" -fi - -GIT_COMMIT=${CIRCLE_SHA1:-$(git rev-parse HEAD)} +GCLOUD_REGISTRY="us-central1-docker.pkg.dev/khan-internal-services/refinery" + +# Parse flags +PUSH=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --push) + PUSH=true + shift + ;; + *) + echo "Usage: $0 [--push]" + echo " --push Build and push to ${GCLOUD_REGISTRY}/refinery" + echo " (default) Build locally only" + exit 1 + ;; + esac +done + +VERSION=$(git describe --tags --match='v[0-9]*' --always) +VERSION=${VERSION#v} +GIT_COMMIT=$(git rev-parse HEAD) unset GOOS unset GOARCH export GOFLAGS="-ldflags=-X=main.BuildID=$VERSION" export SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH:-$(make latest_modification_time)} -# Build the image once, either to a remote registry designated by PRIMARY_DOCKER_REPO -# or to the local repository as "ko.local/refinery:" if PRIMARY_DOCKER_REPO is not set. -export KO_DOCKER_REPO="${PRIMARY_DOCKER_REPO:-ko.local}" +# Force IPv4 to avoid IPv6 connectivity issues when pulling base image layers +export GODEBUG=preferIPv4=1 + +if [[ "$PUSH" == "true" ]]; then + export KO_DOCKER_REPO="$GCLOUD_REGISTRY" +else + export KO_DOCKER_REPO="ko.local" +fi -echo "Building image locally with ko for multi-registry push..." # shellcheck disable=SC2086 -IMAGE_REF=$(./ko publish \ - --tags "${TAGS}" \ +IMAGE_REF=$(ko publish \ + --tags "${VERSION}" \ --base-import-paths \ --platform "linux/amd64,linux/arm64" \ - --image-label org.opencontainers.image.source=https://github.com/honeycombio/refinery \ + --image-label org.opencontainers.image.source=https://github.com/khan/refinery \ --image-label org.opencontainers.image.licenses=Apache-2.0 \ --image-label org.opencontainers.image.revision=${GIT_COMMIT} \ ./cmd/refinery) echo "Built image: ${IMAGE_REF}" - -# If COPY_DOCKER_REPOS is set, copy the built image to each of the listed registries. -# This is a comma-separated list of registry/repo names, e.g. -# "public.ecr.aws/honeycombio,ghcr.io/honeycombio/refinery" -if [[ -n "${COPY_DOCKER_REPOS:-}" ]]; then - echo "Pushing to multiple registries: ${COPY_DOCKER_REPOS}" - - IFS=',' read -ra REPOS <<< "$COPY_DOCKER_REPOS" - for REPO in "${REPOS[@]}"; do - REPO=$(echo "$REPO" | xargs) # trim whitespace - echo "Tagging and pushing to: $REPO" - - # Tag for each tag in the TAGS list - IFS=',' read -ra TAG_LIST <<< "$TAGS" - for TAG in "${TAG_LIST[@]}"; do - TAG=$(echo "$TAG" | xargs) # trim whitespace - TARGET_IMAGE="$REPO/refinery:$TAG" - echo "Copying $IMAGE_REF to $TARGET_IMAGE" - ./crane copy "$IMAGE_REF" "$TARGET_IMAGE" - done - done -fi From 42f0b8b84f99e7c077ad87882a1856bbd6a84da0 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 9 Mar 2026 14:10:48 -0700 Subject: [PATCH 03/35] detect suitable root span when root is absent Extracts findSuitableRootSpan: prefers the trace's RootSpan, falls back to the first non-annotation span (not a span event or link). Updates computeCustomCounts to use it so custom counts land on the best available span even when no root has arrived before the trace times out. --- collect/collect.go | 20 ++++++++++++++++-- collect/collect_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/collect/collect.go b/collect/collect.go index da7f3a282d..9e28a946fb 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -708,9 +708,25 @@ func (i *InMemCollector) initSpanCounterConfigs() { i.mutex.Unlock() } +// findSuitableRootSpan returns the root span of the trace if one is present. +// If no root span has been identified, it falls back to the first non-annotation +// span (i.e. not a span event or link). Returns nil if no suitable span exists. +func findSuitableRootSpan(t sendableTrace) *types.Span { + if t.RootSpan != nil { + return t.RootSpan + } + for _, sp := range t.GetSpans() { + if sp.AnnotationType() != types.SpanAnnotationTypeSpanEvent && + sp.AnnotationType() != types.SpanAnnotationTypeLink { + return sp + } + } + return nil +} + // computeCustomCounts computes each counter's value by iterating all spans in the trace // and attaches the results to the root span. -// Returns nil, nil if there are no counters configured or no root span. +// Returns nil, nil if there are no counters configured or no suitable target span. // // Stress relief note: this runs inside sendTraces(), the sole consumer of the // tracesToSend channel. Work is O(N×M) — N spans × M counters — so large @@ -730,7 +746,7 @@ func (i *InMemCollector) computeCustomCounts(t sendableTrace) (*types.Span, map[ return nil, nil } - targetSpan := t.RootSpan + targetSpan := findSuitableRootSpan(t) if targetSpan == nil { return nil, nil } diff --git a/collect/collect_test.go b/collect/collect_test.go index fcc950a189..ff2297faaf 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -2115,6 +2115,53 @@ func TestCustomSpanCounts_MultipleCounters(t *testing.T) { assert.Equal(t, int64(2), rootEvent.Data.Get("error_spans"), "2 spans have error=true") } +// TestCustomSpanCounts_NoRootSpan verifies that when a trace times out without +// a root span, custom counts land on the first non-annotation span instead. +func TestCustomSpanCounts_NoRootSpan(t *testing.T) { + conf := customCountConf([]config.SpanCounterConfig{{Key: "all_spans"}}) + conf.GetTracesConfigVal.TraceTimeout = config.Duration(5 * time.Millisecond) + + coll := newTestCollector(t, conf) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "no-root" + // annotation span: should not be the target + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: func() types.Payload { + p := types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x"}) + p.MetaAnnotationType = "span_event" + return p + }(), + APIKey: legacyAPIKey, + }, + }) + // regular span: should be the target + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, map[string]interface{}{"trace.parent_id": "x"}), + APIKey: legacyAPIKey, + }, + }) + + events := transmission.GetBlock(2) + require.Equal(t, 2, len(events)) + + // Exactly one span should carry the custom count (the first real span). + var counted []*types.Event + for _, ev := range events { + if ev.Data.Get("all_spans") != nil { + counted = append(counted, ev) + } + } + require.Equal(t, 1, len(counted), "custom count should appear on exactly one span when there is no root") + assert.Equal(t, int64(2), counted[0].Data.Get("all_spans"), "both spans should be counted") +} + // BenchmarkCollectorWithSamplers runs benchmarks for different sampler configurations. // This is a tricky benchmark to interpret because just setting up the input data // can easily be more expensive than the collector's routing code. The goal is to From a4dec74562a0c570984270ea9006221e8bf1e039 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Thu, 12 Mar 2026 16:42:09 +0000 Subject: [PATCH 04/35] ci(OTEL-125): remove pipeline-team dependabot reviewer (#1794) ## Which problem is this PR solving? Removes the hardcoded `honeycombio/pipeline-team` reviewer from the dependabot config as part of renaming the GitHub team. ## Short description of the changes - Removes the `reviewers` entry from `.github/dependabot.yml` Dependabot PRs will still require a review via the CODEOWNERS branch protection rule. Part of OTEL-125 --- .github/dependabot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a5e4aa310b..b323bc40c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,8 +11,6 @@ updates: interval: "monthly" labels: - "type: dependencies" - reviewers: - - "honeycombio/pipeline-team" groups: minor-patch: update-types: From 7d8cc3534436f89bd94a336592c9a7d6a1259c56 Mon Sep 17 00:00:00 2001 From: Ben Hanzl Date: Fri, 13 Mar 2026 15:15:08 -0400 Subject: [PATCH 05/35] chore: update codeowners (#1796) Update codeowners to the `agentic-observability` team. --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e52fa0e62c..71031f1523 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @honeycombio/pipeline-team +* @honeycombio/agentic-observability From 7fe57f6ad4710f6d9e4426441017ead12d4ecfef Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Fri, 13 Mar 2026 13:06:23 -0700 Subject: [PATCH 06/35] fix: move span counters to rules, update config meta --- collect/collect.go | 22 ++++++++++---------- collect/collect_test.go | 12 +++++------ config.md | 2 +- config/config.go | 2 +- config/file_config.go | 5 ++--- config/metadata/rulesMeta.yaml | 29 ++++++++++++++++++++++++++ config/mock.go | 6 +++--- config/sampler_config.go | 5 +++-- config/span_counter_config.go | 10 ++++----- config/span_counter_config_test.go | 22 ++++++++++---------- config/validate.go | 25 ++++++++++++++++++++++ config_complete.yaml | 2 +- metrics.md | 2 +- refinery_rules.md | 24 +++++++++++++++++++++ rules.md | 30 ++++++++++++++++++++++++++- tools/convert/configDataNames.txt | 2 +- tools/convert/minimal_config.yaml | 2 +- tools/convert/templates/configV2.tmpl | 2 +- 18 files changed, 155 insertions(+), 49 deletions(-) diff --git a/collect/collect.go b/collect/collect.go index 9e28a946fb..04772c51ac 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -114,7 +114,7 @@ type InMemCollector struct { hostname string memMetricSample []rtmetrics.Sample // Memory monitoring using runtime/metrics - spanCounterConfigs []config.SpanCounterConfig + spanCounters []config.SpanCounter } // These are the names of the metrics we use to track the number of events sent to peers through the router. @@ -172,7 +172,7 @@ func (i *InMemCollector) Start() error { i.Logger.Info().WithField("num_workers", numWorkers).Logf("Starting InMemCollector with %d workers", numWorkers) i.StressRelief.UpdateFromConfig() - i.initSpanCounterConfigs() + i.initSpanCounters() // Set queue capacity metrics for stress relief calculations i.Metrics.Store(DENOMINATOR_INCOMING_CAP, float64(imcConfig.IncomingQueueSize)) i.Metrics.Store(DENOMINATOR_PEER_CAP, float64(imcConfig.PeerQueueSize)) @@ -242,7 +242,7 @@ func (i *InMemCollector) reloadConfigs() { i.SamplerFactory.ClearDynsamplers() i.StressRelief.UpdateFromConfig() - i.initSpanCounterConfigs() + i.initSpanCounters() // Send reload signals to all workers to clear their local samplers // so that the new configuration will be propagated @@ -694,17 +694,17 @@ func (i *InMemCollector) addAdditionalAttributes(sp *types.Span) { } } -// initSpanCounterConfigs loads and initializes span counter configs from the current config. +// initSpanCounters loads and initializes span counters from the current config. // Must be called at startup and on config reload. -func (i *InMemCollector) initSpanCounterConfigs() { - cfgs := i.Config.GetSpanCounterConfig() - for j := range cfgs { - if err := cfgs[j].Init(); err != nil { - i.Logger.Error().WithField("error", err).Logf("failed to initialize span counter config entry %q", cfgs[j].Key) +func (i *InMemCollector) initSpanCounters() { + counters := i.Config.GetSpanCounters() + for j := range counters { + if err := counters[j].Init(); err != nil { + i.Logger.Error().WithField("error", err).Logf("failed to initialize span counter %q", counters[j].Key) } } i.mutex.Lock() - i.spanCounterConfigs = cfgs + i.spanCounters = counters i.mutex.Unlock() } @@ -739,7 +739,7 @@ func findSuitableRootSpan(t sendableTrace) *types.Span { // computed or attached to stress-sampled traces. func (i *InMemCollector) computeCustomCounts(t sendableTrace) (*types.Span, map[string]int64) { i.mutex.RLock() - counters := i.spanCounterConfigs + counters := i.spanCounters i.mutex.RUnlock() if len(counters) == 0 { diff --git a/collect/collect_test.go b/collect/collect_test.go index ff2297faaf..e0a7b97f34 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -1902,7 +1902,7 @@ func TestWorkerHealthReporting(t *testing.T) { } // customCountConf returns a base MockConfig suitable for custom span count tests. -func customCountConf(counters []config.SpanCounterConfig) *config.MockConfig { +func customCountConf(counters []config.SpanCounter) *config.MockConfig { return &config.MockConfig{ GetTracesConfigVal: config.TracesConfig{ SendTicker: config.Duration(2 * time.Millisecond), @@ -1924,7 +1924,7 @@ func customCountConf(counters []config.SpanCounterConfig) *config.MockConfig { IncomingQueueSize: 10, PeerQueueSize: 10, }, - SpanCounterConfigs: counters, + SpanCounters: counters, } } @@ -1958,7 +1958,7 @@ func TestCustomSpanCounts_NoCounters(t *testing.T) { // TestCustomSpanCounts_CountsLandOnRoot verifies that a counter with no // conditions counts all spans and attaches the result to the root span only. func TestCustomSpanCounts_CountsLandOnRoot(t *testing.T) { - counters := []config.SpanCounterConfig{ + counters := []config.SpanCounter{ {Key: "all_spans"}, } coll := newTestCollector(t, customCountConf(counters)) @@ -2005,7 +2005,7 @@ func TestCustomSpanCounts_CountsLandOnRoot(t *testing.T) { // TestCustomSpanCounts_ConditionalCounting verifies that only spans matching // a condition are counted. func TestCustomSpanCounts_ConditionalCounting(t *testing.T) { - counters := []config.SpanCounterConfig{ + counters := []config.SpanCounter{ { Key: "error_spans", Conditions: []*config.RulesBasedSamplerCondition{ @@ -2061,7 +2061,7 @@ func TestCustomSpanCounts_ConditionalCounting(t *testing.T) { // TestCustomSpanCounts_MultipleCounters verifies that multiple counters with // different conditions produce independent counts on the root span. func TestCustomSpanCounts_MultipleCounters(t *testing.T) { - counters := []config.SpanCounterConfig{ + counters := []config.SpanCounter{ { Key: "db_spans", Conditions: []*config.RulesBasedSamplerCondition{ @@ -2118,7 +2118,7 @@ func TestCustomSpanCounts_MultipleCounters(t *testing.T) { // TestCustomSpanCounts_NoRootSpan verifies that when a trace times out without // a root span, custom counts land on the first non-annotation span instead. func TestCustomSpanCounts_NoRootSpan(t *testing.T) { - conf := customCountConf([]config.SpanCounterConfig{{Key: "all_spans"}}) + conf := customCountConf([]config.SpanCounter{{Key: "all_spans"}}) conf.GetTracesConfigVal.TraceTimeout = config.Duration(5 * time.Millisecond) coll := newTestCollector(t, conf) diff --git a/config.md b/config.md index fb1b4ca340..20133d728c 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-03-13 at 20:03:54 UTC. ## The Config file diff --git a/config/config.go b/config/config.go index cbbd211265..a709cf1b41 100644 --- a/config/config.go +++ b/config/config.go @@ -151,7 +151,7 @@ type Config interface { GetAddCountsToRoot() bool - GetSpanCounterConfig() []SpanCounterConfig + GetSpanCounters() []SpanCounter GetConfigMetadata() []ConfigMetadata diff --git a/config/file_config.go b/config/file_config.go index dbcf87a7b7..9eae575ea5 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -193,7 +193,6 @@ type RefineryTelemetryConfig struct { AddSpanCountToRoot *DefaultTrue `yaml:"AddSpanCountToRoot" default:"true"` // Avoid pointer woe on access, use GetAddSpanCountToRoot() instead. AddCountsToRoot bool `yaml:"AddCountsToRoot"` AddHostMetadataToTrace *DefaultTrue `yaml:"AddHostMetadataToTrace" default:"true"` // Avoid pointer woe on access, use GetAddHostMetadataToTrace() instead. - CustomSpanCounts []SpanCounterConfig `yaml:"CustomSpanCounts,omitempty"` } type TracesConfig struct { @@ -1117,11 +1116,11 @@ func (f *fileConfig) GetAddCountsToRoot() bool { return f.mainConfig.Telemetry.AddCountsToRoot } -func (f *fileConfig) GetSpanCounterConfig() []SpanCounterConfig { +func (f *fileConfig) GetSpanCounters() []SpanCounter { f.mux.RLock() defer f.mux.RUnlock() - return f.mainConfig.Telemetry.CustomSpanCounts + return f.rulesConfig.SpanCounters } func (f *fileConfig) GetSampleCacheConfig() SampleCacheConfig { diff --git a/config/metadata/rulesMeta.yaml b/config/metadata/rulesMeta.yaml index b80e4ee983..a4a9a5c823 100644 --- a/config/metadata/rulesMeta.yaml +++ b/config/metadata/rulesMeta.yaml @@ -738,3 +738,32 @@ groups: The best practice is to always specify `Datatype`; this avoids ambiguity, allows for more accurate comparisons, and offers a minor performance improvement. + + - name: SpanCounters + title: "Custom Span Count Configuration" + sortorder: 80 + description: > + Defines a single custom span counter. Each counter has a Key that names + the field written to the root span, and an optional list of Conditions + that must all match for a span to be counted. Spans are counted when + all of the entry's Conditions match. If Conditions is empty, every span + in the trace is counted. The counter value is written to the root span + under the key specified by `Key`. If no root span exists when the trace + is sent, the counter is written to the first non-annotation span instead. + fields: + - name: Key + type: string + validations: + - type: notempty + summary: is the field name written to the root span with the counter value. + description: > + The name of the field that will be added to the root span. Must not + be empty. + + - name: Conditions + type: objectarray + summary: is the list of conditions a span must satisfy to be counted. + description: > + All conditions must match for a span to be counted. If empty, every + span in the trace is counted. Uses the same condition format as + rules-based sampler conditions. diff --git a/config/mock.go b/config/mock.go index 57eef3af73..58660a281f 100644 --- a/config/mock.go +++ b/config/mock.go @@ -52,7 +52,7 @@ type MockConfig struct { AdditionalErrorFields []string AddSpanCountToRoot bool AddCountsToRoot bool - SpanCounterConfigs []SpanCounterConfig + SpanCounters []SpanCounter CacheOverrunStrategy string SampleCache SampleCacheConfig StressRelief StressReliefConfig @@ -416,11 +416,11 @@ func (f *MockConfig) GetAddCountsToRoot() bool { return f.AddSpanCountToRoot } -func (f *MockConfig) GetSpanCounterConfig() []SpanCounterConfig { +func (f *MockConfig) GetSpanCounters() []SpanCounter { f.Mux.RLock() defer f.Mux.RUnlock() - return f.SpanCounterConfigs + return f.SpanCounters } func (f *MockConfig) GetSampleCacheConfig() SampleCacheConfig { diff --git a/config/sampler_config.go b/config/sampler_config.go index 2560d322e8..fa8f39fbc9 100644 --- a/config/sampler_config.go +++ b/config/sampler_config.go @@ -172,8 +172,9 @@ func (v *RulesBasedDownstreamSampler) NameMeaningfulRate() string { } type V2SamplerConfig struct { - RulesVersion int `json:"rulesversion" yaml:"RulesVersion" validate:"required,ge=2"` - Samplers map[string]*V2SamplerChoice `json:"samplers" yaml:"Samplers,omitempty" validate:"required"` + RulesVersion int `json:"rulesversion" yaml:"RulesVersion" validate:"required,ge=2"` + Samplers map[string]*V2SamplerChoice `json:"samplers" yaml:"Samplers,omitempty" validate:"required"` + SpanCounters []SpanCounter `json:"spancounters" yaml:"SpanCounters,omitempty"` } type GetSamplingFielder interface { diff --git a/config/span_counter_config.go b/config/span_counter_config.go index 6985e9203d..1eb6c1130d 100644 --- a/config/span_counter_config.go +++ b/config/span_counter_config.go @@ -2,22 +2,22 @@ package config import "strings" -// SpanData is the interface required for matching span fields in a SpanCounterConfig. +// SpanData is the interface required for matching span fields in a SpanCounter. // It is satisfied by *types.Payload. type SpanData interface { Get(key string) any Exists(key string) bool } -// SpanCounterConfig defines a custom span count to be computed and added to +// SpanCounter defines a custom span count to be computed and added to // the root span under Key. Spans are counted if they satisfy all Conditions. -type SpanCounterConfig struct { +type SpanCounter struct { Key string `yaml:"Key"` Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` } // Init initializes all conditions. Must be called before MatchesSpan. -func (c *SpanCounterConfig) Init() error { +func (c *SpanCounter) Init() error { for _, cond := range c.Conditions { if err := cond.Init(); err != nil { return err @@ -28,7 +28,7 @@ func (c *SpanCounterConfig) Init() error { // MatchesSpan returns true if the span satisfies all conditions. // span is the span being tested; root is the root span's data (may be nil). -func (c *SpanCounterConfig) MatchesSpan(span SpanData, root SpanData) bool { +func (c *SpanCounter) MatchesSpan(span SpanData, root SpanData) bool { for _, cond := range c.Conditions { var value any var exists bool diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go index 366a95803b..287e438aa2 100644 --- a/config/span_counter_config_test.go +++ b/config/span_counter_config_test.go @@ -175,18 +175,18 @@ func TestConditionMatchesValue(t *testing.T) { } // ---------------------------------------------------------------------------- -// SpanCounterConfig.MatchesSpan +// SpanCounter.MatchesSpan // ---------------------------------------------------------------------------- func TestMatchesSpan_NoConditions(t *testing.T) { // A counter with no conditions matches every span. - counter := SpanCounterConfig{Key: "all"} + counter := SpanCounter{Key: "all"} assert.True(t, counter.MatchesSpan(spanData{"foo": "bar"}, nil)) assert.True(t, counter.MatchesSpan(spanData{}, nil)) } func TestMatchesSpan_SingleCondition(t *testing.T) { - counter := SpanCounterConfig{ + counter := SpanCounter{ Key: "errors", Conditions: []*RulesBasedSamplerCondition{cond("error", EQ, true)}, } @@ -197,7 +197,7 @@ func TestMatchesSpan_SingleCondition(t *testing.T) { } func TestMatchesSpan_MultipleConditionsAllMustMatch(t *testing.T) { - counter := SpanCounterConfig{ + counter := SpanCounter{ Key: "slow-errors", Conditions: []*RulesBasedSamplerCondition{ cond("error", EQ, true), @@ -213,7 +213,7 @@ func TestMatchesSpan_MultipleConditionsAllMustMatch(t *testing.T) { func TestMatchesSpan_RootPrefixedField(t *testing.T) { // "root.service.name" reads from the root span data, not the span itself. - counter := SpanCounterConfig{ + counter := SpanCounter{ Key: "svc-db", Conditions: []*RulesBasedSamplerCondition{cond("root.service.name", EQ, "database")}, } @@ -227,7 +227,7 @@ func TestMatchesSpan_RootPrefixedField(t *testing.T) { func TestMatchesSpan_RootPrefixedField_NilRoot(t *testing.T) { // When root is nil a root-prefixed field is never found → field is absent. - counter := SpanCounterConfig{ + counter := SpanCounter{ Key: "svc", Conditions: []*RulesBasedSamplerCondition{cond("root.service.name", EQ, "database")}, } @@ -243,7 +243,7 @@ func TestMatchesSpan_MultiFieldFallback(t *testing.T) { if err := c.Init(); err != nil { t.Fatal(err) } - counter := SpanCounterConfig{Key: "has-trace", Conditions: []*RulesBasedSamplerCondition{c}} + counter := SpanCounter{Key: "has-trace", Conditions: []*RulesBasedSamplerCondition{c}} assert.True(t, counter.MatchesSpan(spanData{"trace.trace_id": "abc"}, nil)) assert.True(t, counter.MatchesSpan(spanData{"traceId": "abc"}, nil)) @@ -261,7 +261,7 @@ func TestMatchesSpan_MultiFieldFallback_FirstWins(t *testing.T) { if err := c.Init(); err != nil { t.Fatal(err) } - counter := SpanCounterConfig{Key: "k", Conditions: []*RulesBasedSamplerCondition{c}} + counter := SpanCounter{Key: "k", Conditions: []*RulesBasedSamplerCondition{c}} // "a" is found with wrong value; "b" has the right value but is not checked. assert.False(t, counter.MatchesSpan(spanData{"a": "no", "b": "yes"}, nil)) @@ -272,7 +272,7 @@ func TestMatchesSpan_MultiFieldFallback_FirstWins(t *testing.T) { func TestMatchesSpan_TypedCondition(t *testing.T) { // When Datatype is set, Init wires up a type-coercing Matches function. // Verify that MatchesSpan delegates to it correctly. - counter := SpanCounterConfig{ + counter := SpanCounter{ Key: "count-int", Conditions: []*RulesBasedSamplerCondition{condTyped("code", EQ, 200, "int")}, } @@ -283,11 +283,11 @@ func TestMatchesSpan_TypedCondition(t *testing.T) { } func TestMatchesSpan_ExistsAndNotExists(t *testing.T) { - exists := SpanCounterConfig{ + exists := SpanCounter{ Key: "has-field", Conditions: []*RulesBasedSamplerCondition{cond("db.query", Exists, nil)}, } - notExists := SpanCounterConfig{ + notExists := SpanCounter{ Key: "no-field", Conditions: []*RulesBasedSamplerCondition{cond("db.query", NotExists, nil)}, } diff --git a/config/validate.go b/config/validate.go index 41b0c135b7..92f89f5c33 100644 --- a/config/validate.go +++ b/config/validate.go @@ -653,6 +653,31 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { } } hasSamplers = true + case "SpanCounters": + if arr, ok := v.([]any); !ok { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters must be an array, but %v is %T", v, v), + Severity: Error, + }) + } else { + for i, entry := range arr { + if entryMap, ok := entry.(map[string]any); ok { + rulesmap := map[string]any{"SpanCounters": entryMap} + subresults := m.Validate(rulesmap) + for _, result := range subresults { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("Within SpanCounters[%d]: %s", i, result.Message), + Severity: result.Severity, + }) + } + } else { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d] must be an object, but %v is %T", i, entry, entry), + Severity: Error, + }) + } + } + } default: results = append(results, ValidationResult{ Message: fmt.Sprintf("unknown top-level key %s", k), diff --git a/config_complete.yaml b/config_complete.yaml index beb7eaf4ba..290572bca6 100644 --- a/config_complete.yaml +++ b/config_complete.yaml @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created on 2026-02-25 at 20:49:27 UTC from ../../config.yaml using a template generated on 2026-02-25 at 20:49:24 UTC +# created on 2026-03-13 at 20:03:53 UTC from ../../config.yaml using a template generated on 2026-03-13 at 20:03:51 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of diff --git a/metrics.md b/metrics.md index 4be5fcf988..bcee93bd04 100644 --- a/metrics.md +++ b/metrics.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Metrics Documentation This document contains the description of various metrics used in Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-03-13 at 20:03:53 UTC. Note: This document does not include metrics defined in the dynsampler-go dependency, as those metrics are generated dynamically at runtime. As a result, certain metrics may be missing or incomplete in this document, but they will still be available during execution with their full names. diff --git a/refinery_rules.md b/refinery_rules.md index 5fcd2b9592..6a22a5d98c 100644 --- a/refinery_rules.md +++ b/refinery_rules.md @@ -671,3 +671,27 @@ If your traces are consistent lengths and changes in trace length is a useful in - Type: `bool` +## Custom Span Count Configuration + +Defines a single custom span counter. +Each counter has a Key that names the field written to the root span, and an optional list of Conditions that must all match for a span to be counted. +Spans are counted when all of the entry's Conditions match. +If Conditions is empty, every span in the trace is counted. +The counter value is written to the root span under the key specified by `Key`. +If no root span exists when the trace is sent, the counter is written to the first non-annotation span instead. + +### `Key` + +The name of the field that will be added to the root span. +Must not be empty. + +- Type: `string` + +### `Conditions` + +All conditions must match for a span to be counted. +If empty, every span in the trace is counted. +Uses the same condition format as rules-based sampler conditions. + +- Type: `objectarray` + diff --git a/rules.md b/rules.md index ee6023bc0e..e3059099d2 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-03-13 at 20:03:54 UTC. ## The Rules file @@ -55,6 +55,7 @@ The remainder of this document describes the samplers that can be used within th - [Rules for Rules-based Samplers](#rules-for-rules-based-samplers) - [Conditions for the Rules in Rules-based Samplers](#conditions-for-the-rules-in-rules-based-samplers) - [Total Throughput Sampler](#total-throughput-sampler) +- [Custom Span Count Configuration](#custom-span-count-configuration) --- ## Deterministic Sampler @@ -715,3 +716,30 @@ If your traces are consistent lengths and changes in trace length is a useful in Type: `bool` +--- +## Custom Span Count Configuration + +### Name: `SpanCounters` + +Defines a single custom span counter. +Each counter has a Key that names the field written to the root span, and an optional list of Conditions that must all match for a span to be counted. +Spans are counted when all of the entry's Conditions match. +If Conditions is empty, every span in the trace is counted. +The counter value is written to the root span under the key specified by `Key`. +If no root span exists when the trace is sent, the counter is written to the first non-annotation span instead. + +### `Key` + +The name of the field that will be added to the root span. +Must not be empty. + +Type: `string` + +### `Conditions` + +All conditions must match for a span to be counted. +If empty, every span in the trace is counted. +Uses the same condition format as rules-based sampler conditions. + +Type: `objectarray` + diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 793d4bb1e7..5cc84b2595 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-02-25 at 20:49:25 UTC. +# Automatically generated on 2026-03-13 at 20:03:51 UTC. General: - ConfigurationVersion diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index eb49635629..3c666d0511 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-02-25 at 20:49:25 UTC +# automatically generated on 2026-03-13 at 20:03:52 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 7f91a2f2a5..b2577f70b1 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-02-25 at 20:49:24 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-03-13 at 20:03:51 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of From 903d73974296c54217f3011632996400dcf5bd2f Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 16 Mar 2026 12:44:44 -0700 Subject: [PATCH 07/35] point to the sre-team project when building docker --- build-docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-docker.sh b/build-docker.sh index 8a5cfb2631..f1b9f8137d 100755 --- a/build-docker.sh +++ b/build-docker.sh @@ -3,7 +3,7 @@ set -o nounset set -o pipefail set -o xtrace -GCLOUD_REGISTRY="us-central1-docker.pkg.dev/khan-internal-services/refinery" +GCLOUD_REGISTRY="gcr.io/sre-team-418623" # Parse flags PUSH=false From d5e35cf57aa2855a37f6bb389c59e8aaa7c28fd4 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 16 Mar 2026 16:04:04 -0700 Subject: [PATCH 08/35] fix(root): select the earliest span as the root --- collect/collect.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/collect/collect.go b/collect/collect.go index 04772c51ac..be24512518 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -709,19 +709,23 @@ func (i *InMemCollector) initSpanCounters() { } // findSuitableRootSpan returns the root span of the trace if one is present. -// If no root span has been identified, it falls back to the first non-annotation -// span (i.e. not a span event or link). Returns nil if no suitable span exists. +// If no root span has been identified, it falls back to the non-annotation +// span (i.e. not a span event or link) with the earliest timestamp, which is +// the most likely root. Returns nil if no suitable span exists. func findSuitableRootSpan(t sendableTrace) *types.Span { if t.RootSpan != nil { return t.RootSpan } + var best *types.Span for _, sp := range t.GetSpans() { if sp.AnnotationType() != types.SpanAnnotationTypeSpanEvent && sp.AnnotationType() != types.SpanAnnotationTypeLink { - return sp + if best == nil || sp.Timestamp.Before(best.Timestamp) { + best = sp + } } } - return nil + return best } // computeCustomCounts computes each counter's value by iterating all spans in the trace From 66ceb61717b019f45a9a7d13fbf7efad59667ceb Mon Sep 17 00:00:00 2001 From: Mike Terhar Date: Wed, 25 Mar 2026 12:15:50 -0400 Subject: [PATCH 09/35] feat: add capacity/limit companion metrics for queues and memory (#1799) Include metrics to show when Refinery is getting close to the limit for peer queue, incoming queue, and memory allocation. --- collect/collect.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/collect/collect.go b/collect/collect.go index ea430892f1..0a27bfd583 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -128,11 +128,14 @@ var inMemCollectorMetrics = []metrics.Metadata{ {Name: "trace_span_count", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "number of spans in a trace"}, {Name: "collector_incoming_queue", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "number of spans currently in the incoming queue"}, {Name: "collector_peer_queue_length", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "number of spans in the peer queue"}, + {Name: "collector_peer_queue_capacity", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "configured maximum number of spans in the peer queue"}, {Name: "collector_incoming_queue_length", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "number of spans in the incoming queue"}, + {Name: "collector_incoming_queue_capacity", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "configured maximum number of spans in the incoming queue"}, {Name: "collector_peer_queue", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "number of spans currently in the peer queue"}, {Name: "collector_cache_size", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "number of traces currently stored in the trace cache"}, {Name: "collect_cache_entries", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "Total number of traces currently stored in the cache from all workers"}, {Name: "memory_heap_allocation", Type: metrics.Gauge, Unit: metrics.Bytes, Description: "current heap allocation"}, + {Name: "memory_limit", Type: metrics.Gauge, Unit: metrics.Bytes, Description: "configured maximum memory allocation for the collector (derived from MaxAlloc or AvailableMemory * MaxMemoryPercentage)"}, {Name: "span_received", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of spans received by the collector"}, {Name: "span_processed", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of spans processed by the collector"}, {Name: "spans_waiting", Type: metrics.UpDown, Unit: metrics.Dimensionless, Description: "number of spans waiting to be processed by the collector"}, @@ -341,6 +344,13 @@ func (i *InMemCollector) monitor() { // Check worker health and report aggregated status i.Health.Ready(collectorHealthKey, i.isReady()) + // Emit queue capacity limits and memory limit so consumers can compute utilization + monitorConfig := i.Config.GetCollectionConfig() + i.Metrics.Gauge("collector_incoming_queue_capacity", float64(monitorConfig.IncomingQueueSize)) + i.Metrics.Gauge("collector_peer_queue_capacity", float64(monitorConfig.PeerQueueSize)) + maxAlloc := monitorConfig.GetMaxAlloc() + i.Metrics.Gauge("memory_limit", float64(maxAlloc)) + // Aggregate metrics totalIncoming := 0 totalPeer := 0 From 3a683f34bc21d1aeb96ae782f7b0efe5bc936f3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:58:16 -0400 Subject: [PATCH 10/35] maint(deps): bump the minor-patch group across 1 directory with 21 updates (#1795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-patch group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/honeycombio/husky](https://github.com/honeycombio/husky) | `0.41.0` | `0.43.0` | | [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.2` | `1.18.4` | | [github.com/open-telemetry/opamp-go](https://github.com/open-telemetry/opamp-go) | `0.22.0` | `0.23.0` | | [github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest](https://github.com/open-telemetry/opentelemetry-collector-contrib) | `0.145.0` | `0.147.0` | | [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) | `9.17.3` | `9.18.0` | | [github.com/tinylib/msgp](https://github.com/tinylib/msgp) | `1.6.2` | `1.6.3` | | [github.com/valyala/fastjson](https://github.com/valyala/fastjson) | `1.6.7` | `1.6.10` | | [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.67.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.67.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp](https://github.com/open-telemetry/opentelemetry-go) | `1.40.0` | `1.42.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://github.com/open-telemetry/opentelemetry-go) | `1.40.0` | `1.42.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) | `1.40.0` | `1.42.0` | Updates `github.com/honeycombio/husky` from 0.41.0 to 0.43.0
Release notes

Sourced from github.com/honeycombio/husky's releases.

v0.43.0

What's Changed

✨ Added

  • feat(otlp): emit app.schema_urls telemetry for traces and logs (#349) | Mike Goldsmith
  • feat: replace OTLP proto fork with a 0.7 compat package (#347) | Robb Kidd

Full Changelog: https://github.com/honeycombio/husky/compare/v0.42.0...v0.43.0

v0.42.0

What's Changed

✨ Added

Full Changelog: https://github.com/honeycombio/husky/compare/v0.41.0...v0.42.0

Changelog

Sourced from github.com/honeycombio/husky's changelog.

v0.43.0 2026-03-06

✨ Added

  • feat(otlp): emit app.schema_urls telemetry for traces and logs (#349) | Mike Goldsmith
  • feat: replace OTLP proto fork with a 0.7 compat package (#347) | Robb Kidd

v0.42.0 2026-03-04

✨ Added

Commits
  • 64a409a rel: prepare v0.43.0 release (#350)
  • 0a25e2e feat(otlp): emit app.schema_urls telemetry for traces and logs [OTEL-121] (#349)
  • 21a078f feat: replace OTLP proto fork with a 0.7 compat package (#347)
  • 0817529 rel: Prepare the v0.42.0 release (#348)
  • 551e5e8 feat(otlp): record schema_url for traces and logs (#344)
  • See full diff in compare view

Updates `github.com/klauspost/compress` from 1.18.2 to 1.18.4
Release notes

Sourced from github.com/klauspost/compress's releases.

v1.18.4

What's Changed

New Contributors

Full Changelog: https://github.com/klauspost/compress/compare/v1.18.2...v1.18.4

v1.18.3

Downstream CVE-2025-61728

See golang/go#77102

Full Changelog: https://github.com/klauspost/compress/compare/v1.18.2...v1.18.3

Commits
  • c03560f zstd: Add ResetWithOptions to encoder/decoder (#1122)
  • 0874ab8 build(deps): bump the github-actions group with 3 updates (#1126)
  • 4a36836 doc: Clarify documentation in readme (#1125)
  • 4309644 zstd: document concurrency option handling in encoder (#1124)
  • c262ec6 Update README.md
  • 861ca97 Downstream CVE-2025-61728 (#1123)
  • 03de960 gzhttp: Add zstandard to server handler wrapper (#1121)
  • bb1ab3b build(deps): bump the github-actions group with 2 updates (#1120)
  • 986a51e fix(gzhttp): preserve qvalue when extra parameters follow in Accept-Encoding ...
  • fbe3b12 build(deps): bump the github-actions group with 3 updates (#1118)
  • Additional commits viewable in compare view

Updates `github.com/open-telemetry/opamp-go` from 0.22.0 to 0.23.0
Release notes

Sourced from github.com/open-telemetry/opamp-go's releases.

v0.23.0

What's Changed

New Contributors

Full Changelog: https://github.com/open-telemetry/opamp-go/compare/v0.22.0...v0.23.0

Commits
  • f68db34 Exclude full status on websocket reconnect (#512)
  • 623ef46 Update opamp-spec to v0.16.0 (#516)
  • 97981b3 Handle Broken Pipe and Connection reset issues with wssender (#506)
  • 7cd1897 Add scale test mode to example agent (#481)
  • 3c4aa12 chore(deps): update github/codeql-action action to v4.32.2 (#508)
  • 50ebdcc chore(deps): update module golang.org/x/tools to v0.42.0 (#511)
  • f640fa8 fix: flaky TestHTTPClientStartWithHeartbeatInterval test (#510)
  • b7c50ee chore(deps): update fossas/fossa-action action to v1.8.0 (#509)
  • cd3b0ab Add OpAMP-Instance-UID request header (#503)
  • 647a076 Update opamp-spec to v0.15.0 (#505)
  • Additional commits viewable in compare view

Updates `github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest` from 0.145.0 to 0.147.0
Release notes

Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's releases.

v0.147.0

The OpenTelemetry Collector Contrib contains everything in the opentelemetry-collector release, be sure to check the release notes there as well.

End User Changelog

🛑 Breaking changes 🛑

  • all: Remove unmaintained receiver/bigip component (#46040)

  • exporter/elasticsearch: Ignore mapping::mode config option (#45246) The mapping::mode config option has already been deprecated and is now ignored. Instead, use the X-Elastic-Mapping-Mode client metadata key (via headers_setter extension) or the elastic.mapping.mode scope attribute to control the mapping mode per-request. See the README for migration instructions.

  • extension/sumologic: Migrate updateCollectorMetadata from feature gate to config parameter and enable by default (#46102) The extension.sumologic.updateCollectorMetadata feature gate has been replaced with an update_metadata configuration parameter. The feature is now enabled by default, eliminating the need for users to specify the --feature-gates flag. Users can control this behavior via the YAML configuration:

    extensions:
      sumologic:
        update_metadata: true  # default: true
    
  • processor/azuredetector: Changed cloud platform value for Azure VM from azure_vm to azure.vm to align with OpenTelemetry semantic conventions v1.39.0. (#45030)

  • processor/resourcedetection: Remove feature gate processor.resourcedetection.removeGCPFaasID (#45808)

  • processor/resourcedetection: Changed cloud platform value for Azure EKS from azure_eks to azure.eks to align with OpenTelemetry semantic conventions v1.39.0. (#45030)

  • receiver/kafka: Remove deprecated topic and exclude_topic fields (#46232)

  • receiver/mongodb: Add service.instance.id resource attribute and move database from resource to db.namespace metric attribute in MongoDB receiver (#45506) The service.instance.id attribute is a deterministic UUID v5 derived from server address and port. The database attribute has been moved from a resource attribute to a metric-level attribute (db.namespace) on all per-database metrics, following OpenTelemetry semantic conventions. This produces a single resource per MongoDB server instead of one per database, fixing Prometheus batching compatibility.

🚩 Deprecations 🚩

  • processor/k8s_attributes: Rename internal telemetry metrics to use dots instead of underscores (#45871)

💡 Enhancements 💡

  • connector/grafanacloud: Make the default configuration of the Grafana Cloud Connector support Kubernetes deployments. (#45469) Add "k8S.node.uid" and "k8S.cluster.name" to the default set of resource attributes evaluated by the Grafana Cloud Connector.

  • exporter/clickhouse: Do not crash the exporter if it cannot inspect the table schemas (#46285) Previously setting create_schema: false would skip validating the actual connection on start. This was useful if you had unstable targets, as you could run the collector and let the sending queue/failover connector handle connection errors to ClickHouse.

  • exporter/loadbalancing: Support metrics routing by attributes in the loadbalancing exporter (#45675)

... (truncated)

Changelog

Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's changelog.

v0.147.0

💡 Enhancements 💡

  • extension/oauth2client: Expose a context-aware Token method from oauth2clientauth extension (#45917) This change exposes a Token(ctx context.Context) (*oauth2.Token, error) method that clients can use to obtain a Token. This may be used by components that are not HTTP-based, such as the Kafka components for use with SASL/OAUTHBEARER.

  • pkg/pdatatest: Add entity references comparison to CompareResource and IgnoreResourceEntityRefs option (#46345)

  • pkg/xk8stest: Display pod events and logs on collector startup timeout for easier diagnosis of e2e failures. (#46305)

  • receiver/splunkenterprise: Enables dynamic metric reaggregation in the Splunk Enterprise receiver. This does not break existing configuration files. (#45396)

v0.146.0

🚩 Deprecations 🚩

  • pkg/stanza: Package "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/parser/json" has been deprecated. Use "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/parser/jsonparser" instead (#45006)
  • pkg/stanza: Package "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/errors" has been deprecated. Use "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/stanzaerrors" instead (#45006)
  • pkg/stanza: Package "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/parser/time" has been deprecated. Use "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/operator/parser/timeparser" instead (#45006)

💡 Enhancements 💡

  • processor/filter: Introduces inferred context conditions for filtering (#37904) Introduces three new top-level config fields [metric_conditions, log_conditions, trace_conditions]. A user can supply OTTL conditions for each without needing to supply context.

  • receiver/pprof: Implement the functionality of transforming pprof to OTel Profiles (#45411)

🧰 Bug fixes 🧰

  • processor/sumologic: Export config types to allow programmatic configuration via Go API (#45880)
  • receiver/filestats: Ensure that bsd build tags are respected by renaming filestats_darwin.go to filestats_bsd.go (#42645)
Commits
  • 28446b3 [chore] Prepare release 0.147.0 (#46554)
  • f9a60cd [chore] Update core dependencies (#46549)
  • 3fcd89a [chore][pkg/stanza] Bump missing semconv to v1.39.0 (#46529)
  • f92adcb Bump go.opentelemetry.io/otel/sdk from 1.37.0 to 1.40.0 in /internal/docker (...
  • 540ae31 [receiver/haproxy] Enable re-aggregation feature (#46518)
  • 277dbbd Oracle Stored Procedure Fixes (#46466)
  • d2a325c [receiver/oracledb] Enable re-aggregation feature (#46512)
  • b140c8e [chore] Fix test flakiness awsxrayreceiver (#46509)
  • 18e4bd8 [chore] Add display names for receivers - batch 4 (#46472)
  • a6e7210 [receiver/awscloudwatchreceiver] fix logs being skipped (#46253)
  • Additional commits viewable in compare view

Updates `github.com/redis/go-redis/v9` from 9.17.3 to 9.18.0
Release notes

Sourced from github.com/redis/go-redis/v9's releases.

9.18.0

Redis 8.6 Support

Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS.

Smart Client Handoff (Maintenance Notifications) for Cluster

note: Pending RS version release

This release introduces comprehensive support for Redis Enterprise Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by:

  • Relaxing timeouts during migration (SMIGRATING) to prevent false failures
  • Triggering lazy cluster state reloads upon completion (SMIGRATED)
  • Enabling seamless operations during Redis Enterprise maintenance windows

(#3643) by @​ndyakov

OpenTelemetry Native Metrics Support

Added comprehensive OpenTelemetry metrics support following the OpenTelemetry Database Client Semantic Conventions. The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new extra/redisotel-native package.

Metric groups include:

  • Command metrics: Operation duration with retry tracking
  • Connection basic: Connection count and creation time
  • Resiliency: Errors, handoffs, timeout relaxation
  • Connection advanced: Wait time and use time
  • Pubsub metrics: Published and received messages
  • Stream metrics: Processing duration and maintenance notifications

(#3637) by @​ofekshenawa

✨ New Features

  • HOTKEYS Commands: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization (#3695) by @​ofekshenawa
  • Streams Idempotent Production: Added support for Redis 8.6+ Streams Idempotent Production with ProducerID, IdempotentID, IdempotentAuto in XAddArgs and new XCFGSET command (#3693) by @​ofekshenawa
  • NaN Values for TimeSeries: Added support for NaN (Not a Number) values in Redis time series commands (#3687) by @​ofekshenawa
  • DialerRetries Options: Added DialerRetries and DialerRetryTimeout to ClusterOptions, RingOptions, and FailoverOptions (#3686) by @​naveenchander30
  • ConnMaxLifetimeJitter: Added jitter configuration to distribute connection expiration times and prevent thundering herd (#3666) by @​cyningsun
  • Digest Helper Functions: Added DigestString and DigestBytes helper functions for client-side xxh3 hashing compatible with Redis DIGEST command (#3679) by @​ofekshenawa
  • SMIGRATED New Format: Updated SMIGRATED parser to support new format and remember original host:port (#3697) by @​ndyakov
  • Cluster State Reload Interval: Added cluster state reload interval option for maintenance notifications (#3663) by @​ndyakov

🐛 Bug Fixes

  • PubSub nil pointer dereference: Fixed nil pointer dereference in PubSub after WithTimeout() - pubSubPool is now properly cloned (#3710) by @​Copilot
  • MaintNotificationsConfig nil check: Guard against nil MaintNotificationsConfig in initConn (#3707) by @​veeceey
  • wantConnQueue zombie elements: Fixed zombie wantConn elements accumulation in wantConnQueue (#3680) by @​cyningsun
  • XADD/XTRIM approx flag: Fixed XADD and XTRIM to use = when approx is false (#3684) by @​ndyakov
  • Sentinel timeout retry: When connection to a sentinel times out, attempt to connect to other sentinels (#3654) by @​cxljs

... (truncated)

Changelog

Sourced from github.com/redis/go-redis/v9's changelog.

9.18.0 (2026-02-16)

🚀 Highlights

Redis 8.6 Support

Added support for Redis 8.6, including new commands and features for streams idempotent production and HOTKEYS.

Smart Client Handoff (Maintenance Notifications) for Cluster

This release introduces comprehensive support for Redis Cluster maintenance notifications via SMIGRATING/SMIGRATED push notifications. The client now automatically handles slot migrations by:

  • Relaxing timeouts during migration (SMIGRATING) to prevent false failures
  • Triggering lazy cluster state reloads upon completion (SMIGRATED)
  • Enabling seamless operations during Redis Enterprise maintenance windows

(#3643) by @​ndyakov

OpenTelemetry Native Metrics Support

Added comprehensive OpenTelemetry metrics support following the OpenTelemetry Database Client Semantic Conventions. The implementation uses a Bridge Pattern to keep the core library dependency-free while providing optional metrics instrumentation through the new extra/redisotel-native package.

Metric groups include:

  • Command metrics: Operation duration with retry tracking
  • Connection basic: Connection count and creation time
  • Resiliency: Errors, handoffs, timeout relaxation
  • Connection advanced: Wait time and use time
  • Pubsub metrics: Published and received messages
  • Stream metrics: Processing duration and maintenance notifications

(#3637) by @​ofekshenawa

✨ New Features

  • HOTKEYS Commands: Added support for Redis HOTKEYS feature for identifying hot keys based on CPU consumption and network utilization (#3695) by @​ofekshenawa
  • Streams Idempotent Production: Added support for Redis 8.6+ Streams Idempotent Production with ProducerID, IdempotentID, IdempotentAuto in XAddArgs and new XCFGSET command (#3693) by @​ofekshenawa
  • NaN Values for TimeSeries: Added support for NaN (Not a Number) values in Redis time series commands (#3687) by @​ofekshenawa
  • DialerRetries Options: Added DialerRetries and DialerRetryTimeout to ClusterOptions, RingOptions, and FailoverOptions (#3686) by @​naveenchander30
  • ConnMaxLifetimeJitter: Added jitter configuration to distribute connection expiration times and prevent thundering herd (#3666) by @​cyningsun
  • Digest Helper Functions: Added DigestString and DigestBytes helper functions for client-side xxh3 hashing compatible with Redis DIGEST command (#3679) by @​ofekshenawa
  • SMIGRATED New Format: Updated SMIGRATED parser to support new format and remember original host:port (#3697) by @​ndyakov
  • Cluster State Reload Interval: Added cluster state reload interval option for maintenance notifications (#3663) by @​ndyakov

🐛 Bug Fixes

  • PubSub nil pointer dereference: Fixed nil pointer dereference in PubSub after WithTimeout() - pubSubPool is now properly cloned (#3710) by @​Copilot
  • MaintNotificationsConfig nil check: Guard against nil MaintNotificationsConfig in initConn (#3707) by @​veeceey
  • wantConnQueue zombie elements: Fixed zombie wantConn elements accumulation in wantConnQueue (#3680) by @​cyningsun
  • XADD/XTRIM approx flag: Fixed XADD and XTRIM to use = when approx is false (#3684) by @​ndyakov
  • Sentinel timeout retry: When connection to a sentinel times out, attempt to connect to other sentinels (#3654) by @​cxljs

... (truncated)

Commits
  • 90faf06 chore(release): update versions in deps (#3712)
  • bf8e8e3 chore(release): v9.18.0 (#3711)
  • a881cd4 fix(clone): nil pointer dereference in PubSub after WithTimeout() (#3710)
  • ee6e9db feat(otel): Add OpenTelemetry Native Metrics Support (#3637)
  • b53f2b0 feat(sch): MaintNotifications for ClusterClient (#3643)
  • f25343d chore(tests): Add comprehensive TLS tests and example (#3681)
  • 33ca5cb feat(commands): Add support for Redis HOTKEYS commands (#3695)
  • 34f4568 fix(conn): guard against nil MaintNotificationsConfig in initConn (#3707)
  • 2fc030f perf(options): perf Fuzz Test Go File (#3692)
  • 63ed1fd Add support for Redis Streams Idempotent Production (#3693)
  • Additional commits viewable in compare view

Updates `github.com/tinylib/msgp` from 1.6.2 to 1.6.3
Release notes

Sourced from github.com/tinylib/msgp's releases.

v1.6.3

  • Remove debug println.

Full Changelog: https://github.com/tinylib/msgp/compare/v1.6.2...v1.6.3

Commits

Updates `github.com/valyala/fastjson` from 1.6.7 to 1.6.10
Commits
  • d652a1b Properly reset the cache (#118)
  • 4c89762 Properly clear references to external objects and byte slices on Arena.Reset(...
  • 87cb56e Add Arena.CloneString() function for cloning the string to the internal buffe...
  • 227c53b run go fix -rangeint
  • See full diff in compare view

Updates `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` from 0.65.0 to 0.67.0
Release notes

Sourced from go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc's releases.

v1.42.0/v2.4.0/v0.67.0/v0.36.0/v0.22.0/v0.17.0/v0.15.0/v0.14.0

Added

  • Add environment variables propagation carrier in go.opentelemetry.io/contrib/propagators/envcar. (#8442)

Changed

  • Upgrade go.opentelemetry.io/otel/semconv to v1.40.0, including updates across instrumentation and detector modules. (#8631)

    • The semantic conventions v1.40.0 release introduces RPC breaking changes applied in this repository:
      • RPC spans and metrics no longer include network.protocol.name, network.protocol.version, or network.transport attributes.
      • rpc.client.request.size, rpc.client.response.size, rpc.server.request.size, and rpc.server.response.size are no longer emitted in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
      • rpc.message span events and their message attributes are no longer emitted in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc (including when WithMessageEvents is configured).

    See semantic-conventions v1.40.0 release for complete details.

Fixed

  • Ignore informational response status codes (100-199) except 101 Switching Protocols when storing the HTTP status code in go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp and go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux. (#6913)
  • Make Body handling in Transport consistent with stdlib in go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8618)
  • Fix bucket boundaries for rpc.server.call.duration and rpc.client.call.duration in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8642)
  • Host resource detector in go.opentelemetry.io/contrib/otelconf now includes os. attributes. (#8578)

Removed

What's Changed

... (truncated)

Changelog

Sourced from go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc's changelog.

[1.42.0/2.4.0/0.67.0/0.36.0/0.22.0/0.17.0/0.15.0/0.14.0] - 2026-03-06

Added

  • Add environment variables propagation carrier in go.opentelemetry.io/contrib/propagators/envcar. (#8442)

Changed

  • Upgrade go.opentelemetry.io/otel/semconv to v1.40.0, including updates across instrumentation and detector modules. (#8631)

    • The semantic conventions v1.40.0 release introduces RPC breaking changes applied in this repository:
      • RPC spans and metrics no longer include network.protocol.name, network.protocol.version, or network.transport attributes.
      • rpc.client.request.size, rpc.client.response.size, rpc.server.request.size, and rpc.server.response.size are no longer emitted in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.
      • rpc.message span events and their message attributes are no longer emitted in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc (including when WithMessageEvents is configured).

    See semantic-conventions v1.40.0 release for complete details.

Fixed

  • Ignore informational response status codes (100-199) except 101 Switching Protocols when storing the HTTP status code in go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp and go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux. (#6913)
  • Make Body handling in Transport consistent with stdlib in go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8618)
  • Fix bucket boundaries for rpc.server.call.duration and rpc.client.call.duration in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8642)
  • Host resource detector in go.opentelemetry.io/contrib/otelconf now includes os. attributes. (#8578)

Removed

  • Drop support for [Go 1.24]. (#8628)

[1.41.0/2.3.0/0.66.0/0.35.0/0.21.0/0.16.0/0.14.0/0.13.0] - 2026-03-02

This release is the last to support [Go 1.24]. The next release will require at least [Go 1.25].

Added

  • Add WithSpanKind option in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc to override the default span kind. (#8506)
  • Add const Version in go.opentelemetry.io/contrib/bridges/otelzap. (#8544)
  • Support testing of [Go 1.26]. (#8549)
  • Add const Version in go.opentelemetry.io/contrib/detectors/autodetect. (#8555)
  • Add const Version in go.opentelemetry.io/contrib/detectors/azure/azurevm. (#8553)
  • Add const Version in go.opentelemetry.io/contrib/processors/baggagecopy. (#8557)
  • Add const Version in go.opentelemetry.io/contrib/detectors/aws/lambda. (#8510)
  • Add const Version in go.opentelemetry.io/contrib/propagators/autoprop. (#8488)
  • Add const Version in go.opentelemetry.io/c... _Description has been truncated_ --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> --- LICENSES/go.uber.org/atomic/LICENSE.txt | 19 ++++ go.mod | 60 ++++++----- go.sum | 136 +++++++++++++----------- route/otlp_trace_test.go | 2 +- 4 files changed, 123 insertions(+), 94 deletions(-) create mode 100644 LICENSES/go.uber.org/atomic/LICENSE.txt diff --git a/LICENSES/go.uber.org/atomic/LICENSE.txt b/LICENSES/go.uber.org/atomic/LICENSE.txt new file mode 100644 index 0000000000..8765c9fbc6 --- /dev/null +++ b/LICENSES/go.uber.org/atomic/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/go.mod b/go.mod index 6cbe637eef..f8ef965331 100644 --- a/go.mod +++ b/go.mod @@ -15,40 +15,40 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/honeycombio/dynsampler-go v0.6.4 github.com/honeycombio/hpsf v0.14.0 - github.com/honeycombio/husky v0.41.0 + github.com/honeycombio/husky v0.43.0 github.com/honeycombio/libhoney-go v1.26.0 github.com/jessevdk/go-flags v1.6.1 github.com/jonboulle/clockwork v0.5.0 github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.18.2 - github.com/open-telemetry/opamp-go v0.22.0 - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.145.0 + github.com/klauspost/compress v1.18.4 + github.com/open-telemetry/opamp-go v0.23.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 github.com/panmari/cuckoofilter v1.0.6 github.com/pelletier/go-toml/v2 v2.2.4 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 - github.com/redis/go-redis/v9 v9.17.3 + github.com/redis/go-redis/v9 v9.18.0 github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 - github.com/tinylib/msgp v1.6.2 - github.com/valyala/fastjson v1.6.7 + github.com/tinylib/msgp v1.6.3 + github.com/valyala/fastjson v1.6.10 github.com/vmihailenco/msgpack/v5 v5.4.1 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 - go.opentelemetry.io/otel/metric v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/sdk/metric v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 + go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/sdk/metric v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 go.opentelemetry.io/proto/otlp v1.9.0 golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -62,7 +62,7 @@ require ( github.com/hashicorp/go-version v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 // indirect github.com/otiai10/copy v1.10.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -72,9 +72,11 @@ require ( github.com/stretchr/objx v0.5.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/featuregate v1.51.0 // indirect + go.opentelemetry.io/collector/featuregate v1.53.0 // indirect + go.opentelemetry.io/collector/pdata/xpdata v0.147.0 // indirect go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 // indirect go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sync v0.19.0 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect @@ -95,7 +97,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -107,15 +109,15 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - go.opentelemetry.io/collector/pdata v1.51.0 + go.opentelemetry.io/collector/pdata v1.53.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.31.0 - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.33.0 // indirect - golang.org/x/tools v0.40.0 - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + golang.org/x/mod v0.32.0 + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.41.0 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect ) tool github.com/google/go-licenses/v2 diff --git a/go.sum b/go.sum index 3eb28ed3bf..3a0d1e634d 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -122,8 +122,8 @@ github.com/honeycombio/dynsampler-go v0.6.4 h1:EM3FXN2Lfmso41MRMmSvRynMrz+AHiRff github.com/honeycombio/dynsampler-go v0.6.4/go.mod h1:M5YYNOfxRrBlEWDatTlHMYo5F7GjwVnptx5z+uXIVMo= github.com/honeycombio/hpsf v0.14.0 h1:LeQbDuT+aVmiJnWp9Kqb9Qqz5OZcjDk85RMzzwKtCKI= github.com/honeycombio/hpsf v0.14.0/go.mod h1:VyPjyn1GViOiCrpBbPZCkEJnuDuSTUpU8LV5CWVTQm4= -github.com/honeycombio/husky v0.41.0 h1:6iuC3FJpU2xZUveLFGAWvDP/Xp9Vnt1vMgwu2UCeQfA= -github.com/honeycombio/husky v0.41.0/go.mod h1:kgwFQfPCC82f5BxuBb8BAuuC1Q7e5NK7EVsjcjztuXo= +github.com/honeycombio/husky v0.43.0 h1:L2LcKG9vHuTu/tbfHoWkGCs+93cVf9wLKGGq/4u35gk= +github.com/honeycombio/husky v0.43.0/go.mod h1:lQ1VzGZxeYPCr4zxmak1lVe29HJFqJ6bQXWCl0ZqlNg= github.com/honeycombio/libhoney-go v1.26.0 h1:fdwS7c/5h6ifJqQZ178nm4UEZha04GTbwJMZ7xkShhk= github.com/honeycombio/libhoney-go v1.26.0/go.mod h1:cR+t7pq9heP00+1/+TNWCrAfjSA74xKWI8YGOANlzYY= github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat h1:g6pUF6IZVLG93vZbUefK0qF20CGx0zf0q3n3Fw4gv1s= @@ -136,8 +136,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -157,14 +159,14 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/open-telemetry/opamp-go v0.22.0 h1:7UnsQgFFS7ffM09JQk+9aGVBAAlsLfcooZ9xvSYwxWM= -github.com/open-telemetry/opamp-go v0.22.0/go.mod h1:339N71soCPrhHywbAcKUZJDODod581ZOxCpTkrl3zYQ= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.145.0 h1:lbxy2bYh3v0YIyqd/JVttEwYlC7yU5o3JU2N/m5Qnq8= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.145.0/go.mod h1:kGlLjX8CJSE+9SfLARgaXTFBuAvNadjLvPsHO7fcVeE= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.145.0 h1:0ithmsGyVtjzODmAPp9pkxA4IlnYpyeXmDWrryTkHNo= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.145.0/go.mod h1:r+K/aCWpUCDDM5Gisznf9ZQjpZcyFr84CuATA9486JQ= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 h1:sB4yuYx45zig1ceQ+kmrEYy0xMZ+mGagwYIFtJkkU1w= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0/go.mod h1:uLhceuH7ZtiVxk+B0MHI0vhJG2Y4aOzT/hrV6c5KjVU= +github.com/open-telemetry/opamp-go v0.23.0 h1:k7h7w/muprut9/DAhUC4anX4v7hIdgO02gIsSjV4uq0= +github.com/open-telemetry/opamp-go v0.23.0/go.mod h1:DIIVdkLefdqPW5L+4I2twmAicVrTB0Bp5XJAfedZzAM= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0 h1:nxCNHHUItl2j0sjknI/mRbBBcQCxu0yv3baii9GNB1U= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0/go.mod h1:LrW8KarPjlu+1VdP2t6kjJeOTF+y3/n2wCZAdc/NWg0= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 h1:jgmHvcC3WCrkA49VBm/Tay6O5OEaLvevlqd+OEoPI3M= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0/go.mod h1:NMXNbNZ4aEhyW0Oe4BfbGLLOI8Y9FmB/unZp01HUlKU= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 h1:EYy8gmyjGLS0iYV7ksOOHrjZgiTjbWU26vziBAt4jKI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0/go.mod h1:VDqy65biIJI9iYN9rrVi2nm9KAvSfq+6Fzrm8WyL4Qw= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 h1:lFowWhr/qx5Gm2X8H0BbG87xZh/e+4S0PQw8HQO5D4Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0/go.mod h1:JybcaNLHHzJQh690eSp+KDbLrxB1+AhKNLlibqrogt4= github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ= @@ -194,8 +196,8 @@ github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8A github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rdleal/go-priorityq v0.0.0-20240324224830-28716009213d h1:OuC714/HtVeMJo6Y1mRkeuDmu3t+F0cgh6qPDGqLmqI= github.com/rdleal/go-priorityq v0.0.0-20240324224830-28716009213d/go.mod h1:X4AAZOixX/7z5rgQkIkMa72A0++MLRke9nipxYUg+8E= -github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= -github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -229,46 +231,50 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tinylib/msgp v1.6.2 h1:D40LN895O9HJpN8n5Ksqk+abl7zw6RtizDwgRCE7hXk= -github.com/tinylib/msgp v1.6.2/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= -github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= -github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= +github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw= -go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo= -go.opentelemetry.io/collector/internal/testutil v0.145.0 h1:H/KL0GH3kGqSMKxZvnQ0B0CulfO9xdTg4DZf28uV7fY= -go.opentelemetry.io/collector/internal/testutil v0.145.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c= -go.opentelemetry.io/collector/pdata v1.51.0 h1:DnDhSEuDXNdzGRB7f6oOfXpbDApwBX3tY+3K69oUrDA= -go.opentelemetry.io/collector/pdata v1.51.0/go.mod h1:GoX1bjKDR++mgFKdT7Hynv9+mdgQ1DDXbjs7/Ww209Q= -go.opentelemetry.io/collector/pdata/pprofile v0.145.0 h1:ASMKpoqokf8HhzjoeMKZf0K6UXLhufVwNXH0sSuUn5w= -go.opentelemetry.io/collector/pdata/pprofile v0.145.0/go.mod h1:a60GC7wQPhLAixWzKbbP51QLwwc+J0Cmp4SurOlhGUk= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= +go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= +go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU= +go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= +go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= +go.opentelemetry.io/collector/pdata/pprofile v0.147.0 h1:yQS3RBvcvRcy9N7AnJvsxmse0AxJcRqBZfwMA22xBA8= +go.opentelemetry.io/collector/pdata/pprofile v0.147.0/go.mod h1:pm9mUqHNpT1SaCkxILu4FW1BvMAelh7EKhpSKe2KJIQ= +go.opentelemetry.io/collector/pdata/xpdata v0.147.0 h1:JZPYCIrIhmpmUJ1SNkGv13LQykBPY9eLpC+kQm8fex0= +go.opentelemetry.io/collector/pdata/xpdata v0.147.0/go.mod h1:w3iv1rH00eMB/7lYBn9dDJuYujJUpgca5Zoz3KDLgrc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 h1:40vBjolEOioNBl8zPj1wxqlA7kJ82RxR4HnUv7W8zRI= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0/go.mod h1:4wAsc1dEVb4D1ZykBNC9AriTU9uLYtmziLrB+7G4lb4= go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 h1:yXinc284C6bmzA1r9jk7MxAhrBIIOH3qwmqwBmylZrA= @@ -279,6 +285,8 @@ go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -293,16 +301,16 @@ golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aC golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -313,19 +321,19 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= @@ -334,17 +342,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/route/otlp_trace_test.go b/route/otlp_trace_test.go index a57b36ac64..ab9d99b836 100644 --- a/route/otlp_trace_test.go +++ b/route/otlp_trace_test.go @@ -633,7 +633,7 @@ func TestOTLPHandler(t *testing.T) { event := events[0] // Note: GRPC clients override the user-agent header with their own value. // This is expected behavior and differs from HTTP where custom user-agents are preserved. - assert.Equal(t, "grpc-go/1.78.0", event.Data.MetaRefineryIncomingUserAgent) + assert.Equal(t, "grpc-go/1.79.3", event.Data.MetaRefineryIncomingUserAgent) }) t.Run("spans record incoming user agent - HTTP", func(t *testing.T) { From dab7477604434bf5f7fb4c2b9a116ab25f5ef78e Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:53:09 -0400 Subject: [PATCH 11/35] maint: prep for v3.1.2 release (#1800) ## Which problem is this PR solving? Prepare for v3.1.2 release ## Short description of the changes - update changelog, release notes, and update metric doc --- CHANGELOG.md | 12 ++++++++++++ RELEASE_NOTES.md | 8 ++++++++ metrics.md | 3 +++ tools/convert/metricsMeta.yaml | 12 ++++++++++++ 4 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10acfd4d8..7acc7d9acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Refinery Changelog +## 3.1.2 2026-03-25 + +This release addresses security vulnerabilities CVE-2026-27139, CVE-2026-27142, and CVE-2026-25679. + +### Features + +- feat: add capacity/limit companion metrics for queues and memory by @mterhar in https://github.com/honeycombio/refinery/pull/1799 + +### Maintenance + +- maint(deps): bump the minor-patch group across 1 directory with 21 updates by @dependabot in https://github.com/honeycombio/refinery/pull/1795 + ## 3.1.1 2026-02-25 ### Features diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2e25ca23f1..cf4695e40e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,14 @@ While [CHANGELOG.md](./CHANGELOG.md) contains detailed documentation and links to all the source code changes in a given release, this document is intended to be aimed at a more comprehensible version of the contents of the release from the point of view of users of Refinery. +## Version 3.1.2 + +This patch release primarily addresses security vulnerabilities in dependencies. + +### Maintenance + +- Updated dependencies to address security vulnerabilities CVE-2026-27139, CVE-2026-27142, and CVE-2026-25679. + ## Version 3.1.1 This patch release includes bug fixes and a new feature for configuring additional HTTP headers. diff --git a/metrics.md b/metrics.md index 4be5fcf988..56c4234a70 100644 --- a/metrics.md +++ b/metrics.md @@ -34,11 +34,14 @@ This table includes metrics with fully defined names. | trace_span_count | Histogram | Dimensionless | number of spans in a trace | | collector_incoming_queue | Histogram | Dimensionless | number of spans currently in the incoming queue | | collector_peer_queue_length | Gauge | Dimensionless | number of spans in the peer queue | +| collector_peer_queue_capacity | Gauge | Dimensionless | configured maximum number of spans in the peer queue | | collector_incoming_queue_length | Gauge | Dimensionless | number of spans in the incoming queue | +| collector_incoming_queue_capacity | Gauge | Dimensionless | configured maximum number of spans in the incoming queue | | collector_peer_queue | Histogram | Dimensionless | number of spans currently in the peer queue | | collector_cache_size | Gauge | Dimensionless | number of traces currently stored in the trace cache | | collect_cache_entries | Histogram | Dimensionless | Total number of traces currently stored in the cache from all workers | | memory_heap_allocation | Gauge | Bytes | current heap allocation | +| memory_limit | Gauge | Bytes | configured maximum memory allocation for the collector (derived from MaxAlloc or AvailableMemory * MaxMemoryPercentage) | | span_received | Counter | Dimensionless | number of spans received by the collector | | span_processed | Counter | Dimensionless | number of spans processed by the collector | | spans_waiting | UpDown | Dimensionless | number of spans waiting to be processed by the collector | diff --git a/tools/convert/metricsMeta.yaml b/tools/convert/metricsMeta.yaml index 67fadc5de3..d7a9db8d39 100644 --- a/tools/convert/metricsMeta.yaml +++ b/tools/convert/metricsMeta.yaml @@ -87,10 +87,18 @@ complete: type: Gauge unit: Dimensionless description: number of spans in the peer queue + - name: collector_peer_queue_capacity + type: Gauge + unit: Dimensionless + description: configured maximum number of spans in the peer queue - name: collector_incoming_queue_length type: Gauge unit: Dimensionless description: number of spans in the incoming queue + - name: collector_incoming_queue_capacity + type: Gauge + unit: Dimensionless + description: configured maximum number of spans in the incoming queue - name: collector_peer_queue type: Histogram unit: Dimensionless @@ -107,6 +115,10 @@ complete: type: Gauge unit: Bytes description: current heap allocation + - name: memory_limit + type: Gauge + unit: Bytes + description: configured maximum memory allocation for the collector (derived from MaxAlloc or AvailableMemory * MaxMemoryPercentage) - name: span_received type: Counter unit: Dimensionless From 8e92d04ca714e198eadbd69a6d8e865558c65669 Mon Sep 17 00:00:00 2001 From: Davin Date: Tue, 7 Apr 2026 15:20:40 -0500 Subject: [PATCH 12/35] fix: update ko build tooling and fix flaky integration test (#1806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Bumps ko from v0.11.2 to v0.18.0 for local image builds - Fixes ko release asset arch suffix for v0.18.0 compatibility (`x86_64`) - Replaces racy `time.Sleep(15ms)` in integration tests with TCP readiness polling, fixing intermittent test failures Split from PR #1798 as independent infrastructure improvements. ## Test plan - [ ] `make local_image` builds successfully with ko v0.18.0 - [ ] Integration tests pass reliably without flaky timing issues - [ ] `go vet ./...` passes - [ ] All existing tests continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- Makefile | 6 +----- app/app_test.go | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 189fcd6ccf..bc5b4cf28d 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ wait_for_redis: dockerize # You can override this version from an environment variable. HOST_OS := $(shell uname -s | tr A-Z a-z) # You can override this version from an environment variable. -KO_VERSION ?= 0.11.2 +KO_VERSION ?= 0.18.0 KO_RELEASE_ASSET := ko_${KO_VERSION}_${HOST_OS}_x86_64.tar.gz # ensure the ko command is available ko: ko_${KO_VERSION}.tar.gz @@ -189,7 +189,3 @@ unsmoke: @echo "+++ Spinning down the smokers." @echo "" cd smoke-test && docker-compose down --volumes - - - - diff --git a/app/app_test.go b/app/app_test.go index 1ac3a737cd..cff02a5e6c 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -382,10 +382,19 @@ func newStartedApp( assert.NoError(t, err) err = startstop.Start(g.Objects(), nil) - assert.NoError(t, err) + require.NoError(t, err) + + // Wait for the HTTP server to be ready by polling the listen address. + listenAddr := c.GetListenAddr() + require.Eventually(t, func() bool { + conn, err := net.DialTimeout("tcp", listenAddr, 50*time.Millisecond) + if err != nil { + return false + } + conn.Close() + return true + }, 2*time.Second, 10*time.Millisecond, "server failed to start listening on %s", listenAddr) - // Racy: wait just a moment for ListenAndServe to start up. - time.Sleep(15 * time.Millisecond) return &a, g } From f2452f21abcbb1b17a8edefa635bd2045df29123 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:51:19 -0400 Subject: [PATCH 13/35] fix: include AdditionalErrorFields in logs for transmission code (#1807) ## Which problem is this PR solving? #1802 ## Short description of the changes - Make sure that all error logs for sending events are populated with the default fields `dataset`, `apihost`, and `environment`. - Make sure all error logs for sending events has the AdditionalErrorFields added. --- transmit/direct_transmit.go | 63 +++++++++++-------- transmit/direct_transmit_test.go | 103 +++++++++++++++++++++++++++---- 2 files changed, 129 insertions(+), 37 deletions(-) diff --git a/transmit/direct_transmit.go b/transmit/direct_transmit.go index 46d36ec572..beeb73b4a2 100644 --- a/transmit/direct_transmit.go +++ b/transmit/direct_transmit.go @@ -316,24 +316,19 @@ func (d *DirectTransmission) Stop() error { return nil } -// handleBatchFailure handles metrics updates when the entire batch fails -func (d *DirectTransmission) handleBatchFailure(batch []*types.Event) { - d.Metrics.Increment(d.metricKeys.counterSendErrors) - for range batch { - d.Metrics.Down(d.metricKeys.updownQueuedItems) - } -} - -// handleEventError logs an error and updates metrics for a single event -func (d *DirectTransmission) handleEventError(ev *types.Event, statusCode int, queueTime int64, errorMsg string, responseBody []byte) { +// handleError logs an error with common fields and custom message +func (d *DirectTransmission) handleError(ev *types.Event, statusCode int, queueTime int64, errorMsg string, responseBody []byte, logMessage string) { log := d.Logger.Error().WithFields(map[string]any{ - "status_code": statusCode, "api_host": ev.APIHost, "dataset": ev.Dataset, "environment": ev.Environment, "roundtrip_usec": queueTime, }) + if statusCode > 0 { + log = log.WithField("status_code", statusCode) + } + if errorMsg != "" { log = log.WithField("error", errorMsg) } @@ -350,7 +345,28 @@ func (d *DirectTransmission) handleEventError(ev *types.Event, statusCode int, q } } - log.Logf("error when sending event") + log.Logf(logMessage) +} + +// handleBatchFailure handles metrics updates when the entire batch fails +func (d *DirectTransmission) handleBatchFailure(batch []*types.Event, errorMsg string, logMessage string) { + d.Metrics.Increment(d.metricKeys.counterSendErrors) + if len(batch) > 0 { + queueTime := time.Now().UnixMicro() - batch[0].EnqueuedUnixMicro + d.handleError(batch[0], 0, queueTime, errorMsg, nil, logMessage) + } + + for range batch { + d.Metrics.Down(d.metricKeys.updownQueuedItems) + } +} + +// handleEventError logs an error and updates metrics for a single event +func (d *DirectTransmission) handleEventError(ev *types.Event, statusCode int, queueTime int64, errorMsg string, responseBody []byte, logMessage string) { + if logMessage == "" { + logMessage = "error when sending event" + } + d.handleError(ev, statusCode, queueTime, errorMsg, responseBody, logMessage) d.Metrics.Increment(d.metricKeys.counterResponseErrors) d.Metrics.Down(d.metricKeys.updownQueuedItems) d.Metrics.Histogram(d.metricKeys.histogramQueueTime, float64(queueTime)) @@ -407,9 +423,7 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { if err != nil { // Skip this message and remove it from the list, so we don't // try to account for it again. - d.Logger.Error().WithField("err", err.Error()).Logf("failed to marshal event") - d.Metrics.Down(d.metricKeys.updownQueuedItems) - d.Metrics.Increment(d.metricKeys.counterResponseErrors) + d.handleEventError(wholeBatch[i], 0, time.Now().UnixMicro()-wholeBatch[i].EnqueuedUnixMicro, err.Error(), nil, "failed to marshal event") continue } if len(newPacked) > apiMaxBatchSize { @@ -440,8 +454,7 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { apiURL, err := buildRequestURL(apiHost, dataset) if err != nil { - d.Logger.Error().WithField("err", err.Error()).Logf("failed to create request URL") - d.handleBatchFailure(subBatch) + d.handleBatchFailure(subBatch, err.Error(), "failed to create request URL") continue } @@ -471,8 +484,7 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { req, err = http.NewRequest("POST", apiURL, readerPtr) if err != nil { - d.Logger.Error().WithField("err", err.Error()).Logf("failed to create request") - d.handleBatchFailure(subBatch) + d.handleBatchFailure(subBatch, err.Error(), "failed to create request") break } @@ -523,12 +535,10 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { dequeuedAt := d.Clock.Now() if err != nil { - d.Logger.Error().WithField("err", err.Error()).Logf("http POST failed") - // Network/connection error - affects all events in batch for _, ev := range subBatch { queueTime := dequeuedAt.UnixMicro() - ev.EnqueuedUnixMicro - d.handleEventError(ev, 0, queueTime, err.Error(), nil) + d.handleEventError(ev, 0, queueTime, err.Error(), nil, "") } continue } @@ -544,15 +554,18 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { if resp.Header.Get("Content-Type") == "application/msgpack" { err = msgpack.NewDecoder(resp.Body).Decode(&batchResponses) if err != nil { + // This is an error from processing response body, not an error from sending events. No need to include event information here d.Logger.Error().WithField("err", err.Error()).Logf("failed to decode msgpack batch response") } } else { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { + // This is an error from processing response body, not an error from sending events. No need to include event information here d.Logger.Error().WithField("err", err.Error()).Logf("failed to read response body") } else { err = json.Unmarshal(bodyBytes, &batchResponses) if err != nil { + // This is an error from processing response body, not an error from sending events. No need to include event information here d.Logger.Error().WithField("err", err.Error()).Logf("failed to decode JSON batch response") } } @@ -569,12 +582,12 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { // Check if we have a response for this event if i >= len(batchResponses) { // Missing response - treat as server error - d.handleEventError(ev, http.StatusInternalServerError, queueTime, "insufficient responses from server", nil) + d.handleEventError(ev, http.StatusInternalServerError, queueTime, "insufficient responses from server", nil, "insufficient responses from server") continue } if batchResponses[i].Status != http.StatusAccepted { - d.handleEventError(ev, batchResponses[i].Status, queueTime, "", nil) + d.handleEventError(ev, batchResponses[i].Status, queueTime, "", nil, "") } else { // Success d.Metrics.Increment(d.metricKeys.counterResponse20x) @@ -610,7 +623,7 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { for _, ev := range subBatch { queueTime := dequeuedAt.UnixMicro() - ev.EnqueuedUnixMicro - d.handleEventError(ev, resp.StatusCode, queueTime, "", bodyBytes) + d.handleEventError(ev, resp.StatusCode, queueTime, "", bodyBytes, "") } } } diff --git a/transmit/direct_transmit_test.go b/transmit/direct_transmit_test.go index 3cbe9aa9c7..2cb185d34e 100644 --- a/transmit/direct_transmit_test.go +++ b/transmit/direct_transmit_test.go @@ -239,6 +239,12 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { defer errorServer.Close() dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + + // Configure AdditionalErrorFields + dt.Config = &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + // Send 4 events to ensure we get 2 successes and 2 errors sendTestEvents(dt, errorServer.URL, 4, "test-api-key") err := dt.Stop() @@ -267,6 +273,9 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, "test-dataset", errorEvent.Fields["dataset"]) assert.Equal(t, "test", errorEvent.Fields["environment"]) assert.Contains(t, errorEvent.Fields, "roundtrip_usec") + + // Verify AdditionalErrorFields + assert.Contains(t, errorEvent.Fields, "event_id") } }) @@ -280,6 +289,12 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { defer errorServer.Close() dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + + // Configure AdditionalErrorFields + dt.Config = &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + sendTestEvents(dt, errorServer.URL, 2, "test-api-key") err := dt.Stop() require.NoError(t, err) @@ -303,6 +318,9 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, "error when sending event", errorEvent.Fields["error"]) assert.Equal(t, http.StatusInternalServerError, errorEvent.Fields["status_code"]) assert.Contains(t, errorEvent.Fields, "response_body") + + // Verify AdditionalErrorFields + assert.Contains(t, errorEvent.Fields, "event_id") } }) @@ -365,7 +383,13 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { })) defer msgpackServer.Close() - dt, mockMetrics, _ := setupDirectTransmissionTest(t) + dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + + // Configure AdditionalErrorFields + dt.Config = &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + sendTestEvents(dt, msgpackServer.URL, 2, "test-api-key") err := dt.Stop() require.NoError(t, err) @@ -380,6 +404,21 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, float64(1), errors) assert.Equal(t, float64(1), batchesSent) // Single batch containing 2 events assert.Equal(t, float64(2), messagesSent) + + // Verify error log has all expected fields + errorEvents := getErrorEvents(mockLogger) + require.Len(t, errorEvents, 1, "Expected one error log for rejected event") + + errorEvent := errorEvents[0] + assert.Equal(t, "error when sending event", errorEvent.Fields["error"]) + assert.Equal(t, http.StatusBadRequest, errorEvent.Fields["status_code"]) + assert.Equal(t, msgpackServer.URL, errorEvent.Fields["api_host"]) + assert.Equal(t, "test-dataset", errorEvent.Fields["dataset"]) + assert.Equal(t, "test", errorEvent.Fields["environment"]) + assert.Contains(t, errorEvent.Fields, "roundtrip_usec") + + // Verify AdditionalErrorFields + assert.Contains(t, errorEvent.Fields, "event_id") }) t.Run("insufficient responses from server", func(t *testing.T) { @@ -393,6 +432,12 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { defer insufficientServer.Close() dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + + // Configure AdditionalErrorFields + dt.Config = &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + sendTestEvents(dt, insufficientServer.URL, 2, "test-api-key") err := dt.Stop() require.NoError(t, err) @@ -408,14 +453,20 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, float64(1), batchesSent) // Single batch containing 2 events assert.Equal(t, float64(2), messagesSent) - // Verify error log message mentions insufficient responses + // Verify error log has all expected fields errorEvents := getErrorEvents(mockLogger) require.Len(t, errorEvents, 1, "Expected exactly one error log for the missing response") errorEvent := errorEvents[0] - assert.Equal(t, "error when sending event", errorEvent.Fields["error"]) + assert.Equal(t, "insufficient responses from server", errorEvent.Fields["error"]) assert.Equal(t, http.StatusInternalServerError, errorEvent.Fields["status_code"]) + assert.Equal(t, insufficientServer.URL, errorEvent.Fields["api_host"]) + assert.Equal(t, "test-dataset", errorEvent.Fields["dataset"]) + assert.Equal(t, "test", errorEvent.Fields["environment"]) assert.Contains(t, errorEvent.Fields, "roundtrip_usec") + + // Verify AdditionalErrorFields + assert.Contains(t, errorEvent.Fields, "event_id") }) t.Run("response decode errors", func(t *testing.T) { @@ -427,7 +478,13 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { })) defer decodeErrorServer.Close() - dt, mockMetrics, _ := setupDirectTransmissionTest(t) + dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + + // Configure AdditionalErrorFields + dt.Config = &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + sendTestEvents(dt, decodeErrorServer.URL, 1, "test-api-key") err := dt.Stop() require.NoError(t, err) @@ -440,6 +497,16 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, float64(1), decodeErrors) assert.Equal(t, float64(1), batchesSent) assert.Equal(t, float64(1), messagesSent) + + // Verify decode error log has context fields + var foundErrorLog bool + for _, event := range mockLogger.Events { + if msg, ok := event.Fields["error"].(string); ok && strings.Contains(msg, "failed to decode msgpack batch response") { + foundErrorLog = true + break + } + } + require.True(t, foundErrorLog, "Expected decode error log") }) t.Run("event over 1M size", func(t *testing.T) { @@ -453,8 +520,14 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { dt, mockMetrics, mockLogger := setupDirectTransmissionTest(t) + // Configure AdditionalErrorFields + mockCfg := &config.MockConfig{ + AdditionalErrorFields: []string{"event_id"}, + } + dt.Config = mockCfg + // Create an event with data over 1M - eventData := types.NewPayload(&config.MockConfig{}, map[string]any{ + eventData := types.NewPayload(mockCfg, map[string]any{ "large_field": strings.Repeat("a", 1024*1024+1000), "event_id": 1, }) @@ -480,15 +553,21 @@ func TestDirectTransmissionErrorHandling(t *testing.T) { assert.Equal(t, float64(0), success) assert.Equal(t, float64(1), errors) - // Verify error log message about oversized event - var oversizedFound bool + // Verify error log has all expected fields + var oversizedLog *logger.MockLoggerEvent for _, event := range mockLogger.Events { - if errorMsg, ok := event.Fields["err"].(string); ok && strings.Contains(errorMsg, "exceeds max event size") { - oversizedFound = true + if msg, ok := event.Fields["error"].(string); ok && strings.Contains(msg, "failed to marshal event") { + oversizedLog = event break } } - require.True(t, oversizedFound, "Expected error log for oversized event") + require.NotNil(t, oversizedLog, "Expected error log for oversized event") + + assert.Equal(t, server.URL, oversizedLog.Fields["api_host"]) + assert.Equal(t, "test-dataset", oversizedLog.Fields["dataset"]) + assert.Equal(t, "test", oversizedLog.Fields["environment"]) + assert.Contains(t, oversizedLog.Fields, "roundtrip_usec") + assert.Contains(t, oversizedLog.Fields, "error") }) } @@ -728,7 +807,7 @@ func TestDirectTransmission(t *testing.T) { // Verify all events were queued and dequeued, net = 0 assert.Equal(t, float64(0), queuedItems) // Verify queue time histogram was updated for all events - assert.Equal(t, expectedEvents, mockMetrics.GetHistogramCount(dt.metricKeys.histogramQueueTime)) + assert.Equal(t, len(allEvents), mockMetrics.GetHistogramCount(dt.metricKeys.histogramQueueTime)) // Verify batch and message counts // Dataset A: 5 events -> 2 batches (3+2) @@ -815,7 +894,7 @@ func TestDirectTransmissionBatchSizeLimit(t *testing.T) { assert.Equal(t, float64(expectedEvents), success) assert.Equal(t, float64(len(allEvents)-expectedEvents), errors) assert.Equal(t, float64(0), queuedItems) - assert.Equal(t, expectedEvents, mockMetrics.GetHistogramCount(dt.metricKeys.histogramQueueTime)) + assert.Equal(t, len(allEvents), mockMetrics.GetHistogramCount(dt.metricKeys.histogramQueueTime)) // Verify batch and message counts - events are large so batches will be smaller assert.Greater(t, batchesSent, float64(0), "Should have sent at least one batch") From b2aed2319e9461d9136194ae73e177ecfc5d02e1 Mon Sep 17 00:00:00 2001 From: Davin Date: Thu, 9 Apr 2026 11:10:07 -0500 Subject: [PATCH 14/35] feat: add ReceiveKeyIDs config option for key ID-based authorization (#1803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `AccessKeys.ReceiveKeyIDs` config option allowing Refinery to authorize traffic by Honeycomb ingest key IDs (from `/1/auth` endpoint) instead of requiring full API keys - Updates `IsAccepted()` to check both `ReceiveKeys` (full keys) and `ReceiveKeyIDs` (key IDs) simultaneously - Enables live reload for both `ReceiveKeys` and `ReceiveKeyIDs` config fields - Adds benchmarks for the `IsAccepted` hot path with various list sizes This is part of the RaaS (Refinery as a Service) feature set, split from PR #1798. Closes FRE-6 ## Test plan - [x] Unit tests pass for `IsAccepted()` with key IDs, full keys, and mixed scenarios - [x] Benchmarks verify performance of key lookup with 10/100 keys - [x] Config validation passes (`make validate` in `tools/convert/`) - [x] `go vet ./...` passes - [x] All existing tests continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 --- config.md | 23 +++- config/file_config.go | 28 +++-- config/file_config_test.go | 170 +++++++++++++++++++++++--- config/metadata/configMeta.yaml | 35 +++++- config_complete.yaml | 29 ++++- metrics.md | 2 +- refinery_config.md | 21 +++- route/middleware.go | 8 +- route/otlp_logs.go | 16 ++- route/otlp_logs_test.go | 4 +- route/otlp_trace.go | 20 ++- route/otlp_trace_test.go | 6 +- route/route.go | 68 ++++++++--- route/route_test.go | 34 +++--- rules.md | 2 +- tools/convert/configDataNames.txt | 4 +- tools/convert/minimal_config.yaml | 5 +- tools/convert/templates/configV2.tmpl | 28 ++++- 18 files changed, 397 insertions(+), 106 deletions(-) diff --git a/config.md b/config.md index fb1b4ca340..f70c0a782b 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-04-08 at 21:59:38 UTC. ## The Config file @@ -181,16 +181,31 @@ ReceiveKeys is a set of Honeycomb API keys that the proxy will treat specially. This list only applies to span traffic - other Honeycomb API actions will be proxied through to the upstream API directly without modifying keys. -- Not eligible for live reload. +- Eligible for live reload. - Type: `stringarray` - Example: `your-key-goes-here` +### `ReceiveKeyIDs` + +ReceiveKeyIDs is a set of Honeycomb Ingest Key IDs that the proxy will treat specially. + +When `AcceptOnlyListedKeys` is `true`, traffic using an API key whose Honeycomb ingest key ID matches an entry in this list will be accepted. +The key ID is the `id` field returned by the Honeycomb `/1/auth` endpoint; it is distinct from the full API key value. +This allows authorization based on key IDs rather than full key values, which avoids storing secret key material in the configuration file. +Both `ReceiveKeys` and `ReceiveKeyIDs` may be used simultaneously. +Note: This feature does not support legacy API keys. +Only Honeycomb Ingest Keys (which have a key ID) are compatible with this setting. + +- Eligible for live reload. +- Type: `stringarray` +- Example: `your-key-id-goes-here` + ### `AcceptOnlyListedKeys` AcceptOnlyListedKeys is a boolean flag that causes events arriving with API keys not in the `ReceiveKeys` list to be rejected. -If `true`, then only traffic using the keys listed in `ReceiveKeys` is accepted. -Events arriving with API keys not in the `ReceiveKeys` list will be rejected with an HTTP `401` error. +If `true`, then only traffic using the keys listed in `ReceiveKeys` or whose key ID is listed in `ReceiveKeyIDs` is accepted. +Events arriving with API keys not in either list will be rejected with an HTTP `401` error. If `false`, then all traffic is accepted and `ReceiveKeys` is ignored. This setting is applied **before** the `SendKey` and `SendKeyMode` settings. diff --git a/config/file_config.go b/config/file_config.go index 43206dae90..4a215910fe 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -92,16 +92,21 @@ type NetworkConfig struct { type AccessKeyConfig struct { ReceiveKeys []string `yaml:"ReceiveKeys" default:"[]"` + ReceiveKeyIDs []string `yaml:"ReceiveKeyIDs" default:"[]"` SendKey string `yaml:"SendKey" cmdenv:"SendKey"` SendKeyMode string `yaml:"SendKeyMode" default:"none"` AcceptOnlyListedKeys bool `yaml:"AcceptOnlyListedKeys"` } -// IsAccepted checks if the given key is in the list of received keys or a configured SendKey. -// if not, it returns an error with the key truncated to 8 characters for logging. -func (a *AccessKeyConfig) IsAccepted(key string) error { +// IsAccepted checks if the given key (or its associated key ID) is authorized. +// keyID is the Honeycomb ingest key ID returned by the /1/auth endpoint; it may +// be empty if the lookup has not yet occurred or if the key is a legacy key. +// If not accepted, it returns an error with the key truncated to 8 characters for logging. +func (a *AccessKeyConfig) IsAccepted(key, keyID string) error { if a.AcceptOnlyListedKeys { - if (len(a.SendKey) > 0 && key == a.SendKey) || slices.Contains(a.ReceiveKeys, key) { + if (len(a.SendKey) > 0 && key == a.SendKey) || + slices.Contains(a.ReceiveKeys, key) || + (keyID != "" && slices.Contains(a.ReceiveKeyIDs, keyID)) { return nil } @@ -110,10 +115,15 @@ func (a *AccessKeyConfig) IsAccepted(key string) error { return nil } +// HasKeyIDs returns true if ReceiveKeyIDs has been configured. +func (a *AccessKeyConfig) HasKeyIDs() bool { + return len(a.ReceiveKeyIDs) > 0 +} + // GetReplaceKey checks the given API key against the configuration // and possibly replaces it with the configured SendKey, if the settings so indicate. // It returns the key to use, or an error if the key is invalid given the settings. -func (a *AccessKeyConfig) GetReplaceKey(apiKey string) (string, error) { +func (a *AccessKeyConfig) GetReplaceKey(apiKey, keyID string) (string, error) { if a.SendKey != "" { overwriteWith := "" switch a.SendKeyMode { @@ -129,10 +139,10 @@ func (a *AccessKeyConfig) GetReplaceKey(apiKey string) (string, error) { overwriteWith = a.SendKey } case "listedonly": - // only replace keys that are listed in the `ReceiveKeys` list, + // only replace keys that are listed in the `ReceiveKeys` or `ReceiveKeyIDs` list, // otherwise use original key overwriteWith = apiKey - if slices.Contains(a.ReceiveKeys, apiKey) { + if slices.Contains(a.ReceiveKeys, apiKey) || (keyID != "" && slices.Contains(a.ReceiveKeyIDs, keyID)) { overwriteWith = a.SendKey } case "missingonly": @@ -143,11 +153,11 @@ func (a *AccessKeyConfig) GetReplaceKey(apiKey string) (string, error) { overwriteWith = a.SendKey } case "unlisted": - // only replace nonblank keys that are NOT listed in the `ReceiveKeys` list + // only replace nonblank keys that are NOT listed in the `ReceiveKeys` or `ReceiveKeyIDs` list // otherwise use original key if apiKey != "" { overwriteWith = apiKey - if !slices.Contains(a.ReceiveKeys, apiKey) { + if !slices.Contains(a.ReceiveKeys, apiKey) && !(keyID != "" && slices.Contains(a.ReceiveKeyIDs, keyID)) { overwriteWith = a.SendKey } } diff --git a/config/file_config_test.go b/config/file_config_test.go index d09e9e3ba6..0d6bd3945f 100644 --- a/config/file_config_test.go +++ b/config/file_config_test.go @@ -2,6 +2,7 @@ package config import ( "errors" + "fmt" "runtime" "testing" @@ -56,6 +57,7 @@ func Test_GetQueueSizesPerWorker(t *testing.T) { func TestAccessKeyConfig_GetReplaceKey(t *testing.T) { type fields struct { ReceiveKeys []string + ReceiveKeyIDs []string SendKey string SendKeyMode string AcceptOnlyListedKeys bool @@ -71,6 +73,12 @@ func TestAccessKeyConfig_GetReplaceKey(t *testing.T) { SendKey: "sendkey", SendKeyMode: "listedonly", } + fListedWithKeyIDs := fields{ + ReceiveKeys: []string{"key1", "key2"}, + ReceiveKeyIDs: []string{"kid1", "kid2"}, + SendKey: "sendkey", + SendKeyMode: "listedonly", + } fMissing := fields{ ReceiveKeys: []string{"key1", "key2"}, SendKey: "sendkey", @@ -81,36 +89,50 @@ func TestAccessKeyConfig_GetReplaceKey(t *testing.T) { SendKey: "sendkey", SendKeyMode: "unlisted", } + fUnlistedWithKeyIDs := fields{ + ReceiveKeys: []string{"key1", "key2"}, + ReceiveKeyIDs: []string{"kid1", "kid2"}, + SendKey: "sendkey", + SendKeyMode: "unlisted", + } tests := []struct { name string fields fields apiKey string + keyID string want string wantErr bool }{ - {"send all known", fSendAll, "key1", "sendkey", false}, - {"send all unknown", fSendAll, "userkey", "sendkey", false}, - {"send all missing", fSendAll, "", "sendkey", false}, - {"listed known", fListed, "key1", "sendkey", false}, - {"listed unknown", fListed, "userkey", "userkey", false}, - {"listed missing", fListed, "", "", true}, - {"missing known", fMissing, "key1", "key1", false}, - {"missing unknown", fMissing, "userkey", "userkey", false}, - {"missing missing", fMissing, "", "sendkey", false}, - {"unlisted known", fUnlisted, "key1", "key1", false}, - {"unlisted unknown", fUnlisted, "userkey", "sendkey", false}, - {"unlisted missing", fUnlisted, "", "", true}, + {"send all known", fSendAll, "key1", "", "sendkey", false}, + {"send all unknown", fSendAll, "userkey", "", "sendkey", false}, + {"send all missing", fSendAll, "", "", "sendkey", false}, + {"listed known", fListed, "key1", "", "sendkey", false}, + {"listed unknown", fListed, "userkey", "", "userkey", false}, + {"listed missing", fListed, "", "", "", true}, + {"listed by keyID known", fListedWithKeyIDs, "unknownkey", "kid1", "sendkey", false}, + {"listed by keyID unknown", fListedWithKeyIDs, "unknownkey", "unknownkid", "unknownkey", false}, + {"listed by keyID empty", fListedWithKeyIDs, "unknownkey", "", "unknownkey", false}, + {"missing known", fMissing, "key1", "", "key1", false}, + {"missing unknown", fMissing, "userkey", "", "userkey", false}, + {"missing missing", fMissing, "", "", "sendkey", false}, + {"unlisted known", fUnlisted, "key1", "", "key1", false}, + {"unlisted unknown", fUnlisted, "userkey", "", "sendkey", false}, + {"unlisted missing", fUnlisted, "", "", "", true}, + {"unlisted by keyID known", fUnlistedWithKeyIDs, "unknownkey", "kid1", "unknownkey", false}, + {"unlisted by keyID unknown", fUnlistedWithKeyIDs, "unknownkey", "unknownkid", "sendkey", false}, + {"unlisted by keyID empty", fUnlistedWithKeyIDs, "unknownkey", "", "sendkey", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &AccessKeyConfig{ ReceiveKeys: tt.fields.ReceiveKeys, + ReceiveKeyIDs: tt.fields.ReceiveKeyIDs, SendKey: tt.fields.SendKey, SendKeyMode: tt.fields.SendKeyMode, AcceptOnlyListedKeys: tt.fields.AcceptOnlyListedKeys, } - got, err := a.GetReplaceKey(tt.apiKey) + got, err := a.GetReplaceKey(tt.apiKey, tt.keyID) if (err != nil) != tt.wantErr { t.Errorf("AccessKeyConfig.GetReplaceKey() error = %v, wantErr %v", err, tt.wantErr) return @@ -125,6 +147,7 @@ func TestAccessKeyConfig_GetReplaceKey(t *testing.T) { func TestAccessKeyConfig_IsAccepted(t *testing.T) { type fields struct { ReceiveKeys []string + ReceiveKeyIDs []string SendKey string SendKeyMode string AcceptOnlyListedKeys bool @@ -133,24 +156,33 @@ func TestAccessKeyConfig_IsAccepted(t *testing.T) { name string fields fields key string + keyID string want error }{ - {"no keys", fields{}, "key1", nil}, - {"known key", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "key1", nil}, - {"unknown key", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "key2", errors.New("api key key2... not found in list of authorized keys")}, - {"reject missing key with sendkey configured", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true, SendKey: "key2"}, "", errors.New("api key ... not found in list of authorized keys")}, - {"reject missing key without sendkey configured", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "", errors.New("api key ... not found in list of authorized keys")}, - {"accept sendkey", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true, SendKey: "key2"}, "key2", nil}, + {"no keys", fields{}, "key1", "", nil}, + {"known key", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "key1", "", nil}, + {"unknown key", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "key2", "", errors.New("api key key2... not found in list of authorized keys")}, + {"reject missing key with sendkey configured", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true, SendKey: "key2"}, "", "", errors.New("api key ... not found in list of authorized keys")}, + {"reject missing key without sendkey configured", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true}, "", "", errors.New("api key ... not found in list of authorized keys")}, + {"accept sendkey", fields{ReceiveKeys: []string{"key1"}, AcceptOnlyListedKeys: true, SendKey: "key2"}, "key2", "", nil}, + // ReceiveKeyIDs tests + {"known key id", fields{ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "anykey", "kid1", nil}, + {"unknown key id", fields{ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "anykey", "kid2", errors.New("api key anykey... not found in list of authorized keys")}, + {"key id with empty keyID param", fields{ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "anykey", "", errors.New("api key anykey... not found in list of authorized keys")}, + {"accept by key id when full key not listed", fields{ReceiveKeys: []string{"key1"}, ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "key2", "kid1", nil}, + {"accept by full key when key id not listed", fields{ReceiveKeys: []string{"key1"}, ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "key1", "kid2", nil}, + {"reject when neither full key nor key id match", fields{ReceiveKeys: []string{"key1"}, ReceiveKeyIDs: []string{"kid1"}, AcceptOnlyListedKeys: true}, "key2", "kid2", errors.New("api key key2... not found in list of authorized keys")}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &AccessKeyConfig{ ReceiveKeys: tt.fields.ReceiveKeys, + ReceiveKeyIDs: tt.fields.ReceiveKeyIDs, SendKey: tt.fields.SendKey, SendKeyMode: tt.fields.SendKeyMode, AcceptOnlyListedKeys: tt.fields.AcceptOnlyListedKeys, } - err := a.IsAccepted(tt.key) + err := a.IsAccepted(tt.key, tt.keyID) if tt.want == nil { require.NoError(t, err) return @@ -160,6 +192,104 @@ func TestAccessKeyConfig_IsAccepted(t *testing.T) { } } +func BenchmarkAccessKeyConfig_IsAccepted(b *testing.B) { + // Generate realistic key lists + makeKeys := func(n int) []string { + keys := make([]string, n) + for i := range keys { + keys[i] = fmt.Sprintf("key-%06d", i) + } + return keys + } + + benchmarks := []struct { + name string + config AccessKeyConfig + key string + keyID string + }{ + { + name: "no_filtering", + config: AccessKeyConfig{AcceptOnlyListedKeys: false}, + key: "anykey", + keyID: "", + }, + { + name: "ReceiveKeys_10_match_last", + config: AccessKeyConfig{ + ReceiveKeys: makeKeys(10), + AcceptOnlyListedKeys: true, + }, + key: "key-000009", + keyID: "", + }, + { + name: "ReceiveKeys_100_match_last", + config: AccessKeyConfig{ + ReceiveKeys: makeKeys(100), + AcceptOnlyListedKeys: true, + }, + key: "key-000099", + keyID: "", + }, + { + name: "ReceiveKeys_100_no_match", + config: AccessKeyConfig{ + ReceiveKeys: makeKeys(100), + AcceptOnlyListedKeys: true, + }, + key: "unknown-key", + keyID: "", + }, + { + name: "ReceiveKeyIDs_10_match_last", + config: AccessKeyConfig{ + ReceiveKeyIDs: makeKeys(10), + AcceptOnlyListedKeys: true, + }, + key: "anykey", + keyID: "key-000009", + }, + { + name: "ReceiveKeyIDs_100_match_last", + config: AccessKeyConfig{ + ReceiveKeyIDs: makeKeys(100), + AcceptOnlyListedKeys: true, + }, + key: "anykey", + keyID: "key-000099", + }, + { + name: "ReceiveKeyIDs_100_no_match", + config: AccessKeyConfig{ + ReceiveKeyIDs: makeKeys(100), + AcceptOnlyListedKeys: true, + }, + key: "anykey", + keyID: "unknown-kid", + }, + { + name: "both_100_match_by_keyID", + config: AccessKeyConfig{ + ReceiveKeys: makeKeys(100), + ReceiveKeyIDs: makeKeys(100), + AcceptOnlyListedKeys: true, + }, + key: "unknown-key", + keyID: "key-000050", + }, + } + + for _, bm := range benchmarks { + b.Run(bm.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = bm.config.IsAccepted(bm.key, bm.keyID) + } + }) + } +} + func TestCalculateSamplerKey(t *testing.T) { testCases := []struct { name string diff --git a/config/metadata/configMeta.yaml b/config/metadata/configMeta.yaml index 289f53e905..4b0cd885dd 100644 --- a/config/metadata/configMeta.yaml +++ b/config/metadata/configMeta.yaml @@ -213,7 +213,7 @@ groups: valuetype: stringarray v1name: APIKeys example: "your-key-goes-here" - reload: false + reload: true validations: - type: elementType arg: string @@ -223,20 +223,45 @@ groups: will be proxied through to the upstream API directly without modifying keys. + - name: ReceiveKeyIDs + type: stringarray + valuetype: stringarray + example: "your-key-id-goes-here" + firstversion: v3.2 + reload: true + validations: + - type: elementType + arg: string + summary: is a set of Honeycomb Ingest Key IDs that the proxy will treat specially. + description: > + When `AcceptOnlyListedKeys` is `true`, traffic using an API key whose + Honeycomb ingest key ID matches an entry in this list will be accepted. + The key ID is the `id` field returned by the Honeycomb `/1/auth` + endpoint; it is distinct from the full API key value. + + This allows authorization based on key IDs rather than full key values, + which avoids storing secret key material in the configuration file. + Both `ReceiveKeys` and `ReceiveKeyIDs` may be used simultaneously. + + Note: This feature does not support legacy API keys. Only Honeycomb + Ingest Keys (which have a key ID) are compatible with this setting. + - name: AcceptOnlyListedKeys type: bool valuetype: conditional extra: nostar APIKeys default: false reload: true - validation: + validations: - type: requiredWith arg: ReceiveKeys + - type: requiredWith + arg: ReceiveKeyIDs summary: is a boolean flag that causes events arriving with API keys not in the `ReceiveKeys` list to be rejected. description: > - If `true`, then only traffic using the keys listed in `ReceiveKeys` is - accepted. Events arriving with API keys not in the `ReceiveKeys` list - will be rejected with an HTTP `401` error. + If `true`, then only traffic using the keys listed in `ReceiveKeys` or + whose key ID is listed in `ReceiveKeyIDs` is accepted. Events arriving + with API keys not in either list will be rejected with an HTTP `401` error. If `false`, then all traffic is accepted and `ReceiveKeys` is ignored. diff --git a/config_complete.yaml b/config_complete.yaml index beb7eaf4ba..374cb6bee5 100644 --- a/config_complete.yaml +++ b/config_complete.yaml @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created on 2026-02-25 at 20:49:27 UTC from ../../config.yaml using a template generated on 2026-02-25 at 20:49:24 UTC +# created on 2026-04-08 at 21:59:37 UTC from ../../config.yaml using a template generated on 2026-04-08 at 21:59:33 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of @@ -166,16 +166,35 @@ AccessKeys: ## will be proxied through to the upstream API directly without modifying ## keys. ## - ## Not eligible for live reload. + ## Eligible for live reload. # ReceiveKeys: # - your-key-goes-here + ## ReceiveKeyIDs is a set of Honeycomb Ingest Key IDs that the proxy will + ## treat specially. + ## + ## When `AcceptOnlyListedKeys` is `true`, traffic using an API key whose + ## Honeycomb ingest key ID matches an entry in this list will be + ## accepted. The key ID is the `id` field returned by the Honeycomb + ## `/1/auth` endpoint; it is distinct from the full API key value. + ## This allows authorization based on key IDs rather than full key + ## values, which avoids storing secret key material in the configuration + ## file. Both `ReceiveKeys` and `ReceiveKeyIDs` may be used + ## simultaneously. + ## Note: This feature does not support legacy API keys. Only Honeycomb + ## Ingest Keys (which have a key ID) are compatible with this setting. + ## + ## Eligible for live reload. + # ReceiveKeyIDs: + # - your-key-id-goes-here + ## AcceptOnlyListedKeys is a boolean flag that causes events arriving ## with API keys not in the `ReceiveKeys` list to be rejected. ## - ## If `true`, then only traffic using the keys listed in `ReceiveKeys` is - ## accepted. Events arriving with API keys not in the `ReceiveKeys` list - ## will be rejected with an HTTP `401` error. + ## If `true`, then only traffic using the keys listed in `ReceiveKeys` or + ## whose key ID is listed in `ReceiveKeyIDs` is accepted. Events arriving + ## with API keys not in either list will be rejected with an HTTP `401` + ## error. ## If `false`, then all traffic is accepted and `ReceiveKeys` is ignored. ## This setting is applied **before** the `SendKey` and `SendKeyMode` ## settings. diff --git a/metrics.md b/metrics.md index 56c4234a70..5d8300327b 100644 --- a/metrics.md +++ b/metrics.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Metrics Documentation This document contains the description of various metrics used in Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-04-08 at 21:59:37 UTC. Note: This document does not include metrics defined in the dynsampler-go dependency, as those metrics are generated dynamically at runtime. As a result, certain metrics may be missing or incomplete in this document, but they will still be available during execution with their full names. diff --git a/refinery_config.md b/refinery_config.md index fc53f1da5e..497a46a835 100644 --- a/refinery_config.md +++ b/refinery_config.md @@ -158,16 +158,31 @@ Not intended or supported for customer use. This list only applies to span traffic - other Honeycomb API actions will be proxied through to the upstream API directly without modifying keys. -- Not eligible for live reload. +- Eligible for live reload. - Type: `stringarray` - Example: `your-key-goes-here` +### `ReceiveKeyIDs` + +`ReceiveKeyIDs` is a set of Honeycomb Ingest Key IDs that the proxy will treat specially. + +When `AcceptOnlyListedKeys` is `true`, traffic using an API key whose Honeycomb ingest key ID matches an entry in this list will be accepted. +The key ID is the `id` field returned by the Honeycomb `/1/auth` endpoint; it is distinct from the full API key value. +This allows authorization based on key IDs rather than full key values, which avoids storing secret key material in the configuration file. +Both `ReceiveKeys` and `ReceiveKeyIDs` may be used simultaneously. +Note: This feature does not support legacy API keys. +Only Honeycomb Ingest Keys (which have a key ID) are compatible with this setting. + +- Eligible for live reload. +- Type: `stringarray` +- Example: `your-key-id-goes-here` + ### `AcceptOnlyListedKeys` `AcceptOnlyListedKeys` is a boolean flag that causes events arriving with API keys not in the `ReceiveKeys` list to be rejected. -If `true`, then only traffic using the keys listed in `ReceiveKeys` is accepted. -Events arriving with API keys not in the `ReceiveKeys` list will be rejected with an HTTP `401` error. +If `true`, then only traffic using the keys listed in `ReceiveKeys` or whose key ID is listed in `ReceiveKeyIDs` is accepted. +Events arriving with API keys not in either list will be rejected with an HTTP `401` error. If `false`, then all traffic is accepted and `ReceiveKeys` is ignored. This setting is applied **before** the `SendKey` and `SendKeyMode` settings. diff --git a/route/middleware.go b/route/middleware.go index 3b1d0081ed..6a86f47156 100644 --- a/route/middleware.go +++ b/route/middleware.go @@ -45,12 +45,16 @@ func (r *Router) apiKeyProcessor(next http.Handler) http.Handler { } keycfg := r.Config.GetAccessKeyConfig() - if err := keycfg.IsAccepted(apiKey); err != nil { + keyID := "" + if keycfg.HasKeyIDs() { + keyID = r.getKeyID(apiKey) + } + if err := keycfg.IsAccepted(apiKey, keyID); err != nil { r.handlerReturnWithError(w, ErrAuthInvalid, err) return } - replacement, err := keycfg.GetReplaceKey(apiKey) + replacement, err := keycfg.GetReplaceKey(apiKey, keyID) if err != nil { r.handlerReturnWithError(w, ErrAuthInvalid, err) return diff --git a/route/otlp_logs.go b/route/otlp_logs.go index be23128b3e..6f2e39669f 100644 --- a/route/otlp_logs.go +++ b/route/otlp_logs.go @@ -18,11 +18,15 @@ func (r *Router) postOTLPLogs(w http.ResponseWriter, req *http.Request) { ri := huskyotlp.GetRequestInfoFromHttpHeaders(req.Header) apicfg := r.Config.GetAccessKeyConfig() - if err := apicfg.IsAccepted(ri.ApiKey); err != nil { + keyID := "" + if apicfg.HasKeyIDs() { + keyID = r.getKeyID(ri.ApiKey) + } + if err := apicfg.IsAccepted(ri.ApiKey, keyID); err != nil { r.handleOTLPFailureResponse(w, req, huskyotlp.OTLPError{Message: err.Error(), HTTPStatusCode: http.StatusUnauthorized}) return } - keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey) + keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey, keyID) if err := ri.ValidateLogsHeaders(); err != nil { switch err { @@ -79,10 +83,14 @@ func (l *LogsServer) Export(ctx context.Context, req *collectorlogs.ExportLogsSe l.router.Metrics.Increment(l.router.metricsNames.routerOtlpLogGrpc) ri := huskyotlp.GetRequestInfoFromGrpcMetadata(ctx) apicfg := l.router.Config.GetAccessKeyConfig() - if err := apicfg.IsAccepted(ri.ApiKey); err != nil { + keyID := "" + if apicfg.HasKeyIDs() { + keyID = l.router.getKeyID(ri.ApiKey) + } + if err := apicfg.IsAccepted(ri.ApiKey, keyID); err != nil { return nil, status.Error(codes.Unauthenticated, err.Error()) } - keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey) + keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey, keyID) if err := ri.ValidateLogsHeaders(); err != nil && err != huskyotlp.ErrMissingAPIKeyHeader { return nil, huskyotlp.AsGRPCError(err) diff --git a/route/otlp_logs_test.go b/route/otlp_logs_test.go index 5a8d868033..7c97fbf16f 100644 --- a/route/otlp_logs_test.go +++ b/route/otlp_logs_test.go @@ -603,8 +603,8 @@ func TestLogsOTLPHandler(t *testing.T) { }, } { t.Run(fmt.Sprintf("ApiKey %s SendKeyMode %s SendKey %s", tt.apiKey, tt.mode, tt.sendKey), func(t *testing.T) { - router.environmentCache.addItem(tt.apiKey, "local", time.Minute) - router.environmentCache.addItem(tt.sendKey, "local", time.Minute) + router.environmentCache.addItem(tt.apiKey, authData{environment: "local"}, time.Minute) + router.environmentCache.addItem(tt.sendKey, authData{environment: "local"}, time.Minute) // HTTP request, _ := http.NewRequest("POST", "/v1/logs", bytes.NewReader(body)) diff --git a/route/otlp_trace.go b/route/otlp_trace.go index 2e8fddf177..ff1d4df5dd 100644 --- a/route/otlp_trace.go +++ b/route/otlp_trace.go @@ -26,11 +26,15 @@ func (r *Router) postOTLPTrace(w http.ResponseWriter, req *http.Request) { ri := huskyotlp.GetRequestInfoFromHttpHeaders(req.Header) apicfg := r.Config.GetAccessKeyConfig() - if err := apicfg.IsAccepted(ri.ApiKey); err != nil { + keyID := "" + if apicfg.HasKeyIDs() { + keyID = r.getKeyID(ri.ApiKey) + } + if err := apicfg.IsAccepted(ri.ApiKey, keyID); err != nil { r.handleOTLPFailureResponse(w, req, huskyotlp.OTLPError{Message: err.Error(), HTTPStatusCode: http.StatusUnauthorized}) return } - keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey) + keyToUse, _ := apicfg.GetReplaceKey(ri.ApiKey, keyID) if err := ri.ValidateTracesHeaders(); err != nil { switch err { @@ -137,7 +141,11 @@ func (t *TraceServer) ExportTraceData( // Perform final authentication check (key processing already done in handler) apicfg := t.router.Config.GetAccessKeyConfig() - if err := apicfg.IsAccepted(ri.ApiKey); err != nil { + keyID := "" + if apicfg.HasKeyIDs() { + keyID = t.router.getKeyID(ri.ApiKey) + } + if err := apicfg.IsAccepted(ri.ApiKey, keyID); err != nil { return nil, status.Error(codes.Unauthenticated, err.Error()) } @@ -205,7 +213,11 @@ func customTraceExportHandler( // Handle SendKeyMode logic before validation, similar to HTTP handler apicfg := traceServer.router.Config.GetAccessKeyConfig() - keyToUse, err := apicfg.GetReplaceKey(ri.ApiKey) + keyID := "" + if apicfg.HasKeyIDs() { + keyID = traceServer.router.getKeyID(ri.ApiKey) + } + keyToUse, err := apicfg.GetReplaceKey(ri.ApiKey, keyID) if err != nil { return nil, status.Error(codes.Unauthenticated, err.Error()) } diff --git a/route/otlp_trace_test.go b/route/otlp_trace_test.go index ab9d99b836..3a7412f4b9 100644 --- a/route/otlp_trace_test.go +++ b/route/otlp_trace_test.go @@ -505,7 +505,7 @@ func TestOTLPHandler(t *testing.T) { apiKey := "my-api-key" // add cached environment lookup - router.environmentCache.addItem(apiKey, "local", time.Minute) + router.environmentCache.addItem(apiKey, authData{environment: "local"}, time.Minute) req := &collectortrace.ExportTraceServiceRequest{ ResourceSpans: []*trace.ResourceSpans{{ @@ -920,8 +920,8 @@ func TestOTLPHandler(t *testing.T) { }, } { t.Run(fmt.Sprintf("ApiKey %s SendKeyMode %s SendKey %s", tt.apiKey, tt.mode, tt.sendKey), func(t *testing.T) { - router.environmentCache.addItem(tt.apiKey, "local", time.Minute) - router.environmentCache.addItem(tt.sendKey, "local", time.Minute) + router.environmentCache.addItem(tt.apiKey, authData{environment: "local"}, time.Minute) + router.environmentCache.addItem(tt.sendKey, authData{environment: "local"}, time.Minute) // HTTP request, _ := http.NewRequest("POST", "/v1/traces", bytes.NewReader(body)) diff --git a/route/route.go b/route/route.go index 2f0c436a26..cf1a0d6a7e 100644 --- a/route/route.go +++ b/route/route.go @@ -994,18 +994,32 @@ func getFirstValueFromMetadata(key string, md metadata.MD) string { return "" } +// authData holds the information retrieved from the Honeycomb /1/auth endpoint +// and stored in the environment cache. +type authData struct { + environment string + keyID string +} + type environmentCache struct { mutex sync.RWMutex items map[string]*cacheItem ttl time.Duration - getFn func(string) (string, error) + getFn func(string) (authData, error) } +// SetEnvironmentCache replaces the environment cache with a new one using the +// provided TTL and lookup function. The lookup function returns only the +// environment name, and the key ID will be empty in the cached authData. +// This method exists for backward compatibility with tests. func (r *Router) SetEnvironmentCache(ttl time.Duration, getFn func(string) (string, error)) { - r.environmentCache = newEnvironmentCache(ttl, getFn) + r.environmentCache = newEnvironmentCache(ttl, func(key string) (authData, error) { + env, err := getFn(key) + return authData{environment: env}, err + }) } -func newEnvironmentCache(ttl time.Duration, getFn func(string) (string, error)) *environmentCache { +func newEnvironmentCache(ttl time.Duration, getFn func(string) (authData, error)) *environmentCache { return &environmentCache{ items: make(map[string]*cacheItem), ttl: ttl, @@ -1015,13 +1029,13 @@ func newEnvironmentCache(ttl time.Duration, getFn func(string) (string, error)) type cacheItem struct { expiresAt time.Time - value string + value authData } // get queries the cached items, returning cache hits that have not expired. // Cache missed use the configured getFn to populate the cache. -func (c *environmentCache) get(key string) (string, error) { - var val string +func (c *environmentCache) get(key string) (authData, error) { + var val authData // get read lock so that we don't attempt to read from the map // while another routine has a write lock and is actively writing // to the map. @@ -1032,7 +1046,7 @@ func (c *environmentCache) get(key string) (string, error) { } } c.mutex.RUnlock() - if val != "" { + if val.environment != "" { return val, nil } @@ -1051,7 +1065,7 @@ func (c *environmentCache) get(key string) (string, error) { val, err := c.getFn(key) if err != nil { - return "", err + return authData{}, err } c.addItem(key, val, c.ttl) @@ -1060,7 +1074,7 @@ func (c *environmentCache) get(key string) (string, error) { // addItem create a new cache entry in the environment cache. // This is not thread-safe, and should only be used in tests -func (c *environmentCache) addItem(key string, value string, ttl time.Duration) { +func (c *environmentCache) addItem(key string, value authData, ttl time.Duration) { c.items[key] = &cacheItem{ expiresAt: time.Now().Add(ttl), value: value, @@ -1080,6 +1094,7 @@ type AuthInfo struct { APIKeyAccess map[string]bool `json:"api_key_access"` Team TeamInfo `json:"team"` Environment EnvironmentInfo `json:"environment"` + ID string `json:"id"` } func (r *Router) getEnvironmentName(apiKey string) (string, error) { @@ -1087,24 +1102,36 @@ func (r *Router) getEnvironmentName(apiKey string) (string, error) { return "", nil } - env, err := r.environmentCache.get(apiKey) + data, err := r.environmentCache.get(apiKey) if err != nil { return "", err } - return env, nil + return data.environment, nil } -func (r *Router) lookupEnvironment(apiKey string) (string, error) { +// getKeyID returns the Honeycomb ingest key ID associated with the given API +// key. It uses the environment cache, so no additional API call is made if the +// key has already been looked up. Returns an empty string for legacy keys, +// blank keys, or if the lookup fails. +func (r *Router) getKeyID(apiKey string) string { + if apiKey == "" || config.IsLegacyAPIKey(apiKey) { + return "" + } + data, _ := r.environmentCache.get(apiKey) + return data.keyID +} + +func (r *Router) lookupEnvironment(apiKey string) (authData, error) { apiEndpoint := r.Config.GetHoneycombAPI() authURL, err := url.Parse(apiEndpoint) if err != nil { - return "", fmt.Errorf("failed to parse Honeycomb API URL config value. %w", err) + return authData{}, fmt.Errorf("failed to parse Honeycomb API URL config value. %w", err) } authURL.Path = "/1/auth" req, err := http.NewRequest("GET", authURL.String(), nil) if err != nil { - return "", fmt.Errorf("failed to create AuthInfo request. %w", err) + return authData{}, fmt.Errorf("failed to create AuthInfo request. %w", err) } req.Header.Set("x-Honeycomb-team", apiKey) @@ -1112,23 +1139,26 @@ func (r *Router) lookupEnvironment(apiKey string) (string, error) { r.Logger.Debug().WithString("endpoint", authURL.String()).Logf("Attempting to get environment name using API key") resp, err := r.proxyClient.Do(req) if err != nil { - return "", fmt.Errorf("failed sending AuthInfo request to Honeycomb API. %w", err) + return authData{}, fmt.Errorf("failed sending AuthInfo request to Honeycomb API. %w", err) } defer resp.Body.Close() switch { case resp.StatusCode == http.StatusUnauthorized: - return "", fmt.Errorf("received 401 response for AuthInfo request from Honeycomb API - check your API key") + return authData{}, fmt.Errorf("received 401 response for AuthInfo request from Honeycomb API - check your API key") case resp.StatusCode > 299: - return "", fmt.Errorf("received %d response for AuthInfo request from Honeycomb API", resp.StatusCode) + return authData{}, fmt.Errorf("received %d response for AuthInfo request from Honeycomb API", resp.StatusCode) } authinfo := AuthInfo{} if err := json.NewDecoder(resp.Body).Decode(&authinfo); err != nil { - return "", fmt.Errorf("failed to JSON decode of AuthInfo response from Honeycomb API") + return authData{}, fmt.Errorf("failed to JSON decode of AuthInfo response from Honeycomb API") } r.Logger.Debug().WithString("environment", authinfo.Environment.Name).Logf("Got environment") - return authinfo.Environment.Name, nil + return authData{ + environment: authinfo.Environment.Name, + keyID: authinfo.ID, + }, nil } func (r *Router) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { diff --git a/route/route_test.go b/route/route_test.go index 81781a4ec6..1e47db3beb 100644 --- a/route/route_test.go +++ b/route/route_test.go @@ -741,53 +741,53 @@ func TestDependencyInjection(t *testing.T) { func TestEnvironmentCache(t *testing.T) { t.Run("calls getFn on cache miss", func(t *testing.T) { - cache := newEnvironmentCache(time.Second, func(key string) (string, error) { + cache := newEnvironmentCache(time.Second, func(key string) (authData, error) { if key != "key" { t.Errorf("expected %s - got %s", "key", key) } - return "test", nil + return authData{environment: "test"}, nil }) val, err := cache.get("key") if err != nil { t.Errorf("got error calling getOrSet - %e", err) } - if val != "test" { - t.Errorf("expected %s - got %s", "test", val) + if val.environment != "test" { + t.Errorf("expected %s - got %s", "test", val.environment) } }) t.Run("does not call getFn on cache hit", func(t *testing.T) { - cache := newEnvironmentCache(time.Second, func(key string) (string, error) { + cache := newEnvironmentCache(time.Second, func(key string) (authData, error) { t.Errorf("should not have called getFn") - return "", nil + return authData{}, nil }) - cache.addItem("key", "value", time.Second) + cache.addItem("key", authData{environment: "value"}, time.Second) val, err := cache.get("key") if err != nil { t.Errorf("got error calling getOrSet - %e", err) } - if val != "value" { - t.Errorf("expected %s - got %s", "value", val) + if val.environment != "value" { + t.Errorf("expected %s - got %s", "value", val.environment) } }) t.Run("ignores expired items", func(t *testing.T) { called := false - cache := newEnvironmentCache(time.Millisecond, func(key string) (string, error) { + cache := newEnvironmentCache(time.Millisecond, func(key string) (authData, error) { called = true - return "value", nil + return authData{environment: "value"}, nil }) - cache.addItem("key", "value", time.Millisecond) + cache.addItem("key", authData{environment: "value"}, time.Millisecond) time.Sleep(time.Millisecond * 5) val, err := cache.get("key") if err != nil { t.Errorf("got error calling getOrSet - %e", err) } - if val != "value" { - t.Errorf("expected %s - got %s", "value", val) + if val.environment != "value" { + t.Errorf("expected %s - got %s", "value", val.environment) } if !called { t.Errorf("expected to call getFn") @@ -796,8 +796,8 @@ func TestEnvironmentCache(t *testing.T) { t.Run("errors returned from getFn are propagated", func(t *testing.T) { expectedErr := errors.New("error") - cache := newEnvironmentCache(time.Second, func(key string) (string, error) { - return "", expectedErr + cache := newEnvironmentCache(time.Second, func(key string) (authData, error) { + return authData{}, expectedErr }) _, err := cache.get("key") @@ -1206,7 +1206,7 @@ func newBatchRouter(t testing.TB) *Router { Sharder: mockSharder, routerType: types.RouterTypeIncoming, iopLogger: iopLogger{Logger: &logger.NullLogger{}, incomingOrPeer: types.RouterTypeIncoming.String()}, - environmentCache: newEnvironmentCache(time.Second, func(key string) (string, error) { return "test", nil }), + environmentCache: newEnvironmentCache(time.Second, func(key string) (authData, error) { return authData{environment: "test"}, nil }), Tracer: noop.Tracer{}, } var err error diff --git a/rules.md b/rules.md index ee6023bc0e..8a3cc283ca 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-02-25 at 20:49:27 UTC. +It was automatically generated on 2026-04-08 at 21:59:38 UTC. ## The Rules file diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 793d4bb1e7..79cca9d8b4 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-02-25 at 20:49:25 UTC. +# Automatically generated on 2026-04-08 at 21:59:34 UTC. General: - ConfigurationVersion @@ -34,6 +34,8 @@ OpAMP: AccessKeys: - ReceiveKeys (originally APIKeys) + - ReceiveKeyIDs + - AcceptOnlyListedKeys - SendKey diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index eb49635629..487821f6e1 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-02-25 at 20:49:25 UTC +# automatically generated on 2026-04-08 at 21:59:35 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" @@ -18,6 +18,9 @@ AccessKeys: ReceiveKeys: - "your-key-goes-here" + ReceiveKeyIDs: + - "your-key-id-goes-here" + AcceptOnlyListedKeys: false SendKey: SetThisToAHoneycombKey SendKeyMode: none diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 7f91a2f2a5..6768b4d5cd 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-02-25 at 20:49:24 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-04-08 at 21:59:33 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of @@ -165,15 +165,33 @@ AccessKeys: ## will be proxied through to the upstream API directly without modifying ## keys. ## - ## Not eligible for live reload. + ## Eligible for live reload. {{ renderStringarray .Data "ReceiveKeys" "APIKeys" "your-key-goes-here" }} + ## ReceiveKeyIDs is a set of Honeycomb Ingest Key IDs that the proxy will + ## treat specially. + ## + ## When `AcceptOnlyListedKeys` is `true`, traffic using an API key whose + ## Honeycomb ingest key ID matches an entry in this list will be + ## accepted. The key ID is the `id` field returned by the Honeycomb + ## `/1/auth` endpoint; it is distinct from the full API key value. + ## This allows authorization based on key IDs rather than full key + ## values, which avoids storing secret key material in the configuration + ## file. Both `ReceiveKeys` and `ReceiveKeyIDs` may be used + ## simultaneously. + ## Note: This feature does not support legacy API keys. Only Honeycomb + ## Ingest Keys (which have a key ID) are compatible with this setting. + ## + ## Eligible for live reload. + {{ renderStringarray .Data "ReceiveKeyIDs" "ReceiveKeyIDs" "your-key-id-goes-here" }} + ## AcceptOnlyListedKeys is a boolean flag that causes events arriving ## with API keys not in the `ReceiveKeys` list to be rejected. ## - ## If `true`, then only traffic using the keys listed in `ReceiveKeys` is - ## accepted. Events arriving with API keys not in the `ReceiveKeys` list - ## will be rejected with an HTTP `401` error. + ## If `true`, then only traffic using the keys listed in `ReceiveKeys` or + ## whose key ID is listed in `ReceiveKeyIDs` is accepted. Events arriving + ## with API keys not in either list will be rejected with an HTTP `401` + ## error. ## If `false`, then all traffic is accepted and `ReceiveKeys` is ignored. ## This setting is applied **before** the `SendKey` and `SendKeyMode` ## settings. From 108bba1bbf072229039c898a82c9e015cbf2c8bd Mon Sep 17 00:00:00 2001 From: Davin Date: Thu, 9 Apr 2026 17:19:05 -0500 Subject: [PATCH 15/35] feat: add OTelMetrics.AdditionalAttributes config option (#1804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds `OTelMetrics.AdditionalAttributes` config option to inject custom resource attributes (e.g., cluster ID, environment name) into all OTLP metrics emitted by Refinery - Both keys and values must be strings, supplied as comma-separated `key:value` pairs - Fixes metadata for HoneycombLogger `AdditionalAttributes` (removed invalid validations, fixed example quoting, fixed indentation) This is part of the RaaS (Refinery as a Service) feature set, split from PR #1798. Closes FRE-75 ## Test plan - [ ] `TestOTelMetricsAdditionalAttributes` verifies resource attributes are present - [ ] `Test_OTelMetrics_AdditionalAttributes` verifies attributes appear in exported metrics - [ ] Config validation passes (`make validate` in `tools/convert/`) - [ ] `go vet ./...` passes - [ ] All existing tests continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 --- config.md | 18 +++++++++++- config/cmdenv.go | 1 + config/config_test.go | 17 ++++++++++++ config/file_config.go | 13 +++++---- config/metadata/configMeta.yaml | 34 ++++++++++++++++++----- config_complete.yaml | 20 +++++++++++++- metrics.md | 2 +- metrics/otel_metrics.go | 10 +++++-- metrics/otel_metrics_test.go | 40 +++++++++++++++++++++++++++ refinery_config.md | 16 +++++++++++ rules.md | 2 +- tools/convert/configDataNames.txt | 4 ++- tools/convert/minimal_config.yaml | 6 +++- tools/convert/templates/configV2.tmpl | 18 +++++++++++- 14 files changed, 179 insertions(+), 22 deletions(-) diff --git a/config.md b/config.md index f70c0a782b..05f837b711 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-04-08 at 21:59:38 UTC. +It was automatically generated on 2026-04-09 at 16:54:01 UTC. ## The Config file @@ -675,6 +675,22 @@ In rare circumstances, compression costs may outweigh the benefits, in which cas - Default: `gzip` - Options: `none`, `gzip` +### `AdditionalAttributes` + +AdditionalAttributes adds the provided attributes as resource attributes on all OpenTelemetry metrics emitted by Refinery. + +This is useful for injecting deployment-specific metadata (such as a cluster ID or environment name) into metrics so they can be filtered or grouped in the metrics backend. +Both keys and values must be strings. +When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. +When supplying via the command line, the value should be a key value pair. +If multiple key-value pairs are needed, each should be supplied via its own command line flag. +The key-value pairs must use ':' as the separator. + +- Not eligible for live reload. +- Type: `map` +- Example: `pipeline.id:'12345',rollout.id:'67890'` +- Environment variable: `REFINERY_OTEL_METRICS_ADDITIONAL_ATTRIBUTES` + ## OpenTelemetry Tracing `OTelTracing` contains configuration for Refinery's own tracing. diff --git a/config/cmdenv.go b/config/cmdenv.go index d96f008a7f..4daa63a031 100644 --- a/config/cmdenv.go +++ b/config/cmdenv.go @@ -43,6 +43,7 @@ type CmdEnv struct { OpAMPEndpoint string `long:"opamp-server-url" env:"REFINERY_OPAMP_ENDPOINT" description:"URL of the OpAMP server to use for remote management."` TelemetryEndpoint string `long:"telemetry-endpoint" env:"REFINERY_TELEMETRY_ENDPOINT" description:"Endpoint to send Refinery's internal telemetry to. This is separate from the Honeycomb API endpoint and is used for sending metrics about Refinery's performance."` OTelMetricsAPIKey string `long:"otel-metrics-api-key" env:"REFINERY_OTEL_METRICS_API_KEY" description:"API key for OTel metrics if being sent to Honeycomb. Setting this value via a flag may expose credentials - it is recommended to use the env var or a configuration file."` + OTelMetricsAdditionalAttributes map[string]string `long:"otel-metrics-additional-attributes" env:"REFINERY_OTEL_METRICS_ADDITIONAL_ATTRIBUTES" env-delim:"," description:"Additional attributes to add as resource attributes on all OpenTelemetry metrics emitted by Refinery. When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. When supplying via the command line, the value should be a key value pair. If multiple key-value pairs are needed, each should be supplied via its own command line flag. The key-value pairs must use ':' as the separator."` OTelTracesAPIKey string `long:"otel-traces-api-key" env:"REFINERY_OTEL_TRACES_API_KEY" description:"API key for OTel traces if being sent to Honeycomb. Setting this value via a flag may expose credentials - it is recommended to use the env var or a configuration file."` QueryAuthToken string `long:"query-auth-token" env:"REFINERY_QUERY_AUTH_TOKEN" description:"Token for debug/management queries. Setting this value via a flag may expose credentials - it is recommended to use the env var or a configuration file."` AvailableMemory MemorySize `long:"available-memory" env:"REFINERY_AVAILABLE_MEMORY" description:"The maximum memory available for Refinery to use (ex: 4GiB)."` diff --git a/config/config_test.go b/config/config_test.go index 30fd468d4a..04f6faaf94 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -909,6 +909,23 @@ func TestAdditionalAttributes(t *testing.T) { assert.Equal(t, map[string]string{"name": "foo", "other": "bar", "another": "OneHundred"}, c.GetAdditionalAttributes()) } +func TestOTelMetricsAdditionalAttributes(t *testing.T) { + cm := makeYAML( + "General.ConfigurationVersion", 2, + "OTelMetrics.AdditionalAttributes", map[string]string{ + "cluster.id": "my-cluster", + "environment": "production", + }, + ) + rm := makeYAML("ConfigVersion", 2) + config, rules := createTempConfigs(t, cm, rm) + c, err := getConfig([]string{"--no-validate", "--config", config, "--rules_config", rules}) + assert.NoError(t, err) + + otelCfg := c.GetOTelMetricsConfig() + assert.Equal(t, map[string]string{"cluster.id": "my-cluster", "environment": "production"}, otelCfg.AdditionalAttributes) +} + func TestHoneycombIdFieldsConfig(t *testing.T) { cm := makeYAML( "General.ConfigurationVersion", 2, diff --git a/config/file_config.go b/config/file_config.go index 4a215910fe..48aec76c58 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -278,12 +278,13 @@ type PrometheusMetricsConfig struct { } type OTelMetricsConfig struct { - Enabled bool `yaml:"Enabled" default:"false"` - APIHost string `yaml:"APIHost" default:"https://api.honeycomb.io" cmdenv:"TelemetryEndpoint"` - APIKey string `yaml:"APIKey" cmdenv:"OTelMetricsAPIKey,HoneycombAPIKey"` - Dataset string `yaml:"Dataset" default:"Refinery Metrics"` - Compression string `yaml:"Compression" default:"gzip"` - ReportingInterval Duration `yaml:"ReportingInterval" default:"30s"` + Enabled bool `yaml:"Enabled" default:"false"` + APIHost string `yaml:"APIHost" default:"https://api.honeycomb.io" cmdenv:"TelemetryEndpoint"` + APIKey string `yaml:"APIKey" cmdenv:"OTelMetricsAPIKey,HoneycombAPIKey"` + Dataset string `yaml:"Dataset" default:"Refinery Metrics"` + Compression string `yaml:"Compression" default:"gzip"` + ReportingInterval Duration `yaml:"ReportingInterval" default:"30s"` + AdditionalAttributes map[string]string `yaml:"AdditionalAttributes" default:"{}" cmdenv:"OTelMetricsAdditionalAttributes"` } type OTelTracingConfig struct { diff --git a/config/metadata/configMeta.yaml b/config/metadata/configMeta.yaml index 4b0cd885dd..32c4ff1aed 100644 --- a/config/metadata/configMeta.yaml +++ b/config/metadata/configMeta.yaml @@ -698,17 +698,14 @@ groups: valuetype: map example: "pipeline.id:'12345',rollout.id:'67890'" reload: false - validations: - - type: elementType - arg: string summary: adds the provided attributes to all logs written by the Honeycomb logger. envvar: REFINERY_HONEYCOMB_LOGGER_ADDITIONAL_ATTRIBUTES commandline: logger-additional-attributes description: > - When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. - When supplying via the command line, the value should be a key value pair. - If multiple key-value pairs are needed, each should be supplied via its own command line flag. - The key-value pairs must use ':' as the separator. + When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. + When supplying via the command line, the value should be a key value pair. + If multiple key-value pairs are needed, each should be supplied via its own command line flag. + The key-value pairs must use ':' as the separator. - name: StdoutLogger title: "Stdout Logger" @@ -941,6 +938,29 @@ groups: compression costs may outweigh the benefits, in which case `none` may be used. + - name: AdditionalAttributes + type: map + valuetype: map + example: "pipeline.id:'12345',rollout.id:'67890'" + reload: false + firstversion: v3.2 + validations: + - type: elementType + arg: string + summary: adds the provided attributes as resource attributes on all OpenTelemetry metrics emitted by Refinery. + envvar: REFINERY_OTEL_METRICS_ADDITIONAL_ATTRIBUTES + commandline: otel-metrics-additional-attributes + description: > + This is useful for injecting deployment-specific metadata (such as + a cluster ID or environment name) into metrics so they can be + filtered or grouped in the metrics backend. + Both keys and values must be strings. + + When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. + When supplying via the command line, the value should be a key value pair. + If multiple key-value pairs are needed, each should be supplied via its own command line flag. + The key-value pairs must use ':' as the separator. + - name: OTelTracing title: "OpenTelemetry Tracing" description: contains configuration for Refinery's own tracing. diff --git a/config_complete.yaml b/config_complete.yaml index 374cb6bee5..be36cc9d0d 100644 --- a/config_complete.yaml +++ b/config_complete.yaml @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created on 2026-04-08 at 21:59:37 UTC from ../../config.yaml using a template generated on 2026-04-08 at 21:59:33 UTC +# created on 2026-04-09 at 16:54:00 UTC from ../../config.yaml using a template generated on 2026-04-09 at 16:53:56 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of @@ -714,6 +714,24 @@ OTelMetrics: ## Options: none gzip # Compression: gzip + ## AdditionalAttributes adds the provided attributes as resource + ## attributes on all OpenTelemetry metrics emitted by Refinery. + ## + ## This is useful for injecting deployment-specific metadata (such as a + ## cluster ID or environment name) into metrics so they can be filtered + ## or grouped in the metrics backend. Both keys and values must be + ## strings. + ## When supplying via a environment variable, the value should be a + ## string of comma-separated key-value pairs. When supplying via the + ## command line, the value should be a key value pair. If multiple + ## key-value pairs are needed, each should be supplied via its own + ## command line flag. The key-value pairs must use ':' as the separator. + ## + ## Not eligible for live reload. + # AdditionalAttributes: + # pipeline.id: '12345' + # rollout.id: '67890' + ########################### ## OpenTelemetry Tracing ## ########################### diff --git a/metrics.md b/metrics.md index 5d8300327b..be629c5b17 100644 --- a/metrics.md +++ b/metrics.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Metrics Documentation This document contains the description of various metrics used in Refinery. -It was automatically generated on 2026-04-08 at 21:59:37 UTC. +It was automatically generated on 2026-04-09 at 16:54:00 UTC. Note: This document does not include metrics defined in the dynsampler-go dependency, as those metrics are generated dynamically at runtime. As a result, certain metrics may be missing or incomplete in this document, but they will still be available during execution with their full names. diff --git a/metrics/otel_metrics.go b/metrics/otel_metrics.go index 6d969e1046..34d2f56ad4 100644 --- a/metrics/otel_metrics.go +++ b/metrics/otel_metrics.go @@ -118,13 +118,19 @@ func (o *OTelMetrics) Start() error { hostname = hn } - res, err := resource.New(ctx, + // Build resource attributes: start with defaults, then add user-defined additional attributes + resourceOpts := []resource.Option{ resource.WithAttributes(resource.Default().Attributes()...), resource.WithAttributes(attribute.KeyValue{Key: "service.name", Value: attribute.StringValue("refinery")}), resource.WithAttributes(attribute.KeyValue{Key: "service.version", Value: attribute.StringValue(o.Version)}), resource.WithAttributes(attribute.KeyValue{Key: "host.name", Value: attribute.StringValue(hostname)}), resource.WithAttributes(attribute.KeyValue{Key: "hostname", Value: attribute.StringValue(hostname)}), - ) + } + for k, v := range cfg.AdditionalAttributes { + resourceOpts = append(resourceOpts, resource.WithAttributes(attribute.KeyValue{Key: attribute.Key(k), Value: attribute.StringValue(v)})) + } + + res, err := resource.New(ctx, resourceOpts...) if err != nil { return err diff --git a/metrics/otel_metrics_test.go b/metrics/otel_metrics_test.go index 96043bf86a..cc0f7ffd44 100644 --- a/metrics/otel_metrics_test.go +++ b/metrics/otel_metrics_test.go @@ -124,6 +124,46 @@ func Test_OTelMetrics_Raciness(t *testing.T) { metricdatatest.AssertEqual(t, want, got, metricdatatest.IgnoreTimestamp()) } +func Test_OTelMetrics_AdditionalAttributes(t *testing.T) { + rdr := sdkmetric.NewManualReader() + + o := &OTelMetrics{ + Logger: &logger.MockLogger{}, + Config: &config.MockConfig{ + GetOTelMetricsConfigVal: config.OTelMetricsConfig{ + AdditionalAttributes: map[string]string{ + "cluster.id": "test-cluster-123", + "environment": "staging", + }, + }, + }, + testReader: rdr, + } + + err := o.Start() + defer o.Stop() + require.NoError(t, err) + + // Emit a metric so we can collect resource data + o.Register(Metadata{Name: "test_attr", Type: Counter}) + o.Increment("test_attr") + + rm := metricdata.ResourceMetrics{} + err = rdr.Collect(t.Context(), &rm) + require.NoError(t, err) + + // Check that the additional attributes are present as resource attributes + attrs := rm.Resource.Attributes() + attrMap := make(map[string]string) + for _, attr := range attrs { + attrMap[string(attr.Key)] = attr.Value.AsString() + } + + assert.Equal(t, "test-cluster-123", attrMap["cluster.id"], "cluster.id resource attribute should be set") + assert.Equal(t, "staging", attrMap["environment"], "environment resource attribute should be set") + assert.Equal(t, "refinery", attrMap["service.name"], "service.name should still be present") +} + func Benchmark_OTelMetrics_ConcurrentAccess(b *testing.B) { o := &OTelMetrics{ Logger: &logger.NullLogger{}, diff --git a/refinery_config.md b/refinery_config.md index 497a46a835..47d67bd338 100644 --- a/refinery_config.md +++ b/refinery_config.md @@ -659,6 +659,22 @@ In rare circumstances, compression costs may outweigh the benefits, in which cas - Default: `gzip` - Options: `none`, `gzip` +### `AdditionalAttributes` + +`AdditionalAttributes` adds the provided attributes as resource attributes on all OpenTelemetry metrics emitted by Refinery. + +This is useful for injecting deployment-specific metadata (such as a cluster ID or environment name) into metrics so they can be filtered or grouped in the metrics backend. +Both keys and values must be strings. +When supplying via a environment variable, the value should be a string of comma-separated key-value pairs. +When supplying via the command line, the value should be a key value pair. +If multiple key-value pairs are needed, each should be supplied via its own command line flag. +The key-value pairs must use ':' as the separator. + +- Not eligible for live reload. +- Type: `map` +- Example: `pipeline.id:'12345',rollout.id:'67890'` +- Environment variable: `REFINERY_OTEL_METRICS_ADDITIONAL_ATTRIBUTES` + ## OpenTelemetry Tracing `OTelTracing` contains configuration for Refinery's own tracing. diff --git a/rules.md b/rules.md index 8a3cc283ca..5090ce31c2 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-04-08 at 21:59:38 UTC. +It was automatically generated on 2026-04-09 at 16:54:01 UTC. ## The Rules file diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 79cca9d8b4..4d3790df64 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-04-08 at 21:59:34 UTC. +# Automatically generated on 2026-04-09 at 16:53:57 UTC. General: - ConfigurationVersion @@ -138,6 +138,8 @@ OTelMetrics: - Compression + - AdditionalAttributes + OTelTracing: - Enabled diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index 487821f6e1..9abb133fdc 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-04-08 at 21:59:35 UTC +# automatically generated on 2026-04-09 at 16:53:58 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" @@ -71,6 +71,10 @@ OTelMetrics: Dataset: "Refinery Metrics" ReportingInterval: 30s Compression: gzip + AdditionalAttributes: + "pipeline.id": "'12345'" + "rollout.id": "'67890'" + OTelTracing: Enabled: false APIHost: "https://api.honeycomb.io" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 6768b4d5cd..10f7e11013 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-04-08 at 21:59:33 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-04-09 at 16:53:56 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of @@ -708,6 +708,22 @@ OTelMetrics: ## Options: none gzip {{ choice .Data "Compression" "Compression" (makeSlice "none" "gzip") "gzip" }} + ## AdditionalAttributes adds the provided attributes as resource + ## attributes on all OpenTelemetry metrics emitted by Refinery. + ## + ## This is useful for injecting deployment-specific metadata (such as a + ## cluster ID or environment name) into metrics so they can be filtered + ## or grouped in the metrics backend. Both keys and values must be + ## strings. + ## When supplying via a environment variable, the value should be a + ## string of comma-separated key-value pairs. When supplying via the + ## command line, the value should be a key value pair. If multiple + ## key-value pairs are needed, each should be supplied via its own + ## command line flag. The key-value pairs must use ':' as the separator. + ## + ## Not eligible for live reload. + {{ renderMap .Data "AdditionalAttributes" "AdditionalAttributes" "pipeline.id:'12345',rollout.id:'67890'" }} + ########################### ## OpenTelemetry Tracing ## ########################### From afe6f3619046ff2f2e25a03bc0571fd9a4ded454 Mon Sep 17 00:00:00 2001 From: Davin Date: Thu, 9 Apr 2026 17:36:54 -0500 Subject: [PATCH 16/35] feat: add granular event metrics for RaaS usage tracking (#1805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds granular event-level metrics: `events_received_traces`, `events_received_logs`, `events_dropped_traces`, `events_dropped_logs` - Tracks received and dropped event counts per signal type (traces vs logs) separately from byte counts - Reports granular metrics via OTLP usage reporting (alongside existing `bytes_received`) for accurate RaaS usage billing - Metrics are differentiated by a `signal` attribute ("traces" or "logs") on each data point This is part of the RaaS (Refinery as a Service) feature set, split from PR #1798. Closes FRE-76 ## Test plan - [ ] New metrics are registered and increment correctly during event processing - [ ] Usage report includes new signal types in OTLP metric export - [ ] Config validation passes (`make validate` in `tools/convert/`) - [ ] `go vet ./...` passes - [ ] All existing tests continue to pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- agent/agent.go | 16 ++++++++++ agent/otlp_metrics.go | 46 ++++++++++++++++++++++----- agent/usage_report.go | 6 ++-- collect/collect.go | 5 +++ config.md | 2 +- config_complete.yaml | 2 +- metrics.md | 3 +- rules.md | 2 +- tools/convert/configDataNames.txt | 2 +- tools/convert/metricsMeta.yaml | 4 +++ tools/convert/minimal_config.yaml | 2 +- tools/convert/templates/configV2.tmpl | 2 +- 12 files changed, 75 insertions(+), 17 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index f408d8bfd6..736598dd7d 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -228,6 +228,22 @@ func (agent *Agent) healthCheck() { agent.usageTracker.Add(signal_traces, traceUsage) agent.usageTracker.Add(signal_logs, logUsage) + + var eventsReceived float64 + if v, ok := agent.metrics.Get("incoming_router_span"); ok { + eventsReceived += v + } + if v, ok := agent.metrics.Get("incoming_router_nonspan_event"); ok { + eventsReceived += v + } + if v, ok := agent.metrics.Get("incoming_router_event"); ok { + eventsReceived += v + } + agent.usageTracker.Add(signal_events_received, eventsReceived) + + if eventsDropped, ok := agent.metrics.Get("events_dropped"); ok { + agent.usageTracker.Add(signal_events_dropped, eventsDropped) + } } } } diff --git a/agent/otlp_metrics.go b/agent/otlp_metrics.go index 7e9e8140e4..b5e275925c 100644 --- a/agent/otlp_metrics.go +++ b/agent/otlp_metrics.go @@ -9,9 +9,22 @@ import ( "go.opentelemetry.io/collector/pdata/pmetric" ) +type metricMapping struct { + metricName string + signal string +} + +var signalToMetric = map[usageSignal]metricMapping{ + signal_traces: {metricName: "bytes_received", signal: "traces"}, + signal_logs: {metricName: "bytes_received", signal: "logs"}, + signal_events_received: {metricName: "events_received", signal: ""}, + signal_events_dropped: {metricName: "events_dropped", signal: ""}, +} + type otlpMetrics struct { metrics pmetric.Metrics - ms pmetric.Sum + sums map[string]pmetric.Sum + sm pmetric.ScopeMetrics } func newOTLPMetrics(serviceName, version, hostname string) *otlpMetrics { @@ -22,25 +35,42 @@ func newOTLPMetrics(serviceName, version, hostname string) *otlpMetrics { resourceAttrs.PutStr("service.version", version) resourceAttrs.PutStr("host.name", hostname) sm := rm.ScopeMetrics().AppendEmpty() - ms := sm.Metrics().AppendEmpty() - ms.SetName("bytes_received") - sum := ms.SetEmptySum() - sum.SetAggregationTemporality(pmetric.AggregationTemporalityDelta) return &otlpMetrics{ metrics: metrics, - ms: sum, + sums: make(map[string]pmetric.Sum), + sm: sm, + } +} + +func (om *otlpMetrics) getOrCreateSum(metricName string) pmetric.Sum { + if sum, ok := om.sums[metricName]; ok { + return sum } + ms := om.sm.Metrics().AppendEmpty() + ms.SetName(metricName) + sum := ms.SetEmptySum() + sum.SetAggregationTemporality(pmetric.AggregationTemporalityDelta) + om.sums[metricName] = sum + return sum } func (om *otlpMetrics) addOTLPSum(timestamp time.Time, value float64, signal usageSignal) error { + mapping, ok := signalToMetric[signal] + if !ok { + return fmt.Errorf("unknown usage signal: %s", signal) + } + intVal, err := convertFloat64ToInt64(value) if err != nil { return err } - d := om.ms.DataPoints().AppendEmpty() + sum := om.getOrCreateSum(mapping.metricName) + d := sum.DataPoints().AppendEmpty() d.SetTimestamp(pcommon.NewTimestampFromTime(timestamp)) d.SetIntValue(intVal) - d.Attributes().PutStr("signal", string(signal)) + if mapping.signal != "" { + d.Attributes().PutStr("signal", mapping.signal) + } return nil } diff --git a/agent/usage_report.go b/agent/usage_report.go index 0d947a021c..f48e4e333b 100644 --- a/agent/usage_report.go +++ b/agent/usage_report.go @@ -89,6 +89,8 @@ func (ur *usageTracker) completeSend() { type usageSignal string var ( - signal_traces usageSignal = "traces" - signal_logs usageSignal = "logs" + signal_traces usageSignal = "traces" + signal_logs usageSignal = "logs" + signal_events_received usageSignal = "events_received" + signal_events_dropped usageSignal = "events_dropped" ) diff --git a/collect/collect.go b/collect/collect.go index 0a27bfd583..b922bd9341 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -155,6 +155,7 @@ var inMemCollectorMetrics = []metrics.Metadata{ {Name: "dropped_from_stress", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of spans dropped due to stress relief"}, {Name: "kept_from_stress", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of spans kept due to stress relief"}, + {Name: "events_dropped", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of events dropped"}, {Name: "trace_kept_sample_rate", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "sample rate of kept traces"}, {Name: "trace_aggregate_sample_rate", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "aggregate sample rate of both kept and dropped traces"}, {Name: "collector_collect_loop_duration_ms", Type: metrics.Histogram, Unit: metrics.Milliseconds, Description: "duration of the collect loop, the primary event processing goroutine"}, @@ -470,6 +471,7 @@ func (i *InMemCollector) ProcessSpanImmediately(sp *types.Span) (processed bool, if !keep { i.Metrics.Increment("dropped_from_stress") + i.Metrics.Increment("events_dropped") return true, false } @@ -554,6 +556,7 @@ func (i *InMemCollector) dealWithSentTrace(ctx context.Context, tr cache.TraceSe i.Transmission.EnqueueSpan(sp) return } + i.Metrics.Increment("events_dropped") i.Logger.Debug().WithField("trace_id", sp.TraceID).Logf("Dropping span because of previous decision to drop trace") } @@ -610,6 +613,8 @@ func (i *InMemCollector) send(ctx context.Context, trace sendableTrace) { // if we're supposed to drop this trace, and dry run mode is not enabled, then we're done. if !trace.KeepSample && !i.Config.GetIsDryRun() { i.Metrics.Increment("trace_send_dropped") + dropCount := int64(trace.DescendantCount()) + i.Metrics.Count("events_dropped", dropCount) i.Logger.Debug().WithFields(logFields).Logf("Dropping trace because of sampling decision") return } diff --git a/config.md b/config.md index 05f837b711..23178b357d 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-04-09 at 16:54:01 UTC. +It was automatically generated on 2026-04-09 at 22:21:32 UTC. ## The Config file diff --git a/config_complete.yaml b/config_complete.yaml index be36cc9d0d..21e1a24751 100644 --- a/config_complete.yaml +++ b/config_complete.yaml @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created on 2026-04-09 at 16:54:00 UTC from ../../config.yaml using a template generated on 2026-04-09 at 16:53:56 UTC +# created on 2026-04-09 at 22:21:32 UTC from ../../config.yaml using a template generated on 2026-04-09 at 22:21:28 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of diff --git a/metrics.md b/metrics.md index be629c5b17..2b39f3f9f8 100644 --- a/metrics.md +++ b/metrics.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Metrics Documentation This document contains the description of various metrics used in Refinery. -It was automatically generated on 2026-04-09 at 16:54:00 UTC. +It was automatically generated on 2026-04-09 at 22:21:31 UTC. Note: This document does not include metrics defined in the dynsampler-go dependency, as those metrics are generated dynamically at runtime. As a result, certain metrics may be missing or incomplete in this document, but they will still be available during execution with their full names. @@ -59,6 +59,7 @@ This table includes metrics with fully defined names. | trace_send_late_span | Counter | Dimensionless | number of spans that are sent due to late span arrival | | dropped_from_stress | Counter | Dimensionless | number of spans dropped due to stress relief | | kept_from_stress | Counter | Dimensionless | number of spans kept due to stress relief | +| events_dropped | Counter | Dimensionless | number of events dropped | | trace_kept_sample_rate | Histogram | Dimensionless | sample rate of kept traces | | trace_aggregate_sample_rate | Histogram | Dimensionless | aggregate sample rate of both kept and dropped traces | | collector_collect_loop_duration_ms | Histogram | Milliseconds | duration of the collect loop, the primary event processing goroutine | diff --git a/rules.md b/rules.md index 5090ce31c2..a401c882d1 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-04-09 at 16:54:01 UTC. +It was automatically generated on 2026-04-09 at 22:21:32 UTC. ## The Rules file diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 4d3790df64..06cdaaa88e 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-04-09 at 16:53:57 UTC. +# Automatically generated on 2026-04-09 at 22:21:29 UTC. General: - ConfigurationVersion diff --git a/tools/convert/metricsMeta.yaml b/tools/convert/metricsMeta.yaml index d7a9db8d39..032c6d9025 100644 --- a/tools/convert/metricsMeta.yaml +++ b/tools/convert/metricsMeta.yaml @@ -187,6 +187,10 @@ complete: type: Counter unit: Dimensionless description: number of spans kept due to stress relief + - name: events_dropped + type: Counter + unit: Dimensionless + description: number of events dropped - name: trace_kept_sample_rate type: Histogram unit: Dimensionless diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index 9abb133fdc..db18ce37f7 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-04-09 at 16:53:58 UTC +# automatically generated on 2026-04-09 at 22:21:30 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 10f7e11013..4546348b92 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-04-09 at 16:53:56 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-04-09 at 22:21:28 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of From a3188ed5d6c2f664605b5c05f5423b1bb41d03ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:58:33 -0400 Subject: [PATCH 17/35] maint(deps): bump go.opentelemetry.io/otel/sdk from 1.42.0 to 1.43.0 (#1810) Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.42.0 to 1.43.0.
    Changelog

    Sourced from go.opentelemetry.io/otel/sdk's changelog.

    [1.43.0/0.65.0/0.19.0] 2026-04-02

    Added

    • Add IsRandom and WithRandom on TraceFlags, and IsRandom on SpanContext in go.opentelemetry.io/otel/trace for W3C Trace Context Level 2 Random Trace ID Flag support. (#8012)
    • Add service detection with WithService in go.opentelemetry.io/otel/sdk/resource. (#7642)
    • Add DefaultWithContext and EnvironmentWithContext in go.opentelemetry.io/otel/sdk/resource to support plumbing context.Context through default and environment detectors. (#8051)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp. (#8038)
    • Support attributes with empty value (attribute.EMPTY) in go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest. (#8038)
    • Add support for per-series start time tracking for cumulative metrics in go.opentelemetry.io/otel/sdk/metric. Set OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true to enable. (#8060)
    • Add WithCardinalityLimitSelector for metric reader for configuring cardinality limits specific to the instrument kind. (#7855)

    Changed

    • Introduce the EMPTY Type in go.opentelemetry.io/otel/attribute to reflect that an empty value is now a valid value, with INVALID remaining as a deprecated alias of EMPTY. (#8038)
    • Improve slice handling in go.opentelemetry.io/otel/attribute to optimize short slice values with fixed-size fast paths. (#8039)
    • Improve performance of span metric recording in go.opentelemetry.io/otel/sdk/trace by returning early if self-observability is not enabled. (#8067)
    • Improve formatting of metric data diffs in go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest. (#8073)

    Deprecated

    • Deprecate INVALID in go.opentelemetry.io/otel/attribute. Use EMPTY instead. (#8038)

    Fixed

    • Return spec-compliant TraceIdRatioBased description. This is a breaking behavioral change, but it is necessary to make the implementation spec-compliant. (#8027)
    • Fix a race condition in go.opentelemetry.io/otel/sdk/metric where the lastvalue aggregation could collect the value 0 even when no zero-value measurements were recorded. (#8056)
    • Limit HTTP response body to 4 MiB in go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp to mitigate excessive memory usage caused by a misconfigured or malicious server. Responses exceeding the limit are treated as non-retryable errors. (#8108)
    • Limit HTTP response body to 4 MiB in go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp to mitigate excessive memory usage caused by a misconfigured or malicious server. Responses exceeding the limit are treated as non-retryable errors. (#8108)
    • Limit HTTP response body to 4 MiB in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp to mitigate excessive memory usage caused by a misconfigured or malicious server. Responses exceeding the limit are treated as non-retryable errors. (#8108)
    • WithHostID detector in go.opentelemetry.io/otel/sdk/resource to use full path for kenv command on BSD. (#8113)
    • Fix missing request.GetBody in go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp to correctly handle HTTP2 GOAWAY frame. (#8096)
    Commits
    • 9276201 Release v1.43.0 / v0.65.0 / v0.19.0 (#8128)
    • 61b8c94 chore(deps): update module github.com/mattn/go-runewidth to v0.0.22 (#8131)
    • 97a086e chore(deps): update github.com/golangci/dupl digest to c99c5cf (#8122)
    • 5e363de limit response body size for OTLP HTTP exporters (#8108)
    • 35214b6 Use an absolute path when calling bsd kenv (#8113)
    • 290024c fix(deps): update module google.golang.org/grpc to v1.80.0 (#8121)
    • e70658e fix: support getBody in otelploghttp (#8096)
    • 4afe468 fix(deps): update googleapis to 9d38bb4 (#8117)
    • b9ca729 chore(deps): update module github.com/go-git/go-git/v5 to v5.17.2 (#8115)
    • 69472ec chore(deps): update fossas/fossa-action action to v1.9.0 (#8118)
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.opentelemetry.io/otel/sdk&package-manager=go_modules&previous-version=1.42.0&new-version=1.43.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
    Dependabot commands and options
    You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/honeycombio/refinery/network/alerts).
    Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index f8ef965331..082bc3718e 100644 --- a/go.mod +++ b/go.mod @@ -38,14 +38,14 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 - go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 - go.opentelemetry.io/otel/metric v1.42.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/sdk/metric v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel/metric v1.43.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/proto/otlp v1.9.0 golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b google.golang.org/grpc v1.79.3 @@ -113,7 +113,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.32.0 golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.41.0 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect diff --git a/go.sum b/go.sum index 3a0d1e634d..af5b336f03 100644 --- a/go.sum +++ b/go.sum @@ -259,22 +259,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 h1:40vBjolEOioNBl8zPj1wxqlA7kJ82RxR4HnUv7W8zRI= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0/go.mod h1:4wAsc1dEVb4D1ZykBNC9AriTU9uLYtmziLrB+7G4lb4= go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 h1:yXinc284C6bmzA1r9jk7MxAhrBIIOH3qwmqwBmylZrA= @@ -321,8 +321,8 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= From 3f4dbef699a5ab5836798d9f0eb6c3d2f406b782 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:44:12 -0400 Subject: [PATCH 18/35] maint(deps): bump the minor-patch group across 1 directory with 12 updates (#1812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-patch group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.4` | `1.18.5` | | [github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest](https://github.com/open-telemetry/opentelemetry-collector-contrib) | `0.147.0` | `0.149.0` | | [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) | `2.2.4` | `2.3.0` | | [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp](https://github.com/open-telemetry/opentelemetry-go) | `1.42.0` | `1.43.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://github.com/open-telemetry/opentelemetry-go) | `1.42.0` | `1.43.0` | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) | `1.42.0` | `1.43.0` | Updates `github.com/klauspost/compress` from 1.18.4 to 1.18.5
    Release notes

    Sourced from github.com/klauspost/compress's releases.

    v1.18.5

    What's Changed

    Full Changelog: https://github.com/klauspost/compress/compare/v1.18.4...v1.18.5

    Commits

    Updates `github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest` from 0.147.0 to 0.149.0
    Release notes

    Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's releases.

    v0.149.0

    The OpenTelemetry Collector Contrib contains everything in the opentelemetry-collector release, be sure to check the release notes there as well.

    End User Changelog

    🛑 Breaking changes 🛑

    • exporter/elasticsearch: Remove host.os.type encoding in ECS mode (#46900) Use processor/elasticapmprocessor v0.36.2 or later for host.os.type enrichment
    • receiver/prometheus: Remove the deprecated report_extra_scrape_metrics receiver configuration option and obsolete extra scrape metric feature gates. (#44181) report_extra_scrape_metrics is no longer accepted in prometheusreceiver configuration. Control extra scrape metrics through the PromConfig.ScrapeConfigs.ExtraScrapeMetrics setting instead.

    🚩 Deprecations 🚩

    • receiver/awsfirehose: Deprecate built-in unmarshalers (cwlogs, cwmetrics, otlp_v1) in favor of encoding extensions. (#45830) Use the aws_logs_encoding extension (format: cloudwatch) instead of cwlogs, and the awscloudwatchmetricstreams_encoding extension instead of cwmetrics (format: json) or otlp_v1 (format: opentelemetry1.0).

    • receiver/file_log: Rename filelog receiver to file_log with deprecated alias filelog (#45339)

    • receiver/kafka: Deprecate the built-in azure_resource_logs encoding in favour of azureencodingextension. (#46267) The built-in azure_resource_logs encoding does not support all timestamp formats emitted by Azure services (e.g. US-format timestamps from Azure Functions). Users should migrate to the azureencodingextension, which provides full control over time formats and is actively maintained.

    💡 Enhancements 💡

    • cmd/opampsupervisor: Add configuration validation before applying remote config to prevent collector downtime (#41068) Validates collector configurations before applying them, preventing downtime from invalid remote configs. Disabled by default. Enable via agent.validate_config: true. May produce false positives when resources like ports are temporarily unavailable during validation.

    • connector/datadog: Document datadog connector is not supported in aix environments (#47010) Explicitly opt out of host metadata computation in datadog components to support AIX compilation target.

    • connector/signal_to_metrics: Add keys_expression support in include_resource_attributes and attributes for dynamic attribute key resolution at runtime (#46884) The keys_expression field allows specifying an OTTL value expression that resolves to a list of attribute keys at runtime. This enables dynamic resource attribute filtering based on runtime data such as client metadata. Exactly one of key or keys_expression must be set per entry.

    • connector/signal_to_metrics: Reduce per-signal allocations in the hot path by replacing attribute map allocation with a pooled hash-based ID check, and caching filtered resource attributes per metric definition within each resource batch. (#47197)

    • connector/signal_to_metrics: Pre-compute prefixed collector key to avoid a string allocation on every signal processed. (#47183) Pre-computing the collector key avoids a string allocation on every signal processed.

    • exporter/datadog: Document datadog exporter is not supported in aix environments (#47010) Explicitly opt out of host metadata computation in datadog components to support AIX compilation target.

    ... (truncated)

    Changelog

    Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's changelog.

    v0.149.0

    🛑 Breaking changes 🛑

    • pkg/stanza: Change signature of adapter.NewFactory to accept xreceiver.FactoryOptions (#45339) While the change is technically breaking, the existing calls to adapter.NewFactory will continue to work.

    💡 Enhancements 💡

    • pkg/expohisto: Move Go exponential histogram data structure into collector-contrib repository (#46646)

    • receiver/docker_stats: Add TLS configuration support for connecting to the Docker daemon over HTTPS with client and server certificates. (#33557) A new optional tls configuration block is available in docker_stats receiver config (and the shared internal/docker package). When omitted the connection remains insecure (plain HTTP or Unix socket), preserving existing behavior. When provided it supports the standard configtls.ClientConfig fields: ca_file, cert_file, key_file, insecure_skip_verify, min_version, and max_version. A warning is now emitted when a plain tcp:// or http:// endpoint is used without TLS, reflecting Docker's deprecation of unauthenticated TCP connections since Docker v26.0 (see https://docs.docker.com/engine/deprecated/#unauthenticated-tcp-connections).

    • receiver/docker_stats: Add "stream_stats" config option to maintain a persistent Docker stats stream per container instead of opening a new connection on every scrape cycle. (#46493) When stream_stats: true is set, each container maintains a persistent open Docker stats stream instead of opening and closing a new connection on every scrape cycle. The scraper reads from the cached latest value, which reduces connection overhead.

    • receiver/sqlquery: Add row_condition to metric configuration for filtering result rows by column value (#45862) Enables extracting individual metrics from pivot-style result sets where each row represents a different metric (e.g. pgbouncer's SHOW LISTS command). When row_condition is configured on a metric, only rows where the specified column equals the specified value are used; all other rows are silently skipped.

    v0.148.0

    💡 Enhancements 💡

    • pkg/azurelogs: Remove semconv v1.28.0 and v1.34.0 dependencies, migrating to v1.38.0 via paired feature gates (#45033, #45034) Two new alpha feature gates control the migration: pkg.translator.azurelogs.EmitV1LogConventions emits stable attribute names (code.function.name, code.file.path, eventName per log record). pkg.translator.azurelogs.DontEmitV0LogConventions suppresses the old names (code.function, code.filepath, event.name on resource). Both gates default to off; enable EmitV1LogConventions first for a dual-emit migration window.

    • pkg/datadog: Expose feature gate to infer intervals for delta metrics. (#46851)

    • pkg/xstreamencoding: Add stream decoding adapters for unmarshaler interfaces (#46754)

    • processor/tail_sampling: Add hooks to call when a sampling decision is made for a trace. (#46161)

    • receiver/github: Enables dynamic metric reaggregation in the GitHub receiver. This does not break existing configuration files. (#46385)

    Commits
    • 262df71 [chore] Prepare release 0.149.0 (#47280)
    • 4158922 [chore] update dependency (#47273)
    • 9a9c779 fix(deps): update module golang.org/x/sync to v0.20.0 (#47266)
    • 4c1d5a1 [receiver/mysql] Adding trace propagation (#46327)
    • d660724 fix(deps): update module github.com/hetznercloud/hcloud-go/v2 to v2.37.0 (#47...
    • 0cee133 fix(deps): update module github.com/stretchr/testify to v1.11.1 (#47263)
    • 33d749a fix(deps): update module github.com/googleapis/gax-go/v2 to v2.20.0 (#47260)
    • 12912f3 fix(deps): update module github.com/hashicorp/go-version to v1.9.0 (#47261)
    • 173558b [chore] [sentryexporter] Add giortzisg to CODEOWNERS (#47258)
    • 1647290 [receiver/hostmetrics] restrict system.processes.created to Linux and OpenBSD...
    • Additional commits viewable in compare view

    Updates `github.com/pelletier/go-toml/v2` from 2.2.4 to 2.3.0
    Release notes

    Sourced from github.com/pelletier/go-toml/v2's releases.

    v2.3.0

    This is the first release built largely with the help of AI coding agents. Highlights include the complete removal of the unsafe package. go-toml is now fully safe Go code, with a geomean overhead of only ~1.4% vs v2.2.4 and zero additional allocations on benchmarks. This release also adds omitzero struct tag support, improves UnmarshalText/Unmarshaler handling for tables and array tables, and fixes several bugs including nil pointer marshaling, leap second handling, and datetime unmarshaling panics.

    What's Changed

    What's new

    Fixed bugs

    Documentation

    Other changes

    New Contributors

    Full Changelog: https://github.com/pelletier/go-toml/compare/v2.2.4...v2.3.0

    Commits
    • f36a3ec Reduce marshal and unmarshal overhead (#1044)
    • 77f3862 Fix benchmark script replacing internal package imports (#1042)
    • 16b1ef5 Fix parser error pointing to wrong line when last line has no trailing newlin...
    • e14bde7 build(deps): bump docker/login-action from 3 to 4 (#1039)
    • 4b1ff01 build(deps): bump docker/setup-buildx-action from 3 to 4 (#1040)
    • 048a25f Go 1.26 (#1030)
    • b357558 build(deps): bump goreleaser/goreleaser-action from 6 to 7 (#1035)
    • a0be52f build(deps): bump actions/upload-artifact from 6 to 7 (#1036)
    • 316bfc6 Support Unmarshaler interface for tables and array tables (#1027)
    • 2edc61f Fix panic when unmarshaling datetime values to incompatible types (#1028) (#1...
    • Additional commits viewable in compare view

    Updates `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc` from 0.67.0 to 0.68.0
    Release notes

    Sourced from go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc's releases.

    Release v1.43.0/v2.5.0/v0.68.0/v0.37.0/v0.23.0/v0.18.0/v0.16.0/v0.15.0

    Added

    • Add Resource method to SDK in go.opentelemetry.io/contrib/otelconf/v0.3.0 to expose the resolved SDK resource from declarative configuration. (#8660)
    • Add support to set the configuration file via OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    • Add support for service resource detector in go.opentelemetry.io/contrib/otelconf. (#8674)
    • Add support for attribute_count_limit and attribute_value_length_limit in tracer provider configuration in go.opentelemetry.io/contrib/otelconf. (#8687)
    • Add support for attribute_count_limit and attribute_value_length_limit in logger provider configuration in go.opentelemetry.io/contrib/otelconf. (#8686)
    • Add support for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)
    • Add support for OTEL_SEMCONV_STABILITY_OPT_IN in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. Supported values are rpc (default), rpc/dup and rpc/old. (#8726)
    • Add the http.route metric attribute to go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8632)

    Changed

    • Prepend _ to the normalized environment variable name when the key starts with a digit in go.opentelemetry.io/contrib/propagators/envcar, ensuring POSIX compliance. (#8678)
    • Move experimental types from go.opentelemetry.io/contrib/otelconf to go.opentelemetry.io/contrib/otelconf/x. (#8529)
    • Normalize cached environment variable names in go.opentelemetry.io/contrib/propagators/envcar, aligning Carrier.Keys output with the carrier's normalized key format. (#8761)

    Fixed

    • Fix go.opentelemetry.io/contrib/otelconf Prometheus reader converting OTel dot-style label names (e.g. service.name) to underscore-style (service_name) in target_info when both without_type_suffix and without_units are set. Use NoTranslation instead of UnderscoreEscapingWithoutSuffixes to preserve dot-style label names while still suppressing metric name suffixes. (#8763)
    • Limit the request body size at 1MB in go.opentelemetry.io/contrib/zpages. (#8656)
    • Fix server spans using the client's address and port for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)

    Removed

    • Host ID resource detector has been removed when configuring the host resource detector in go.opentelemetry.io/contrib/otelconf. (#8581)

    Deprecated

    • Deprecate OTEL_EXPERIMENTAL_CONFIG_FILE in favour of OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)

    What's Changed

    ... (truncated)

    Changelog

    Sourced from go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc's changelog.

    [1.43.0/2.5.0/0.68.0/0.37.0/0.23.0/0.18.0/0.16.0/0.15.0] - 2026-04-03

    Added

    • Add Resource method to SDK in go.opentelemetry.io/contrib/otelconf/v0.3.0 to expose the resolved SDK resource from declarative configuration. (#8660)
    • Add support to set the configuration file via OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    • Add support for service resource detector in go.opentelemetry.io/contrib/otelconf. (#8674)
    • Add support for attribute_count_limit and attribute_value_length_limit in tracer provider configuration in go.opentelemetry.io/contrib/otelconf. (#8687)
    • Add support for attribute_count_limit and attribute_value_length_limit in logger provider configuration in go.opentelemetry.io/contrib/otelconf. (#8686)
    • Add support for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)
    • Add support for OTEL_SEMCONV_STABILITY_OPT_IN in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. Supported values are rpc (default), rpc/dup and rpc/old. (#8726)
    • Add the http.route metric attribute to go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8632)

    Changed

    • Prepend _ to the normalized environment variable name when the key starts with a digit in go.opentelemetry.io/contrib/propagators/envcar, ensuring POSIX compliance. (#8678)
    • Move experimental types from go.opentelemetry.io/contrib/otelconf to go.opentelemetry.io/contrib/otelconf/x. (#8529)
    • Normalize cached environment variable names in go.opentelemetry.io/contrib/propagators/envcar, aligning Carrier.Keys output with the carrier's normalized key format. (#8761)

    Fixed

    • Fix go.opentelemetry.io/contrib/otelconf Prometheus reader converting OTel dot-style label names (e.g. service.name) to underscore-style (service_name) in target_info when both without_type_suffix and without_units are set. Use NoTranslation instead of UnderscoreEscapingWithoutSuffixes to preserve dot-style label names while still suppressing metric name suffixes. (#8763)
    • Limit the request body size at 1MB in go.opentelemetry.io/contrib/zpages. (#8656)
    • Fix server spans using the client's address and port for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)

    Removed

    • Host ID resource detector has been removed when configuring the host resource detector in go.opentelemetry.io/contrib/otelconf. (#8581)

    Deprecated

    • Deprecate OTEL_EXPERIMENTAL_CONFIG_FILE in favour of OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    Commits
    • 45977a4 Release v1.43.0/v2.5.0/v0.68.0/v0.37.0/v0.23.0/v0.18.0/v0.16.0/v0.15.0 (#8769)
    • 0fcc152 fix(deps): update module github.com/googlecloudplatform/opentelemetry-operati...
    • eaba3cd chore(deps): update googleapis to 6f92a3b (#8776)
    • 6df430c chore(deps): update module github.com/jgautheron/goconst to v1.10.0 (#8771)
    • ae90e32 Fix otelconf prometheus label escaping (#8763)
    • f202c3f otelconf: move experimental types to otelconf/x (#8529)
    • 8ddaece fix(deps): update aws-sdk-go-v2 monorepo (#8764)
    • c7c03a4 chore(deps): update module github.com/mattn/go-runewidth to v0.0.22 (#8766)
    • 717a85a envcar: normalize cached environment variable names (#8761)
    • ad990b6 fix(deps): update module github.com/aws/smithy-go to v1.24.3 (#8765)
    • Additional commits viewable in compare view

    Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.67.0 to 0.68.0
    Release notes

    Sourced from go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp's releases.

    Release v1.43.0/v2.5.0/v0.68.0/v0.37.0/v0.23.0/v0.18.0/v0.16.0/v0.15.0

    Added

    • Add Resource method to SDK in go.opentelemetry.io/contrib/otelconf/v0.3.0 to expose the resolved SDK resource from declarative configuration. (#8660)
    • Add support to set the configuration file via OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    • Add support for service resource detector in go.opentelemetry.io/contrib/otelconf. (#8674)
    • Add support for attribute_count_limit and attribute_value_length_limit in tracer provider configuration in go.opentelemetry.io/contrib/otelconf. (#8687)
    • Add support for attribute_count_limit and attribute_value_length_limit in logger provider configuration in go.opentelemetry.io/contrib/otelconf. (#8686)
    • Add support for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)
    • Add support for OTEL_SEMCONV_STABILITY_OPT_IN in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. Supported values are rpc (default), rpc/dup and rpc/old. (#8726)
    • Add the http.route metric attribute to go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8632)

    Changed

    • Prepend _ to the normalized environment variable name when the key starts with a digit in go.opentelemetry.io/contrib/propagators/envcar, ensuring POSIX compliance. (#8678)
    • Move experimental types from go.opentelemetry.io/contrib/otelconf to go.opentelemetry.io/contrib/otelconf/x. (#8529)
    • Normalize cached environment variable names in go.opentelemetry.io/contrib/propagators/envcar, aligning Carrier.Keys output with the carrier's normalized key format. (#8761)

    Fixed

    • Fix go.opentelemetry.io/contrib/otelconf Prometheus reader converting OTel dot-style label names (e.g. service.name) to underscore-style (service_name) in target_info when both without_type_suffix and without_units are set. Use NoTranslation instead of UnderscoreEscapingWithoutSuffixes to preserve dot-style label names while still suppressing metric name suffixes. (#8763)
    • Limit the request body size at 1MB in go.opentelemetry.io/contrib/zpages. (#8656)
    • Fix server spans using the client's address and port for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)

    Removed

    • Host ID resource detector has been removed when configuring the host resource detector in go.opentelemetry.io/contrib/otelconf. (#8581)

    Deprecated

    • Deprecate OTEL_EXPERIMENTAL_CONFIG_FILE in favour of OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)

    What's Changed

    ... (truncated)

    Changelog

    Sourced from go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp's changelog.

    [1.43.0/2.5.0/0.68.0/0.37.0/0.23.0/0.18.0/0.16.0/0.15.0] - 2026-04-03

    Added

    • Add Resource method to SDK in go.opentelemetry.io/contrib/otelconf/v0.3.0 to expose the resolved SDK resource from declarative configuration. (#8660)
    • Add support to set the configuration file via OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    • Add support for service resource detector in go.opentelemetry.io/contrib/otelconf. (#8674)
    • Add support for attribute_count_limit and attribute_value_length_limit in tracer provider configuration in go.opentelemetry.io/contrib/otelconf. (#8687)
    • Add support for attribute_count_limit and attribute_value_length_limit in logger provider configuration in go.opentelemetry.io/contrib/otelconf. (#8686)
    • Add support for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)
    • Add support for OTEL_SEMCONV_STABILITY_OPT_IN in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. Supported values are rpc (default), rpc/dup and rpc/old. (#8726)
    • Add the http.route metric attribute to go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp. (#8632)

    Changed

    • Prepend _ to the normalized environment variable name when the key starts with a digit in go.opentelemetry.io/contrib/propagators/envcar, ensuring POSIX compliance. (#8678)
    • Move experimental types from go.opentelemetry.io/contrib/otelconf to go.opentelemetry.io/contrib/otelconf/x. (#8529)
    • Normalize cached environment variable names in go.opentelemetry.io/contrib/propagators/envcar, aligning Carrier.Keys output with the carrier's normalized key format. (#8761)

    Fixed

    • Fix go.opentelemetry.io/contrib/otelconf Prometheus reader converting OTel dot-style label names (e.g. service.name) to underscore-style (service_name) in target_info when both without_type_suffix and without_units are set. Use NoTranslation instead of UnderscoreEscapingWithoutSuffixes to preserve dot-style label names while still suppressing metric name suffixes. (#8763)
    • Limit the request body size at 1MB in go.opentelemetry.io/contrib/zpages. (#8656)
    • Fix server spans using the client's address and port for server.address and server.port attributes in go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc. (#8723)

    Removed

    • Host ID resource detector has been removed when configuring the host resource detector in go.opentelemetry.io/contrib/otelconf. (#8581)

    Deprecated

    • Deprecate OTEL_EXPERIMENTAL_CONFIG_FILE in favour of OTEL_CONFIG_FILE in go.opentelemetry.io/contrib/otelconf. (#8639)
    Commits
    • 45977a4 Release v1.43.0/v2.5.0/v0.68.0/v0.37.0/v0.23.0/v0.18.0/v0.16.0/v0.15.0 (#8769)
    • 0fcc152 fix(deps): update module github.com/googlecloudplatform/opentelemetry-operati...
    • eaba3cd chore(deps): update googleapis to 6f92a3b (#8776)
    • 6df430c chore(deps): update module github.com/jgautheron/goconst to v1.10.0 (#8771)
    • ae90e32 Fix otelconf prometheus label escaping (#8763)
    • f202c3f otelconf: move experimental types to otelconf/x (#8529)
    • 8ddaece fix(deps): update aws-sdk-go-v2 monorepo (#8764)
    • c7c03a4 chore(deps): update module github.com/mattn/go-runewidth to v0.0.22 (#8766)
    • 717a85a envcar: normalize cached environment variable names (#8761)
    • ad990b6 fix(deps): update module github.com/aws/smithy-go to v1.24.3 (#8765)
    • Additional commits viewable in compare view

    Updates `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` from 1.42.0 to 1.43.0
    Changelog

    Sourced from go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp's changelog.

    [1.43.0/0.65.0/0.19.0] 2026-04-02

    Added

    • Add IsRandom and WithRandom on TraceFlags, and IsRandom on SpanContext in go.opentelemetry.io/otel/trace for W3C Trace Context Level 2 Random Trace ID Flag support. (#8012)
    • Add service detection with WithService in go.opentelemetry.io/otel/sdk/resource. (#7642)
    • Add DefaultWithContext and EnvironmentWithContext in go.opentelemetry.io/otel/sdk/resource to support plumbing context.Context throug... _Description has been truncated_ --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yingrong <22300958+VinozzZ@users.noreply.github.com> --- go.mod | 42 +++++++-------- go.sum | 108 +++++++++++++++++++-------------------- route/otlp_trace_test.go | 2 +- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/go.mod b/go.mod index 082bc3718e..38bf4480e7 100644 --- a/go.mod +++ b/go.mod @@ -20,11 +20,11 @@ require ( github.com/jessevdk/go-flags v1.6.1 github.com/jonboulle/clockwork v0.5.0 github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.18.4 + github.com/klauspost/compress v1.18.5 github.com/open-telemetry/opamp-go v0.23.0 - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0 github.com/panmari/cuckoofilter v1.0.6 - github.com/pelletier/go-toml/v2 v2.2.4 + github.com/pelletier/go-toml/v2 v2.3.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 @@ -36,19 +36,19 @@ require ( github.com/tinylib/msgp v1.6.3 github.com/valyala/fastjson v1.6.10 github.com/vmihailenco/msgpack/v5 v5.4.1 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 go.opentelemetry.io/otel v1.43.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 - go.opentelemetry.io/proto/otlp v1.9.0 + go.opentelemetry.io/proto/otlp v1.10.0 golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b - google.golang.org/grpc v1.79.3 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -62,7 +62,7 @@ require ( github.com/hashicorp/go-version v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 // indirect github.com/otiai10/copy v1.10.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -72,13 +72,13 @@ require ( github.com/stretchr/objx v0.5.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/featuregate v1.53.0 // indirect - go.opentelemetry.io/collector/pdata/xpdata v0.147.0 // indirect + go.opentelemetry.io/collector/featuregate v1.55.0 // indirect + go.opentelemetry.io/collector/pdata/xpdata v0.149.0 // indirect go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 // indirect go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sync v0.19.0 // indirect + golang.org/x/sync v0.20.0 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect k8s.io/klog/v2 v2.90.1 // indirect ) @@ -109,15 +109,15 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - go.opentelemetry.io/collector/pdata v1.53.0 + go.opentelemetry.io/collector/pdata v1.55.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.32.0 - golang.org/x/net v0.51.0 // indirect + golang.org/x/mod v0.33.0 + golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.41.0 - google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.42.0 + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect ) tool github.com/google/go-licenses/v2 diff --git a/go.sum b/go.sum index af5b336f03..e109c15e20 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -161,12 +161,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/open-telemetry/opamp-go v0.23.0 h1:k7h7w/muprut9/DAhUC4anX4v7hIdgO02gIsSjV4uq0= github.com/open-telemetry/opamp-go v0.23.0/go.mod h1:DIIVdkLefdqPW5L+4I2twmAicVrTB0Bp5XJAfedZzAM= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0 h1:nxCNHHUItl2j0sjknI/mRbBBcQCxu0yv3baii9GNB1U= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.147.0/go.mod h1:LrW8KarPjlu+1VdP2t6kjJeOTF+y3/n2wCZAdc/NWg0= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0 h1:jgmHvcC3WCrkA49VBm/Tay6O5OEaLvevlqd+OEoPI3M= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.147.0/go.mod h1:NMXNbNZ4aEhyW0Oe4BfbGLLOI8Y9FmB/unZp01HUlKU= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0 h1:EYy8gmyjGLS0iYV7ksOOHrjZgiTjbWU26vziBAt4jKI= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.147.0/go.mod h1:VDqy65biIJI9iYN9rrVi2nm9KAvSfq+6Fzrm8WyL4Qw= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.149.0 h1:EuXIJolnTL+oBFzF0almZEkHiV4thwnSjEcr3L5nNu0= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.149.0/go.mod h1:VKgoaDIUxOoTiskp7HK7ESS+CgsgoNKD2PgzF0wRXvQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0 h1:2VC/s/j8LFVE9+CvoNhjAPtKHgDAOgRb/JXkRdrPUnI= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0/go.mod h1:sycQ9JOpSQY+iTiDOVcQL84TgYUIj7fF3z5Yc6nOwnc= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0 h1:OZKthV+cLQO5MCFhBQme3AveZ5vorqaFwb0Qn8jvSQQ= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0/go.mod h1:eB74l+/1nW5tofwCjD5TKRqHFYnBSWo0j0xWD8BHYuE= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 h1:lFowWhr/qx5Gm2X8H0BbG87xZh/e+4S0PQw8HQO5D4Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0/go.mod h1:JybcaNLHHzJQh690eSp+KDbLrxB1+AhKNLlibqrogt4= github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ= @@ -175,8 +175,8 @@ github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/panmari/cuckoofilter v1.0.6 h1:WKb1aSj16h22x0CKVtTCaRkJiCnVGPLEMGbNY8xwXf8= github.com/panmari/cuckoofilter v1.0.6/go.mod h1:bKADbQPGbN6TxUvo/IbMEIUbKuASnpsOvrLTgpSX0aU= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -245,28 +245,28 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= -go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= -go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU= -go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= -go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= -go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= -go.opentelemetry.io/collector/pdata/pprofile v0.147.0 h1:yQS3RBvcvRcy9N7AnJvsxmse0AxJcRqBZfwMA22xBA8= -go.opentelemetry.io/collector/pdata/pprofile v0.147.0/go.mod h1:pm9mUqHNpT1SaCkxILu4FW1BvMAelh7EKhpSKe2KJIQ= -go.opentelemetry.io/collector/pdata/xpdata v0.147.0 h1:JZPYCIrIhmpmUJ1SNkGv13LQykBPY9eLpC+kQm8fex0= -go.opentelemetry.io/collector/pdata/xpdata v0.147.0/go.mod h1:w3iv1rH00eMB/7lYBn9dDJuYujJUpgca5Zoz3KDLgrc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/collector/featuregate v1.55.0 h1:s/bE8135+8GZpVlQ9qLXQjvprE9KNOGsLhNkqm+EDEU= +go.opentelemetry.io/collector/featuregate v1.55.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= +go.opentelemetry.io/collector/internal/testutil v0.149.0 h1:OWfUPO3NFKSaJtz/SBZph/2ENHbr/VbzzlBadKUhm8o= +go.opentelemetry.io/collector/internal/testutil v0.149.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.55.0 h1:WBgye8bo8koUyV9Vmp/r2Q3lgDezdsgfKDQAaM1oT2I= +go.opentelemetry.io/collector/pdata v1.55.0/go.mod h1:6jPrbM4tuliCPACDznjFtxnnHisfKfzwrBVoeuESYuk= +go.opentelemetry.io/collector/pdata/pprofile v0.149.0 h1:4/uI7wsgMnmBZm6Z/VNY6sWnaFN09+Nk3jr7XEmTtOk= +go.opentelemetry.io/collector/pdata/pprofile v0.149.0/go.mod h1:4uprs5wMp4MI1/bcP5mYERfobFxBn+QoeNFQBUSVk/U= +go.opentelemetry.io/collector/pdata/xpdata v0.149.0 h1:crfGmh5LsOAVc1ImdnPIUTMmHbOGYmWXgOyWSDnAKyw= +go.opentelemetry.io/collector/pdata/xpdata v0.149.0/go.mod h1:YgOtcDn7E/4dHw0/Yy/PvSa3GLqMKKAIikzBPM+ML2g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -279,12 +279,12 @@ go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 h1:40vBjo go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0/go.mod h1:4wAsc1dEVb4D1ZykBNC9AriTU9uLYtmziLrB+7G4lb4= go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 h1:yXinc284C6bmzA1r9jk7MxAhrBIIOH3qwmqwBmylZrA= go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0/go.mod h1:ygxocDWPB6Y6bySAjxmHyTebjAJ8jcEUAZc03gu1pxk= -go.opentelemetry.io/proto/slim/otlp v1.9.0 h1:fPVMv8tP3TrsqlkH1HWYUpbCY9cAIemx184VGkS6vlE= -go.opentelemetry.io/proto/slim/otlp v1.9.0/go.mod h1:xXdeJJ90Gqyll+orzUkY4bOd2HECo5JofeoLpymVqdI= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0 h1:o13nadWDNkH/quoDomDUClnQBpdQQ2Qqv0lQBjIXjE8= -go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.2.0/go.mod h1:Gyb6Xe7FTi/6xBHwMmngGoHqL0w29Y4eW8TGFzpefGA= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0 h1:EiUYvtwu6PMrMHVjcPfnsG3v+ajPkbUeH+IL93+QYyk= -go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.2.0/go.mod h1:mUUHKFiN2SST3AhJ8XhJxEoeVW12oqfXog0Bo8W3Ec4= +go.opentelemetry.io/proto/slim/otlp v1.10.0 h1:iR97Vs/ZDR+y9TfuP9b1XBtdPWeC+OMslIBmhcLU7jM= +go.opentelemetry.io/proto/slim/otlp v1.10.0/go.mod h1:lV9250stpjYLPNA5viFabIgP2QlUGRT1GdTgAf8SIUk= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:RUF5rO0hAlgiJt1fzQVzcVs3vZVNHIcMLgOgG4rWNcQ= +go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= +go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -301,22 +301,22 @@ golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aC golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -325,34 +325,34 @@ golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/route/otlp_trace_test.go b/route/otlp_trace_test.go index 3a7412f4b9..dd5653a986 100644 --- a/route/otlp_trace_test.go +++ b/route/otlp_trace_test.go @@ -633,7 +633,7 @@ func TestOTLPHandler(t *testing.T) { event := events[0] // Note: GRPC clients override the user-agent header with their own value. // This is expected behavior and differs from HTTP where custom user-agents are preserved. - assert.Equal(t, "grpc-go/1.79.3", event.Data.MetaRefineryIncomingUserAgent) + assert.Equal(t, "grpc-go/1.80.0", event.Data.MetaRefineryIncomingUserAgent) }) t.Run("spans record incoming user agent - HTTP", func(t *testing.T) { From ca947380bf840c0461cc9cede2d826218a6d461f Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:30:05 -0400 Subject: [PATCH 19/35] maint: prepare for release v3.2.0 (#1813) ## Which problem is this PR solving? Update all releasing docs for v3.2.0 ## Short description of the changes - update CHANGLOG.md - update RELEASING_NOTE.md --- CHANGELOG.md | 18 ++++++++++++++++++ RELEASE_NOTES.md | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acc7d9acb..069794cd72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Refinery Changelog +## 3.2.0 2026-04-13 + +### 💡 Enhancements + +- feat: add ReceiveKeyIDs config option for key ID-based authorization by @tdarwin in https://github.com/honeycombio/refinery/pull/1803 +- feat: add OTelMetrics.AdditionalAttributes config option by @tdarwin in https://github.com/honeycombio/refinery/pull/1804 +- feat: add granular event metrics by @tdarwin in https://github.com/honeycombio/refinery/pull/1805 + +### 🐛 Fixes + +- fix: include AdditionalErrorFields in logs for transmission code by @VinozzZ in https://github.com/honeycombio/refinery/pull/1807 + +### 🛠 Maintenance + +- fix: update ko build tooling and fix flaky integration test by @tdarwin in https://github.com/honeycombio/refinery/pull/1806 +- maint(deps): bump go.opentelemetry.io/otel/sdk from 1.42.0 to 1.43.0 by @dependabot in https://github.com/honeycombio/refinery/pull/1810 +- maint(deps): bump the minor-patch group across 1 directory with 12 updates by @dependabot in https://github.com/honeycombio/refinery/pull/1812 + ## 3.1.2 2026-03-25 This release addresses security vulnerabilities CVE-2026-27139, CVE-2026-27142, and CVE-2026-25679. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cf4695e40e..e686842418 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,23 @@ While [CHANGELOG.md](./CHANGELOG.md) contains detailed documentation and links to all the source code changes in a given release, this document is intended to be aimed at a more comprehensible version of the contents of the release from the point of view of users of Refinery. +## Version 3.2.0 + +This release adds new configuration options for authorization and observability. + +### Configuration Changes + +* **Added**: `AccessKeys.ReceiveKeyIDs` - authorizes incoming traffic by Honeycomb ingest key IDs (obtained from the `/1/auth` endpoint) instead of requiring full API keys. Supports live reload alongside the existing `ReceiveKeys` option. +* **Added**: `OTelMetrics.AdditionalAttributes` - injects custom resource attributes (e.g., cluster ID, environment name) into all OTLP metrics emitted by Refinery. Supplied as comma-separated `key:value` pairs. + +### New Metrics + +* `events_dropped` - Counter tracking the number of events dropped by Refinery. + +### Fixes + +* Fixed `AdditionalErrorFields` not being included in transmission error logs. + ## Version 3.1.2 This patch release primarily addresses security vulnerabilities in dependencies. From cc93b75c13fd66f32330586f7cca78ca70d2fff1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 11:39:42 -0400 Subject: [PATCH 20/35] maint(deps): bump the minor-patch group with 8 updates (#1814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minor-patch group with 8 updates: | Package | From | To | | --- | --- | --- | | [github.com/honeycombio/libhoney-go](https://github.com/honeycombio/libhoney-go) | `1.26.0` | `1.27.1` | | [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.5` | `1.18.6` | | [github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest](https://github.com/open-telemetry/opentelemetry-collector-contrib) | `0.149.0` | `0.151.0` | | [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) | `9.18.0` | `9.19.0` | | [github.com/tinylib/msgp](https://github.com/tinylib/msgp) | `1.6.3` | `1.6.4` | | [go.opentelemetry.io/collector/pdata](https://github.com/open-telemetry/opentelemetry-collector) | `1.55.0` | `1.57.0` | | [golang.org/x/mod](https://github.com/golang/mod) | `0.33.0` | `0.35.0` | | [golang.org/x/tools](https://github.com/golang/tools) | `0.42.0` | `0.43.0` | Updates `github.com/honeycombio/libhoney-go` from 1.26.0 to 1.27.1
      Release notes

      Sourced from github.com/honeycombio/libhoney-go's releases.

      v1.27.1

      Fixes

      • AddFields: Allocate a correctly-sized new map instead of swapping the caller's map pointer, which mutated the caller's map as a side effect (#279)

      Install / Upgrade

      go get github.com/honeycombio/libhoney-go@v1.27.1
      

      v1.27.0

      Enhancements

      Maintenance

      Changelog

      Sourced from github.com/honeycombio/libhoney-go's changelog.

      1.27.1 2026-04-14

      Fixes

      • perf: allocate correctly-sized map in AddFields to avoid rehashing and caller map mutation (#279) | @​lizthegrey

      1.27.0 2026-04-13

      Enhancements

      Maintenance

      Commits
      • 02e9dbf chore: prepare v1.27.1 release (#280)
      • 0771813 perf: allocate correctly-sized map in AddFields to avoid rehashing (#279)
      • ceeb7d9 chore: prepare v1.27.0 release (#277)
      • 271f365 maint(deps): bump github.com/klauspost/compress from 1.17.11 to 1.18.5 (#274)
      • 4199724 maint: bump minimum Go version to 1.24 (#276)
      • a4a7ffd maint(deps): bump github.com/stretchr/testify from 1.10.0 to 1.11.1 (#268)
      • da78507 feat: add bulk AddFields method to Event and fieldHolder (#275)
      • 2863df8 ci(OTEL-125): remove pipeline-team dependabot reviewer (#273)
      • See full diff in compare view

      Updates `github.com/klauspost/compress` from 1.18.5 to 1.18.6
      Release notes

      Sourced from github.com/klauspost/compress's releases.

      v1.18.6

      What's Changed

      New Contributors

      Full Changelog: https://github.com/klauspost/compress/compare/v1.18.5...v1.18.6

      Commits

      Updates `github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest` from 0.149.0 to 0.151.0
      Release notes

      Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's releases.

      v0.151.0

      The OpenTelemetry Collector Contrib contains everything in the opentelemetry-collector release, be sure to check the release notes there as well.

      End User Changelog

      🛑 Breaking changes 🛑

      • all: Removed DNS lookup processor skeleton. (#47874)

      • connector/datadog: Remove stable feature gate connector.datadogconnector.NativeIngest (#47580)

      • exporter/datadog: Remove stable feature gates exporter.datadogexporter.UseLogsAgentExporter and exporter.datadogexporter.metricexportnativeclient (#47583)

      • exporter/signalfx: Default api_url and ingest_url values derived from realm now use *.observability.splunkcloud.com instead of *.signalfx.com. (#47670) Explicit api_url and ingest_url settings are unchanged. Update network allowlists if they targeted only *.signalfx.com.

      • exporter/splunk_hec: Remove deprecated batcher config field. Use sending_queue::batch instead. (#47737)

      • extension/jaegerremotesampling: Remove replaceThriftWithProto feature gate. (#47553)

      • pkg/translator/prometheus: Removes pkg.translator.prometheus.NormalizeName feature gate which has been stable for some time. (#47597)

      • pkg/zipkin: Promote "pkg.translator.zipkin.DontEmitV0NetworkConventions" and "pkg.translator.zipkin.EmitV1NetworkConventions" feature gates to Beta. (#46682) This changes the default behavior to emit the new semantic convention attributes instead of the old deprecated ones. The Zipkin translator will now use network.local.address (replacing net.host.ip), network.peer.address (replacing net.peer.ip), and service.peer.name (replacing peer.service) by default when emitting spans.

      • processor/k8s_attributes: Disable otelcol.k8s.pod.association metric until pod_identifier attribute is properly calculated (#47669)

      • receiver/jaeger: Stabilize DisableRemoteSampling feature gate which has been in beta for over 2 years. (#47599)

      • receiver/prometheus: Remove receiver.prometheusreceiver.EnableNativeHistograms, receiver.prometheusreceiver.RemoveStartTimeAdjustment and receiver.prometheusreceiver.UseCreatedMetric feature gates. (#40606)

      • receiver/prometheus: Removes the feature gate receiver.prometheusreceiver.RemoveLegacyResourceAttributes which has been stable for some time. (#47598)

      🚩 Deprecations 🚩

      • connector/service_graph: Rename component type from servicegraph to service_graph to follow snake_case naming convention. The old name is kept as a deprecated alias. (#47971)
      • connector/span_metrics: Rename component type from spanmetrics to span_metrics to follow snake_case naming convention. The old name is kept as a deprecated alias. (#47963)
      • exporter/honeycomb_marker: Rename exporter type from honeycombmarker to honeycomb_marker (#45339)
      • exporter/prometheusremotewrite: add_metric_suffixes is deprecated. Use translation_strategy: UnderscoreEscapingWithoutSuffixes if you are setting this to false. (#33661)
      • extension/aws_logs_encoding: Deprecates transparent gzip decompression in aws_logs_encoding and clarifies that callers must decompress payloads before invoking the streaming decoder. (#46463)
      • processor/log_dedup: Rename processor type from logdedup to log_dedup (#45339)
      • receiver/file_stats: Rename filestats receiver to file_stats with deprecated alias filestats (#45339)
      • receiver/fluent_forward: Rename receiver type from fluentforward to fluent_forward (#45339)
      • receiver/host_metrics: Rename hostmetrics receiver to host_metrics and add deprecated alias hostmetrics (#45449)
      • receiver/k8s_objects: Rename k8sobjects receiver to k8s_objects and add deprecated alias k8sobjects. (#47440)
      • receiver/ssh_check: Rename sshcheck receiver to ssh_check with deprecated alias sshcheck (#45339)

      🚀 New components 🚀

      • extension/pebble_tail_storage: First PR for new Pebble tail storage extension (#47916)

      • processor/drain: Add drain processor that applies the Drain log clustering algorithm to annotate log records with a derived template string. (#47235) The processor sets log.record.template (e.g. "user <*> logged in from <*>") on each log record. Downstream processors such as the filter processor can act on this attribute to, for example, drop entire classes of noisy logs by template string.

        Key features:

        • Configurable Drain parse tree parameters (depth, similarity threshold, max clusters with LRU eviction)

      ... (truncated)

      Changelog

      Sourced from github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest's changelog.

      v0.151.0

      🛑 Breaking changes 🛑

      • exporter/splunk_hec: Remove deprecated batcher config field. Use sending_queue::batch instead. (#47737)

      • pkg/ottl: OTTL API breaking change in ottlscope.NewTransformContextPtr: the function signature now requires schema URL items for scope and resource. (#47784) What changed:

        • Old: NewTransformContextPtr(instrumentationScope, resource, schemaURLItem, options...)
        • New: NewTransformContextPtr(instrumentationScope, resource, scopeSchemaURLItem, resourceSchemaURLItem, options...)

        Migration:

        • If you previously passed one shared schema URL item, pass it to both new parameters.
        • If scope and resource schema URLs differ, pass distinct items for each.

        Example migration:

        • Before: tCtx := ottlscope.NewTransformContextPtr(scope, resource, schemaURLItem)
        • After (independent items): tCtx := ottlscope.NewTransformContextPtr(scope, resource, scopeSchemaURLItem, resourceSchemaURLItem)
      • pkg/stanza: Remove deprecated packages pkg/stanza/errors, pkg/stanza/operator/parser/json and pkg/stanza/operator/parser/time. (#45006) These packages were renamed to pkg/stanza/stanzaerrors, pkg/stanza/operator/parser/jsonparser and pkg/stanza/operator/parser/timeparser.

      • processor/filter: Change With*Functions and Default*Functions to use pointer-based transform context signatures (#47975) The filter processor function options With*Functions and Default*Functions now use pointer-based transform context signatures and are no longer deprecated. As a result, they are not compatible with older non-pointer signatures anymore and must be updated to use the new signature.

      • processor/transform: Change With*Functions and Default*Functions to use pointer-based transform context signatures (#47970) The transform processor function options With*Functions and Default*Functions now use pointer-based transform context signatures and are no longer deprecated. As a result, they are not compatible with older non-pointer signatures anymore and must be updated to use the new signature.

      • receiver/prometheus: Remove receiver.prometheusreceiver.EnableNativeHistograms, receiver.prometheusreceiver.RemoveStartTimeAdjustment and receiver.prometheusreceiver.UseCreatedMetric feature gates. (#40606)

      🚩 Deprecations 🚩

      • processor/filter: Deprecate custom function options suffixed with New in favor of the existing pointer-based options (#47975) The With*FunctionsNew and Default*FunctionsNew variants are now deprecated and will be removed in a future release. If you register custom filter processor functions, migrate:

        • With*FunctionsNew -> With*Functions
        • Default*FunctionsNew -> Default*Functions
      • processor/transform: Deprecate custom function options suffixed with New in favor of the existing pointer-based options (#47970) The With*FunctionsNew and Default*FunctionsNew variants are now deprecated and will be removed in a future release. If you register custom transform processor functions, migrate:

        • With*FunctionsNew -> With*Functions
        • Default*FunctionsNew -> Default*Functions

      💡 Enhancements 💡

      ... (truncated)

      Commits
      • 25a1fd0 [chore] Prepare release 0.151.0 (#48002)
      • 2f03b15 [chore]Update OTEL_VERSION=v0.151.0 OTEL_STABLE_VERSION=v1.57.0 (#48000)
      • 5ca48dc Graceful handling when PostgreSQL datname and rolname are null (#47769)
      • 423b585 [exporter/honeycomb_marker] rename to honeycomb_marker with deprecated alia...
      • 57aa51f Update module modernc.org/sqlite to v1.50.0 (#47996)
      • c432fc3 Update module golang.org/x/vuln to v1.3.0 (#47995)
      • 3d37d91 Update module gitlab.com/gitlab-org/api/client-go/v2 to v2.21.0 (#47993)
      • 0525302 [chore] group github.com/twmb/franz-go updates in renovate (#47994)
      • 407e59f [receiver/fluent_forward] rename to fluent_forward with deprecated alias `f...
      • bfaa4cf Update module github.com/redis/go-redis/v9 to v9.19.0 (#47990)
      • Additional commits viewable in compare view

      Updates `github.com/redis/go-redis/v9` from 9.18.0 to 9.19.0
      Release notes

      Sourced from github.com/redis/go-redis/v9's releases.

      9.19.0

      🚀 Highlights

      FIPS-Compatible Script Helper

      Script now supports a FIPS-safe execution mode that avoids client-side SHA-1 computation, which is blocked in strict FIPS environments. A new NewScriptServerSHA constructor uses SCRIPT LOAD to obtain and cache the digest from the server, then runs commands via EVALSHA/EVALSHA_RO. Falls back to EVAL/EVALRO if loading fails, and transparently retries once on NOSCRIPT. The default behavior is unchanged for existing users.

      (#3700) by @​chaitanyabodlapati

      FT.AGGREGATE Step-Based Pipeline Builder

      Added a new step-based FT.AGGREGATE pipeline API via FTAggregateOptions.Steps, allowing LOAD, APPLY, GROUPBY, and SORTBY (with per-step MAX) to be repeated and interleaved in arbitrary order — matching Redis's native multi-stage aggregation semantics. The legacy Load/Apply/GroupBy/SortBy/SortByMax fields are now deprecated.

      (#3782) by @​ndyakov

      Raw RESP Protocol Access

      Added DoRaw and DoRawWriteTo methods for executing arbitrary commands and reading the raw RESP response. Useful for proxying, custom protocol inspection, and working with commands not yet wrapped by go-redis.

      (#3713) by @​ofekshenawa

      Configurable Dial Retry Backoff

      Added DialerRetryBackoff option (plumbed through Options, ClusterOptions, RingOptions, FailoverOptions) to let callers customize the delay between failed dial attempts. Helpers DialRetryBackoffConstant and DialRetryBackoffExponential (with jitter and cap) are provided out of the box. Dial timeout is now also applied per attempt rather than across all retries.

      (#3706, #3705) by @​mwhooker

      ✨ New Features

      • FT.AGGREGATE Steps: Step-based pipeline builder for FT.AGGREGATE with support for repeated/interleaved LOAD, APPLY, GROUPBY, and SORTBY stages (#3782) by @​ndyakov
      • VectorSet commands: Added VISMEMBER and WITHATTRIBS support (#3753) by @​romanpovol
      • FIPS-safe Script: NewScriptServerSHA uses SCRIPT LOAD to obtain the digest from the server, avoiding client-side SHA-1 (#3700) by @​chaitanyabodlapati
      • Raw RESP access: DoRaw and DoRawWriteTo for raw RESP protocol access (#3713) by @​ofekshenawa
      • Dial retry backoff: DialerRetryBackoff function option with constant and exponential helpers (#3706) by @​mwhooker
      • Typed NOSCRIPT error: Redis NOSCRIPT replies are now surfaced as a typed error for easier handling (#3738) by @​LINKIWI
      • PubSub ClientSetName: Added ClientSetName method to PubSub (#3727) by @​Flack74
      • ReplicaOf: New ReplicaOf method replaces the deprecated SlaveOf (#3720) by @​Copilot
      • HSCAN BinaryUnmarshaler: HScan now supports types implementing encoding.BinaryUnmarshaler (#3768) by @​Aaditya-dubey1

      🐛 Bug Fixes

      • Auto hostname type detection: Improved endpoint type detection for maintenance notifications using DNS-based classification; handles empty hosts and expanded private-IP ranges (#3789) by @​ndyakov
      • HELLO fallback: Don't send CLIENT MAINT_NOTIFICATIONS handshake when HELLO fails and connection falls back to RESP2; fail fast when explicitly enabled with RESP3 (#3788) by @​ndyakov
      • Dial TCP retry: ShouldRetry now treats net.OpError with Op == "dial" timeout errors as safe to retry since no command was sent (#3787) by @​vladisa88
      • wrappedOnClose leak: Fixed resource leak caused by repeatedly wrapping baseClient close logic; replaced with a bounded, concurrency-safe named-hook registry (#3785) by @​ndyakov
      • Pool Close() on stale connections: Suppress close errors (e.g., TLS closeNotify timeouts) for connections already dropped by the server due to idle timeout (#3778) by @​ofekshenawa
      • FIFO waiter ordering: Fixed race in ConnStateMachine.notifyWaiters that could wake multiple waiters under a single mutex hold and violate FIFO ordering (#3777) by @​0x48core
      • Lua READONLY detection: Detect READONLY errors embedded in Lua script error messages on read-only replicas so commands are correctly retried (#3769) by @​zhengjilei
      • VectorScoreSliceCmd RESP2: Fixed VSimWithScores, VSimWithArgsWithScores, and VLinksWithScores which were broken on RESP2 connections returning flat arrays instead of maps (#3767) by @​Copilot

      ... (truncated)

      Changelog

      Sourced from github.com/redis/go-redis/v9's changelog.

      9.19.0 (2026-04-27)

      🚀 Highlights

      FIPS-Compatible Script Helper

      Script now supports a FIPS-safe execution mode that avoids client-side SHA-1 computation, which is blocked in strict FIPS environments. A new NewScriptServerSHA constructor uses SCRIPT LOAD to obtain and cache the digest from the server, then runs commands via EVALSHA/EVALSHA_RO. Falls back to EVAL/EVALRO if loading fails, and transparently retries once on NOSCRIPT. The default behavior is unchanged for existing users.

      (#3700) by @​chaitanyabodlapati

      FT.AGGREGATE Step-Based Pipeline Builder

      Added a new step-based FT.AGGREGATE pipeline API via FTAggregateOptions.Steps, allowing LOAD, APPLY, GROUPBY, and SORTBY (with per-step MAX) to be repeated and interleaved in arbitrary order — matching Redis's native multi-stage aggregation semantics. The legacy Load/Apply/GroupBy/SortBy/SortByMax fields are now deprecated.

      (#3782) by @​ndyakov

      Raw RESP Protocol Access

      Added DoRaw and DoRawWriteTo methods for executing arbitrary commands and reading the raw RESP response. Useful for proxying, custom protocol inspection, and working with commands not yet wrapped by go-redis.

      (#3713) by @​ofekshenawa

      Configurable Dial Retry Backoff

      Added DialerRetryBackoff option (plumbed through Options, ClusterOptions, RingOptions, FailoverOptions) to let callers customize the delay between failed dial attempts. Helpers DialRetryBackoffConstant and DialRetryBackoffExponential (with jitter and cap) are provided out of the box. Dial timeout is now also applied per attempt rather than across all retries.

      (#3706, #3705) by @​mwhooker

      ✨ New Features

      • FT.AGGREGATE Steps: Step-based pipeline builder for FT.AGGREGATE with support for repeated/interleaved LOAD, APPLY, GROUPBY, and SORTBY stages (#3782) by @​ndyakov
      • VectorSet commands: Added VISMEMBER and WITHATTRIBS support (#3753) by @​romanpovol
      • FIPS-safe Script: NewScriptServerSHA uses SCRIPT LOAD to obtain the digest from the server, avoiding client-side SHA-1 (#3700) by @​chaitanyabodlapati
      • Raw RESP access: DoRaw and DoRawWriteTo for raw RESP protocol access (#3713) by @​ofekshenawa
      • Dial retry backoff: DialerRetryBackoff function option with constant and exponential helpers (#3706) by @​mwhooker
      • Typed NOSCRIPT error: Redis NOSCRIPT replies are now surfaced as a typed error for easier handling (#3738) by @​LINKIWI
      • PubSub ClientSetName: Added ClientSetName method to PubSub (#3727) by @​Flack74
      • ReplicaOf: New ReplicaOf method replaces the deprecated SlaveOf (#3720) by @​Copilot
      • HSCAN BinaryUnmarshaler: HScan now supports types implementing encoding.BinaryUnmarshaler (#3768) by @​Aaditya-dubey1

      🐛 Bug Fixes

      • Auto hostname type detection: Improved endpoint type detection for maintenance notifications using DNS-based classification; handles empty hosts and expanded private-IP ranges (#3789) by @​ndyakov
      • HELLO fallback: Don't send CLIENT MAINT_NOTIFICATIONS handshake when HELLO fails and connection falls back to RESP2; fail fast when explicitly enabled with RESP3 (#3788) by @​ndyakov
      • Dial TCP retry: ShouldRetry now treats net.OpError with Op == "dial" timeout errors as safe to retry since no command was sent (#3787) by @​vladisa88
      • wrappedOnClose leak: Fixed resource leak caused by repeatedly wrapping baseClient close logic; replaced with a bounded, concurrency-safe named-hook registry (#3785) by @​ndyakov
      • Pool Close() on stale connections: Suppress close errors (e.g., TLS closeNotify timeouts) for connections already dropped by the server due to idle timeout (#3778) by @​ofekshenawa
      • FIFO waiter ordering: Fixed race in ConnStateMachine.notifyWaiters that could wake multiple waiters under a single mutex hold and violate FIFO ordering (#3777) by @​0x48core
      • Lua READONLY detection: Detect READONLY errors embedded in Lua script error messages on read-only replicas so commands are correctly retried (#3769) by @​zhengjilei
      • VectorScoreSliceCmd RESP2: Fixed VSimWithScores, VSimWithArgsWithScores, and VLinksWithScores which were broken on RESP2 connections returning flat arrays instead of maps (#3767) by @​Copilot

      ... (truncated)

      Commits
      • e7e9866 chore(release): v9.19.0 (#3796)
      • 22b26f4 feat(ft.aggregate): Add Steps for query building (#3782)
      • d9d7694 fix(pool): two fixes for closed connection handling (#3764)
      • 44e8b73 fix(sch): auto hostname type detection (#3789)
      • ad21622 fix(hello): do not send maintnotifications handshake when hello fails (#3788)
      • 1a7ac74 fix(pool): suppress pool Close() errors for stale connections (#3778)
      • 903d6bd fix(retry): make dial tcp error redirectable (#3786) (#3787)
      • 00a551b fix(credentials): leak in wrappedOnClose (#3785)
      • b5a6f99 refactor(pool): remove redundant Conn.closed atomic field (#3783)
      • 928f27a feat(hscan): add support for encoding.BinaryUnmarshaler (#3768)
      • Additional commits viewable in compare view

      Updates `github.com/tinylib/msgp` from 1.6.3 to 1.6.4
      Release notes

      Sourced from github.com/tinylib/msgp's releases.

      v1.6.4

      What's Changed

      Full Changelog: https://github.com/tinylib/msgp/compare/v1.6.3...v1.6.4

      Commits

      Updates `go.opentelemetry.io/collector/pdata` from 1.55.0 to 1.57.0
      Release notes

      Sourced from go.opentelemetry.io/collector/pdata's releases.

      v1.57.0/v0.151.0

      Images and binaries here: https://github.com/open-telemetry/opentelemetry-collector-releases/releases/tag/v0.151.0

      End User Changelog

      🛑 Breaking changes 🛑

      • cmd/builder: In the generated Collector source, the replace statements in the Go module will now use relative paths by default. (#15097) We expect that this will not break existing use-cases where the generated collector is only used in an interim manner for builds. It enables the possibility of tracking the generated Collector code as a longer living artifact, allowing it to be run on any machine (whereas absolute paths will be different depending on the machine the Collector source is generated on.) We have added dist::use_absolute_replace_paths to go back to the absolute path behaviour in the case where there is an unforeseen use-case that requires absolute paths.

      • pkg/confighttp: Stabilize framedSnappy feature gate. (#15096)

      💡 Enhancements 💡

      • all: Add declarative schema support for service telemetry resource configuration. (#14411) The service::telemetry::resource configuration now accepts the declarative schema with explicit name/value pairs:

        service:
          telemetry:
            resource:
              schema_url: https://opentelemetry.io/schemas/1.38.0
              attributes:
                - name: service.name
                  value: my-collector
                - name: host.name
                  value: collector-host
        

        The legacy inline attribute map format is still supported for backward compatibility:

        service:
          telemetry:
            resource:
              service.name: my-collector
              host.name: collector-host
        

        Note: resource.detectors is accepted for forward compatibility but is not yet applied by the collector.

      • exporter/otlp_grpc: Added the server.address and url.path attributes to metrics generated by the otlp exporter. (#14998)

      • exporter/otlp_http: Added the server.address and url.path attributes to metrics generated by the otlp_http exporter. (#14998)

      • pkg/config/configgrpc: Add UserAgent field to ClientConfig to allow overriding the default gRPC user-agent string. (#14686) The otlp gRPC exporter was unconditionally setting the User-Agent via grpc.WithUserAgent() at dial time, which takes precedence over per-call metadata, causing any user-configured User-Agent to be silently discarded. A dedicated UserAgent field has been added to ClientConfig which, when set, is used in the dial option directly instead of the default BuildInfo-derived string.

      • pkg/config/configgrpc: Accept gRPC resolver scheme URIs in client endpoint (e.g. passthrough:///host:port) to allow control over name resolution (#14990) After the migration to grpc.NewClient, some gRPC client components such as the OTLP

      ... (truncated)

      Changelog

      Sourced from go.opentelemetry.io/collector/pdata's changelog.

      v1.57.0/v0.151.0

      🛑 Breaking changes 🛑

      • receiver/otlp: Config.Protocols is now a named field instead of an anonymous embedded field. (#15178) Access to cfg.GRPC and cfg.HTTP must be updated to cfg.Protocols.GRPC and cfg.Protocols.HTTP.

      🚩 Deprecations 🚩

      • cmd/mdatagen: The DefaultMetricsBuilderConfig function is deprecated. Use NewDefaultMetricsBuilderConfig instead. (#15165) The generated DefaultMetricsBuilderConfig function has been renamed to NewDefaultMetricsBuilderConfig to follow Go naming conventions. The old function is kept as a deprecated wrapper and will be removed in a future release.

      💡 Enhancements 💡

      • cmd/mdatagen: Handle default values for configuration fields in generated code in mdatagen. (#14560)

      • cmd/mdatagen: Add opt-in override_value support for resource_attributes config via override_value_enabled flag (#15109) Components can opt in by setting override_value_enabled: true in their metadata.yaml. When enabled, per-attribute config types are generated with typed override_value fields that let users override resource attribute values in the collector configuration. Components without the flag continue to use the shared ResourceAttributeConfig type.

      • cmd/mdatagen: Extend mdatagen config code generation to correctly handle default values for allOf embedded references (#14560)

      • cmd/mdatagen: Handle string validators in generated config structs (#14807) Supported validators include minLength, maxLength and pattern.

      • pkg/config/configgrpc: Add a DefaultBalancerName constant for the name of the default load balancer (#15139) This replaces the BalancerName function.

      • pkg/config/configgrpc: Accept gRPC resolver scheme URIs in client endpoint (e.g. passthrough:///host:port) to allow control over name resolution (#14990) After the migration to grpc.NewClient, some gRPC client components such as the OTLP exporter experienced connection issues in dual-stack DNS environments. This can now be fixed by using the passthrough:/// gRPC resolver scheme in the endpoint field.

      • pkg/exporterhelper: Added the WithAttrs option to allow custom attributes on exporter metrics (#14998)

      🧰 Bug fixes 🧰

      • cmd/mdatagen: Fix a bug in mdatagen where the allOf field was not being properly handled, resulting in incorrect generation of data models. (#15153)

      • pkg/otelcol: Synchronize Collector Run and Shutdown lifecycles so that Shutdown blocks until Run completes all cleanup. (#4947) Shutdown now blocks until Run finishes cleanup, matching http.Server semantics. If Shutdown is called before Run, the next Run call returns nil after cleaning up the config provider.

      • pkg/pdata: Use pool-aware constructors in gRPC service handlers so top-level request structs participate in the sync.Pool lifecycle. (#14763) The gRPC service handlers for all signal types allocated the top-level ExportXxxServiceRequest with bare new(), bypassing the sync.Pool when pdata.useProtoPooling is enabled. This caused objects returned to the pool via Delete to never be retrieved. The handlers now use the pool-aware New*() constructors.

      ... (truncated)

      Commits

      Updates `golang.org/x/mod` from 0.33.0 to 0.35.0
      Commits
      • 03901d3 go.mod: update golang.org/x dependencies
      • 1ac721d go.mod: update golang.org/x dependencies
      • fb1fac8 all: upgrade go directive to at least 1.25.0 [generated]
      • See full diff in compare view

      Updates `golang.org/x/tools` from 0.42.0 to 0.43.0
      Commits
      • 24a8e95 go.mod: update golang.org/x dependencies
      • 3dd57fb gopls/internal/mcp: refactor unified diff generation
      • fcc014d cmd/digraph: fix package doc
      • 39f0f5c cmd/stress: add -failfast flag
      • 063c264 gopls/test/integration/misc: add diagnostics to flaky test
      • deb6130 gopls/internal/golang: fix hover panic in raw strings with CRLF
      • 5f1186b gopls/internal/analysis/driverutil: remove unnecessary new imports
      • ff45494 go/analysis: expose GoMod etc. to Pass.Module
      • 62daff4 go/analysis/passes/inline: fix panic in inlineAlias with instantiated generic...
      • fcb6088 x/tools: delete obsolete code
      • Additional commits viewable in compare view

      Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
      Dependabot commands and options
      You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
      --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yingrong <22300958+VinozzZ@users.noreply.github.com> --- .../github.com/dgryski/go-rendezvous/LICENSE | 21 --- .../go-version/.github/workflows/go-tests.yml | 18 +-- .../hashicorp/go-version/CHANGELOG.md | 38 +++++ .../github.com/hashicorp/go-version/README.md | 26 ++++ .../hashicorp/go-version/version.go | 54 ++++++- .../hashicorp/go-version/version_test.go | 132 ++++++++++++++++++ go.mod | 25 ++-- go.sum | 70 +++++----- 8 files changed, 302 insertions(+), 82 deletions(-) delete mode 100644 LICENSES/github.com/dgryski/go-rendezvous/LICENSE diff --git a/LICENSES/github.com/dgryski/go-rendezvous/LICENSE b/LICENSES/github.com/dgryski/go-rendezvous/LICENSE deleted file mode 100644 index 22080f736a..0000000000 --- a/LICENSES/github.com/dgryski/go-rendezvous/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017-2020 Damian Gryski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/LICENSES/github.com/hashicorp/go-version/.github/workflows/go-tests.yml b/LICENSES/github.com/hashicorp/go-version/.github/workflows/go-tests.yml index 34a4771aba..5c199c7226 100644 --- a/LICENSES/github.com/hashicorp/go-version/.github/workflows/go-tests.yml +++ b/LICENSES/github.com/hashicorp/go-version/.github/workflows/go-tests.yml @@ -1,6 +1,8 @@ name: go-tests -on: [push] +on: + pull_request: + branches: [ main ] env: TEST_RESULTS: /tmp/test-results @@ -11,16 +13,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ 1.15.3, 1.19 ] + go-version: ['stable', 'oldstable'] steps: - name: Setup go - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Create test directory run: | @@ -30,7 +32,7 @@ jobs: run: go mod download - name: Cache / restore go modules - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: | ~/go/pkg/mod @@ -50,7 +52,7 @@ jobs: fi - name: Run golangci-lint - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # Install gotestsum with go get for 1.15.3; otherwise default to go install - name: Install gotestsum @@ -71,13 +73,13 @@ jobs: # Save coverage report parts - name: Upload and save artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: name: Test Results-${{matrix.go-version}} path: ${{ env.TEST_RESULTS }} - name: Upload coverage report - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f with: path: coverage.out name: Coverage-report-${{matrix.go-version}} diff --git a/LICENSES/github.com/hashicorp/go-version/CHANGELOG.md b/LICENSES/github.com/hashicorp/go-version/CHANGELOG.md index 6d48174bfb..81b423151c 100644 --- a/LICENSES/github.com/hashicorp/go-version/CHANGELOG.md +++ b/LICENSES/github.com/hashicorp/go-version/CHANGELOG.md @@ -1,3 +1,41 @@ +# 1.9.0 (Mar 30, 2026) + +ENHANCEMENTS: + +Support parsing versions with custom prefixes via opt-in option in https://github.com/hashicorp/go-version/pull/79 + +INTERNAL: + +- Bump the github-actions-backward-compatible group across 1 directory with 2 updates in https://github.com/hashicorp/go-version/pull/179 +- Bump the github-actions-breaking group with 4 updates in https://github.com/hashicorp/go-version/pull/180 +- Bump the github-actions-backward-compatible group with 3 updates in https://github.com/hashicorp/go-version/pull/182 +- Update GitHub Actions to trigger on pull requests and update go version in https://github.com/hashicorp/go-version/pull/185 +- Bump actions/upload-artifact from 6.0.0 to 7.0.0 in the github-actions-breaking group across 1 directory in https://github.com/hashicorp/go-version/pull/183 +- Bump the github-actions-backward-compatible group across 1 directory with 2 updates in https://github.com/hashicorp/go-version/pull/186 + +# 1.8.0 (Nov 28, 2025) + +ENHANCEMENTS: + +- Add benchmark test for version.String() in https://github.com/hashicorp/go-version/pull/159 +- Bytes implementation in https://github.com/hashicorp/go-version/pull/161 + +INTERNAL: + +- Add CODEOWNERS file in .github/CODEOWNERS in https://github.com/hashicorp/go-version/pull/145 +- Linting in https://github.com/hashicorp/go-version/pull/151 +- Correct typos in comments in https://github.com/hashicorp/go-version/pull/134 +- Migrate GitHub Actions updates from TSCCR to Dependabot in https://github.com/hashicorp/go-version/pull/155 +- Bump the github-actions-backward-compatible group with 2 updates in https://github.com/hashicorp/go-version/pull/157 +- Update doc reference in README in https://github.com/hashicorp/go-version/pull/135 +- Bump the github-actions-breaking group with 3 updates in https://github.com/hashicorp/go-version/pull/156 +- [Compliance] - PR Template Changes Required in https://github.com/hashicorp/go-version/pull/158 +- Bump actions/cache from 4.2.3 to 4.2.4 in the github-actions-backward-compatible group in https://github.com/hashicorp/go-version/pull/167 +- Bump actions/checkout from 4.2.2 to 5.0.0 in the github-actions-breaking group in https://github.com/hashicorp/go-version/pull/166 +- Bump the github-actions-breaking group across 1 directory with 2 updates in https://github.com/hashicorp/go-version/pull/171 +- [IND-4226] [COMPLIANCE] Update Copyright Headers in https://github.com/hashicorp/go-version/pull/172 +- drop init() in https://github.com/hashicorp/go-version/pull/175 + # 1.7.0 (May 24, 2024) ENHANCEMENTS: diff --git a/LICENSES/github.com/hashicorp/go-version/README.md b/LICENSES/github.com/hashicorp/go-version/README.md index 83a8249f72..5528960215 100644 --- a/LICENSES/github.com/hashicorp/go-version/README.md +++ b/LICENSES/github.com/hashicorp/go-version/README.md @@ -34,6 +34,32 @@ if v1.LessThan(v2) { } ``` +#### Version Parsing and Comparison with Prefixes + +The library also supports parsing versions with a custom prefix. +Using the `WithPrefix` option, you can specify a prefix to strip +before parsing the version. + +Use `WithPrefix` when your input strings carry a known release prefix such as +`deployment-`, `controller-`, etc. + +After parsing, the prefix is not part of the canonical version value. This +means the regular comparison methods such as `Compare`, `LessThan`, `Equal`, +and `GreaterThan` compare only the stripped version. If you compare versions +from different prefixes with these methods, the prefixes are ignored. If you +need to reject cross-prefix comparisons, inspect the parsed prefixes before +comparing the versions. + +```go +v1, _ := version.NewVersion("deployment-v1.2.3-beta+metadata", version.WithPrefix("deployment-")) +v2, _ := version.NewVersion("deployment-v1.2.4", version.WithPrefix("deployment-")) + +if v1.LessThan(v2) { + fmt.Printf("%s (%s) is less than %s (%s)\n", v1, v1.Original(), v2, v2.Original()) + // Outputs: 1.2.3-beta+metadata (deployment-v1.2.3-beta+metadata) is less than 1.2.4 (deployment-v1.2.4) +} +``` + #### Version Constraints ```go diff --git a/LICENSES/github.com/hashicorp/go-version/version.go b/LICENSES/github.com/hashicorp/go-version/version.go index 17b29732ee..b95503d3cf 100644 --- a/LICENSES/github.com/hashicorp/go-version/version.go +++ b/LICENSES/github.com/hashicorp/go-version/version.go @@ -49,6 +49,23 @@ const ( `?` ) +// Optional options for NewVersion function. +type options struct { + // If set, this prefix will be trimmed from the version string before parsing. + prefix string +} + +// Option is a functional option for NewVersion. +type Option func(*options) + +// WithPrefix is a functional option that sets a prefix to be removed from the +// version string before parsing. +func WithPrefix(prefix string) Option { + return func(o *options) { + o.prefix = prefix + } +} + // Version represents a single version. type Version struct { metadata string @@ -56,12 +73,36 @@ type Version struct { segments []int64 si int original string + prefix string } -// NewVersion parses the given version and returns a new -// Version. -func NewVersion(v string) (*Version, error) { - return newVersion(v, getVersionRegexp()) +// NewVersion parses the given version and returns a new Version. +// +// Optional parsing behavior can be enabled with Option values such as +// WithPrefix, which validates and strips an expected prefix before parsing. +func NewVersion(v string, opts ...Option) (*Version, error) { + options := &options{} + for _, opt := range opts { + if opt != nil { + opt(options) + } + } + + vToParse := v + if options.prefix != "" { + if !strings.HasPrefix(v, options.prefix) { + return nil, fmt.Errorf("version %q does not have prefix %q", v, options.prefix) + } + vToParse = strings.TrimPrefix(v, options.prefix) + } + + ver, err := newVersion(vToParse, getVersionRegexp()) + if err != nil { + return nil, err + } + ver.prefix = options.prefix + ver.original = v + return ver, nil } // NewSemver parses the given version and returns a new @@ -424,6 +465,11 @@ func (v *Version) Original() string { return v.original } +// Prefix returns the explicit prefix used with WithPrefix, if any. +func (v *Version) Prefix() string { + return v.prefix +} + // UnmarshalText implements encoding.TextUnmarshaler interface. func (v *Version) UnmarshalText(b []byte) error { temp, err := NewVersion(string(b)) diff --git a/LICENSES/github.com/hashicorp/go-version/version_test.go b/LICENSES/github.com/hashicorp/go-version/version_test.go index 15a062324f..8da634559b 100644 --- a/LICENSES/github.com/hashicorp/go-version/version_test.go +++ b/LICENSES/github.com/hashicorp/go-version/version_test.go @@ -39,6 +39,8 @@ func TestNewVersion(t *testing.T) { {"1.7rc2", false}, {"v1.7rc2", false}, {"1.0-", false}, + {"controller-v0.40.2", true}, + {"azure-cli-v1.4.2", true}, } for _, tc := range cases { @@ -51,6 +53,33 @@ func TestNewVersion(t *testing.T) { } } +func TestNewVersionWithPrefix(t *testing.T) { + cases := []struct { + version string + prefix string + err bool + }{ + {"", "release-", true}, + {"rel-1.2.3", "release-", true}, + {"release_1.2.3", "release-", true}, + {"release_1.2.0-x.Y.0+metadata", "release_", false}, + {"release-1.2.0-x.Y.0+metadata-width-hyphen", "release-", false}, + {"myrelease-1.2.3-rc1-with-hyphen", "myrelease-", false}, + {"prefix-1.2.3.4", "prefix-", false}, + {"controller-v0.40.2", "controller-", false}, + {"azure-cli-v1.4.2", "azure-cli-", false}, + } + + for _, tc := range cases { + _, err := NewVersion(tc.version, WithPrefix(tc.prefix)) + if tc.err && err == nil { + t.Fatalf("expected error for version: %q", tc.version) + } else if !tc.err && err != nil { + t.Fatalf("error for version %q: %s", tc.version, err) + } + } +} + func TestNewSemver(t *testing.T) { cases := []struct { version string @@ -80,6 +109,8 @@ func TestNewSemver(t *testing.T) { {"1.7rc2", true}, {"v1.7rc2", true}, {"1.0-", true}, + {"controller-v0.40.2", true}, + {"azure-cli-v1.4.2", true}, } for _, tc := range cases { @@ -171,6 +202,107 @@ func TestVersionCompare(t *testing.T) { } } +func TestVersionCompareWithPrefix(t *testing.T) { + cases := []struct { + v1 string + v1Prefix string + v2 string + v2Prefix string + expected int + }{ + {"controller-v0.40.2", "controller-", "controller-v0.40.3", "controller-", -1}, + {"0.40.4", "", "controller-v0.40.2", "controller-", 1}, + {"0.40.4", "", "controller-v0.40.4", "controller-", 0}, + {"azure-cli-v1.4.2", "azure-cli-", "azure-cli-v1.4.2", "azure-cli-", 0}, + {"azure-cli-v1.4.1", "azure-cli-", "azure-cli-v1.4.2", "azure-cli-", -1}, + {"1.4.3", "", "azure-cli-v1.4.2", "azure-cli-", 1}, + {"v1.4.3", "", "azure-cli-v1.4.2", "azure-cli-", 1}, + {"controller-v1.4.1", "controller-", "azure-cli-v1.4.2", "azure-cli-", -1}, + } + + for _, tc := range cases { + var v1 *Version + var err error + if tc.v1Prefix != "" { + v1, err = NewVersion(tc.v1, WithPrefix(tc.v1Prefix)) + } else { + v1, err = NewVersion(tc.v1) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + var v2 *Version + if tc.v2Prefix != "" { + v2, err = NewVersion(tc.v2, WithPrefix(tc.v2Prefix)) + } else { + v2, err = NewVersion(tc.v2) + } + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := v1.Compare(v2) + expected := tc.expected + if actual != expected { + t.Fatalf( + "%s <=> %s\nexpected: %d\nactual: %d", + tc.v1, tc.v2, + expected, actual) + } + } +} + +func TestVersionAccessorsWithPrefix(t *testing.T) { + v, err := NewVersion("controller-v1.2.0-beta.2+build.5", WithPrefix("controller-")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if got := v.Prefix(); got != "controller-" { + t.Fatalf("expected prefix %q, got %q", "controller-", got) + } + + if got := v.Original(); got != "controller-v1.2.0-beta.2+build.5" { + t.Fatalf("expected original %q, got %q", "controller-v1.2.0-beta.2+build.5", got) + } + + if got := v.String(); got != "1.2.0-beta.2+build.5" { + t.Fatalf("expected string %q, got %q", "1.2.0-beta.2+build.5", got) + } + + if got := v.Metadata(); got != "build.5" { + t.Fatalf("expected metadata %q, got %q", "build.5", got) + } + + if got := v.Prerelease(); got != "beta.2" { + t.Fatalf("expected prerelease %q, got %q", "beta.2", got) + } + + expectedSegments := []int{1, 2, 0} + if got := v.Segments(); !reflect.DeepEqual(got, expectedSegments) { + t.Fatalf("expected segments %#v, got %#v", expectedSegments, got) + } + + expectedSegments64 := []int64{1, 2, 0} + if got := v.Segments64(); !reflect.DeepEqual(got, expectedSegments64) { + t.Fatalf("expected segments64 %#v, got %#v", expectedSegments64, got) + } +} + +func TestVersionSegmentsWithPrefix(t *testing.T) { + v, err := NewVersion("azure-cli-v1.4.2", WithPrefix("azure-cli-")) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []int{1, 4, 2} + actual := v.Segments() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected: %#v\nactual: %#v", expected, actual) + } +} + func TestVersionCompare_versionAndSemver(t *testing.T) { cases := []struct { versionRaw string diff --git a/go.mod b/go.mod index 38bf4480e7..60b9feb033 100644 --- a/go.mod +++ b/go.mod @@ -16,24 +16,24 @@ require ( github.com/honeycombio/dynsampler-go v0.6.4 github.com/honeycombio/hpsf v0.14.0 github.com/honeycombio/husky v0.43.0 - github.com/honeycombio/libhoney-go v1.26.0 + github.com/honeycombio/libhoney-go v1.27.1 github.com/jessevdk/go-flags v1.6.1 github.com/jonboulle/clockwork v0.5.0 github.com/json-iterator/go v1.1.12 - github.com/klauspost/compress v1.18.5 + github.com/klauspost/compress v1.18.6 github.com/open-telemetry/opamp-go v0.23.0 - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0 + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.151.0 github.com/panmari/cuckoofilter v1.0.6 github.com/pelletier/go-toml/v2 v2.3.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 - github.com/redis/go-redis/v9 v9.18.0 + github.com/redis/go-redis/v9 v9.19.0 github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 - github.com/tinylib/msgp v1.6.3 + github.com/tinylib/msgp v1.6.4 github.com/valyala/fastjson v1.6.10 github.com/vmihailenco/msgpack/v5 v5.4.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 @@ -59,10 +59,10 @@ require ( github.com/google/go-licenses/v2 v2.0.1 // indirect github.com/google/licenseclassifier/v2 v2.0.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 // indirect - github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0 // indirect + github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 // indirect github.com/otiai10/copy v1.10.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect @@ -72,8 +72,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/featuregate v1.55.0 // indirect - go.opentelemetry.io/collector/pdata/xpdata v0.149.0 // indirect + go.opentelemetry.io/collector/featuregate v1.57.0 // indirect + go.opentelemetry.io/collector/pdata/xpdata v0.151.0 // indirect go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 // indirect go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect @@ -88,7 +88,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 // indirect @@ -109,13 +108,13 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - go.opentelemetry.io/collector/pdata v1.55.0 + go.opentelemetry.io/collector/pdata v1.57.0 go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.33.0 + golang.org/x/mod v0.35.0 golang.org/x/net v0.52.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect - golang.org/x/tools v0.42.0 + golang.org/x/tools v0.43.0 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect ) diff --git a/go.sum b/go.sum index e109c15e20..3a26c43136 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-wyhash v0.0.0-20191203203029-c4841ae36371 h1:bz5ApY1kzFBvw3yckuyRBCtqGvprWrKswYK468nm+Gs= github.com/dgryski/go-wyhash v0.0.0-20191203203029-c4841ae36371/go.mod h1:/ENMIO1SQeJ5YQeUWWpbX8f+bS8INHrrhFjXgEqi4LA= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= @@ -114,8 +112,8 @@ github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasn github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/honeycombio/dynsampler-go v0.6.4 h1:EM3FXN2Lfmso41MRMmSvRynMrz+AHiRffaWHPf4ZHDs= @@ -124,8 +122,8 @@ github.com/honeycombio/hpsf v0.14.0 h1:LeQbDuT+aVmiJnWp9Kqb9Qqz5OZcjDk85RMzzwKtC github.com/honeycombio/hpsf v0.14.0/go.mod h1:VyPjyn1GViOiCrpBbPZCkEJnuDuSTUpU8LV5CWVTQm4= github.com/honeycombio/husky v0.43.0 h1:L2LcKG9vHuTu/tbfHoWkGCs+93cVf9wLKGGq/4u35gk= github.com/honeycombio/husky v0.43.0/go.mod h1:lQ1VzGZxeYPCr4zxmak1lVe29HJFqJ6bQXWCl0ZqlNg= -github.com/honeycombio/libhoney-go v1.26.0 h1:fdwS7c/5h6ifJqQZ178nm4UEZha04GTbwJMZ7xkShhk= -github.com/honeycombio/libhoney-go v1.26.0/go.mod h1:cR+t7pq9heP00+1/+TNWCrAfjSA74xKWI8YGOANlzYY= +github.com/honeycombio/libhoney-go v1.27.1 h1:79FR19fVpaeDMqTDfpXtMxd90vzsxhZnIOSysMrUSQQ= +github.com/honeycombio/libhoney-go v1.27.1/go.mod h1:qLZO8Q3ep/hISEoVC7m8N9ZOvn2eqaGdoJg9XXXasqM= github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat h1:g6pUF6IZVLG93vZbUefK0qF20CGx0zf0q3n3Fw4gv1s= github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat/go.mod h1:ZyEcAltAA7tCBVo5o+5klmG2l+43E1fjpxGxvOIskic= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -136,10 +134,10 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -161,12 +159,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/open-telemetry/opamp-go v0.23.0 h1:k7h7w/muprut9/DAhUC4anX4v7hIdgO02gIsSjV4uq0= github.com/open-telemetry/opamp-go v0.23.0/go.mod h1:DIIVdkLefdqPW5L+4I2twmAicVrTB0Bp5XJAfedZzAM= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.149.0 h1:EuXIJolnTL+oBFzF0almZEkHiV4thwnSjEcr3L5nNu0= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.149.0/go.mod h1:VKgoaDIUxOoTiskp7HK7ESS+CgsgoNKD2PgzF0wRXvQ= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0 h1:2VC/s/j8LFVE9+CvoNhjAPtKHgDAOgRb/JXkRdrPUnI= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.149.0/go.mod h1:sycQ9JOpSQY+iTiDOVcQL84TgYUIj7fF3z5Yc6nOwnc= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0 h1:OZKthV+cLQO5MCFhBQme3AveZ5vorqaFwb0Qn8jvSQQ= -github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.149.0/go.mod h1:eB74l+/1nW5tofwCjD5TKRqHFYnBSWo0j0xWD8BHYuE= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.151.0 h1:M+d61Wo6zhJoAWKDVUJeeZa46hepah1s+zKgfPlD0ng= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.151.0/go.mod h1:UjELBH4CzaY+y3fHR4RpenHJ3277jBYxTC4xEa5Sxfk= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.151.0 h1:JbnrAMGHqSW+jvJRL9RS7JGMrWpXqGPXdkAk6JoMHV4= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.151.0/go.mod h1:xoSnCUue2dtnuMyJd/1xz7JaQ2G7eweNxM0Laj1uuVc= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0 h1:c8+upXGwDxokINkuChSD7INYHlpcCAyQs2aXpx4rzSs= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0/go.mod h1:Ln3K9yJgPAwEUXqCoR8htVs6bk3cyj6zIPOyM/LhiPo= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 h1:lFowWhr/qx5Gm2X8H0BbG87xZh/e+4S0PQw8HQO5D4Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0/go.mod h1:JybcaNLHHzJQh690eSp+KDbLrxB1+AhKNLlibqrogt4= github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ= @@ -196,8 +194,8 @@ github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8A github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rdleal/go-priorityq v0.0.0-20240324224830-28716009213d h1:OuC714/HtVeMJo6Y1mRkeuDmu3t+F0cgh6qPDGqLmqI= github.com/rdleal/go-priorityq v0.0.0-20240324224830-28716009213d/go.mod h1:X4AAZOixX/7z5rgQkIkMa72A0++MLRke9nipxYUg+8E= -github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= -github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -231,30 +229,30 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= -github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/featuregate v1.55.0 h1:s/bE8135+8GZpVlQ9qLXQjvprE9KNOGsLhNkqm+EDEU= -go.opentelemetry.io/collector/featuregate v1.55.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= -go.opentelemetry.io/collector/internal/testutil v0.149.0 h1:OWfUPO3NFKSaJtz/SBZph/2ENHbr/VbzzlBadKUhm8o= -go.opentelemetry.io/collector/internal/testutil v0.149.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= -go.opentelemetry.io/collector/pdata v1.55.0 h1:WBgye8bo8koUyV9Vmp/r2Q3lgDezdsgfKDQAaM1oT2I= -go.opentelemetry.io/collector/pdata v1.55.0/go.mod h1:6jPrbM4tuliCPACDznjFtxnnHisfKfzwrBVoeuESYuk= -go.opentelemetry.io/collector/pdata/pprofile v0.149.0 h1:4/uI7wsgMnmBZm6Z/VNY6sWnaFN09+Nk3jr7XEmTtOk= -go.opentelemetry.io/collector/pdata/pprofile v0.149.0/go.mod h1:4uprs5wMp4MI1/bcP5mYERfobFxBn+QoeNFQBUSVk/U= -go.opentelemetry.io/collector/pdata/xpdata v0.149.0 h1:crfGmh5LsOAVc1ImdnPIUTMmHbOGYmWXgOyWSDnAKyw= -go.opentelemetry.io/collector/pdata/xpdata v0.149.0/go.mod h1:YgOtcDn7E/4dHw0/Yy/PvSa3GLqMKKAIikzBPM+ML2g= +go.opentelemetry.io/collector/featuregate v1.57.0 h1:KPDSUKYn6MHwgyGRSGPPcW/G96HH93pxuvvPwM+R8nY= +go.opentelemetry.io/collector/featuregate v1.57.0/go.mod h1:4ga1QBMPEejXXmpyJS8lmaRpknJ3Lb9Bvk6e420bUFU= +go.opentelemetry.io/collector/internal/testutil v0.151.0 h1:CFjDItLuqzblItOsnK6IPSdrsOaZCaDjYpB8qWG+XHI= +go.opentelemetry.io/collector/internal/testutil v0.151.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= +go.opentelemetry.io/collector/pdata v1.57.0 h1:oDWBMjEIqyJO3GJEB+iwqxj47rxDK19OKzwaFEaE4sg= +go.opentelemetry.io/collector/pdata v1.57.0/go.mod h1:wZojinP6mNhLXudH8QXx/bjWzOsKMxi/FXwnk+12G/w= +go.opentelemetry.io/collector/pdata/pprofile v0.151.0 h1:hsU0+DpkvhJh3xL1Y8CX2vAPdLMoJLiw+C+rAMsaxZc= +go.opentelemetry.io/collector/pdata/pprofile v0.151.0/go.mod h1:5zfGTQqRuaKyh2SRaZi4SV4nSD8TzY1kYoOjniOD3uk= +go.opentelemetry.io/collector/pdata/xpdata v0.151.0 h1:trsLPS6jCkwVwJyKxbPqQerAiMpKkQrQLEGIEcyC6yM= +go.opentelemetry.io/collector/pdata/xpdata v0.151.0/go.mod h1:0vID3D52DGVoypLa8S7izv41ElTBEgtAbc0HmB4KF60= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= @@ -301,8 +299,8 @@ golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aC golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -332,8 +330,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= From 280ab2bfbb48ef02de739249a214995165bd9fdb Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Mon, 4 May 2026 16:31:39 -0400 Subject: [PATCH 21/35] maint: update honeycombio/husky to v0.43.1 (#1816) ## Which problem is this PR solving? Update husky to v0.43.1 for[ fixing trace id and span id decoding when sending raw http json to refinery](https://github.com/honeycombio/husky/pull/355) ## Short description of the changes - update go.mod --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 60b9feb033..cf27698972 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/honeycombio/dynsampler-go v0.6.4 github.com/honeycombio/hpsf v0.14.0 - github.com/honeycombio/husky v0.43.0 + github.com/honeycombio/husky v0.43.1 github.com/honeycombio/libhoney-go v1.27.1 github.com/jessevdk/go-flags v1.6.1 github.com/jonboulle/clockwork v0.5.0 diff --git a/go.sum b/go.sum index 3a26c43136..100f3687bc 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/honeycombio/dynsampler-go v0.6.4 h1:EM3FXN2Lfmso41MRMmSvRynMrz+AHiRff github.com/honeycombio/dynsampler-go v0.6.4/go.mod h1:M5YYNOfxRrBlEWDatTlHMYo5F7GjwVnptx5z+uXIVMo= github.com/honeycombio/hpsf v0.14.0 h1:LeQbDuT+aVmiJnWp9Kqb9Qqz5OZcjDk85RMzzwKtCKI= github.com/honeycombio/hpsf v0.14.0/go.mod h1:VyPjyn1GViOiCrpBbPZCkEJnuDuSTUpU8LV5CWVTQm4= -github.com/honeycombio/husky v0.43.0 h1:L2LcKG9vHuTu/tbfHoWkGCs+93cVf9wLKGGq/4u35gk= -github.com/honeycombio/husky v0.43.0/go.mod h1:lQ1VzGZxeYPCr4zxmak1lVe29HJFqJ6bQXWCl0ZqlNg= +github.com/honeycombio/husky v0.43.1 h1:HRaSO59KujOsYNQO1Qkn8YFboizheTJcKlBvVhClDe8= +github.com/honeycombio/husky v0.43.1/go.mod h1:lQ1VzGZxeYPCr4zxmak1lVe29HJFqJ6bQXWCl0ZqlNg= github.com/honeycombio/libhoney-go v1.27.1 h1:79FR19fVpaeDMqTDfpXtMxd90vzsxhZnIOSysMrUSQQ= github.com/honeycombio/libhoney-go v1.27.1/go.mod h1:qLZO8Q3ep/hISEoVC7m8N9ZOvn2eqaGdoJg9XXXasqM= github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat h1:g6pUF6IZVLG93vZbUefK0qF20CGx0zf0q3n3Fw4gv1s= From a1e0304f7bc8507b2a6571089569bdcef6b77032 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Tue, 5 May 2026 10:29:23 -0400 Subject: [PATCH 22/35] maint: prepare for release v3.2.1 (#1817) ## Which problem is this PR solving? prepare for v3.2.1 release ## Short description of the changes - update change logs and release notes --- CHANGELOG.md | 9 +++++++++ RELEASE_NOTES.md | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069794cd72..d79eea8976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Refinery Changelog +## 3.2.1 2026-05-04 + +This release fixes a bug in OTLP JSON ingestion where `traceId` and `spanId` fields were incorrectly treated as base64-encoded. The OTLP JSON spec explicitly requires these fields to be hex-encoded strings, and clients sending data over OTLP HTTP/JSON would receive corrupted ID values as a result. + +### 🛠 Maintenance + +- maint: update honeycombio/husky to v0.43.1 by @VinozzZ in https://github.com/honeycombio/refinery/pull/1816 +- maint(deps): bump the minor-patch group with 8 updates by @dependabot in https://github.com/honeycombio/refinery/pull/1814 + ## 3.2.0 2026-04-13 ### 💡 Enhancements diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e686842418..f42b3075bd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,10 @@ While [CHANGELOG.md](./CHANGELOG.md) contains detailed documentation and links to all the source code changes in a given release, this document is intended to be aimed at a more comprehensible version of the contents of the release from the point of view of users of Refinery. +## Version 3.2.1 + +This release fixes a bug where trace and span IDs were corrupted for clients sending data over OTLP HTTP/JSON. + ## Version 3.2.0 This release adds new configuration options for authorization and observability. From 145db42c7af72e6424ee524c16ead82b49f77bc4 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Wed, 13 May 2026 11:55:37 -0400 Subject: [PATCH 23/35] fix: validator exits non-zero on YAML parse errors in rules (#1820) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #1819. `refinery --validate` was exiting 0 even when `rules.yaml` had a YAML syntax error, causing broken configs to pass CI and ship to production. **Fix:** Replace the type assertion with a `c == nil` check. `NewConfig` already encodes severity in its return: nil config = fatal error, non-nil config = warnings only. Checking `c == nil` catches all fatal errors regardless of their type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 (1M context) --- cmd/refinery/main.go | 6 ++---- config/file_config.go | 10 ++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/refinery/main.go b/cmd/refinery/main.go index 4f68b6a036..efe4e82110 100644 --- a/cmd/refinery/main.go +++ b/cmd/refinery/main.go @@ -99,11 +99,9 @@ func main() { c, err := config.NewConfig(opts, version) if err != nil { - if configErr, isConfigErr := err.(*config.FileConfigError); isConfigErr && configErr.HasErrors() { - fmt.Printf("%+v\n", err) + fmt.Printf("%+v\n", err) + if c == nil { os.Exit(1) - } else { - fmt.Printf("%+v\n", err) } } if opts.Validate { diff --git a/config/file_config.go b/config/file_config.go index 48aec76c58..cccbde5eb3 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -608,6 +608,13 @@ func writeYAMLToFile(data any, filename string) error { // nil, it uses the command line arguments. // It also dumps the config and rules to the given files, if specified, which // will cause the program to exit. +// +// Return values follow an intentional two-level contract: +// - (nil, err): fatal error — config could not be loaded or has hard validation +// errors; the caller should not proceed. +// - (cfg, err): non-fatal warning — config loaded successfully but has deprecation +// or advisory warnings; the caller may log err and proceed using cfg. +// - (cfg, nil): success. func NewConfig(opts *CmdEnv, currentVersion ...string) (Config, error) { cData, rData, err := newConfigAndRules(opts) if err != nil { @@ -615,8 +622,7 @@ func NewConfig(opts *CmdEnv, currentVersion ...string) (Config, error) { } cfg, err := newFileConfig(opts, cData, rData, currentVersion...) - // only exit if we have no config at all; if it fails validation, we'll - // do the rest and return it anyway + // only exit on fatal errors (cfg == nil); non-nil cfg with err means warnings only if err != nil && cfg == nil { return nil, err } From 662b1fc6f64187f49eba397a07257db476ea74e7 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Thu, 14 May 2026 18:17:51 -0400 Subject: [PATCH 24/35] maint: remove proto/otlp fork reference (#1822) --- go.mod | 2 -- go.sum | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index cf27698972..ef158f07ab 100644 --- a/go.mod +++ b/go.mod @@ -120,5 +120,3 @@ require ( ) tool github.com/google/go-licenses/v2 - -replace go.opentelemetry.io/proto/otlp => github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat diff --git a/go.sum b/go.sum index 100f3687bc..bec5b7f467 100644 --- a/go.sum +++ b/go.sum @@ -124,8 +124,6 @@ github.com/honeycombio/husky v0.43.1 h1:HRaSO59KujOsYNQO1Qkn8YFboizheTJcKlBvVhCl github.com/honeycombio/husky v0.43.1/go.mod h1:lQ1VzGZxeYPCr4zxmak1lVe29HJFqJ6bQXWCl0ZqlNg= github.com/honeycombio/libhoney-go v1.27.1 h1:79FR19fVpaeDMqTDfpXtMxd90vzsxhZnIOSysMrUSQQ= github.com/honeycombio/libhoney-go v1.27.1/go.mod h1:qLZO8Q3ep/hISEoVC7m8N9ZOvn2eqaGdoJg9XXXasqM= -github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat h1:g6pUF6IZVLG93vZbUefK0qF20CGx0zf0q3n3Fw4gv1s= -github.com/honeycombio/opentelemetry-proto-go/otlp v1.9.0-compat/go.mod h1:ZyEcAltAA7tCBVo5o+5klmG2l+43E1fjpxGxvOIskic= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= @@ -273,6 +271,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0 h1:40vBjolEOioNBl8zPj1wxqlA7kJ82RxR4HnUv7W8zRI= go.opentelemetry.io/proto/otlp/collector/profiles/v1development v0.2.0/go.mod h1:4wAsc1dEVb4D1ZykBNC9AriTU9uLYtmziLrB+7G4lb4= go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 h1:yXinc284C6bmzA1r9jk7MxAhrBIIOH3qwmqwBmylZrA= From 408a376368216cab226d1c7518d837467c87f225 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Mon, 18 May 2026 15:21:34 -0400 Subject: [PATCH 25/35] fix: increment send_errors for network errors from request (#1823) ## Which problem is this PR solving? The `send_errors` metric was never incremented for network errors. ## Short description of the changes - Replaced the per-event error loop at the `httpClient.Do` failure site with a single `handleBatchFailure` call, so network errors now increment `counterSendErrors` once and log once (for the first event) - Updated test --- transmit/direct_transmit.go | 12 +++++------- transmit/direct_transmit_test.go | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/transmit/direct_transmit.go b/transmit/direct_transmit.go index beeb73b4a2..607250bab1 100644 --- a/transmit/direct_transmit.go +++ b/transmit/direct_transmit.go @@ -351,12 +351,14 @@ func (d *DirectTransmission) handleError(ev *types.Event, statusCode int, queueT // handleBatchFailure handles metrics updates when the entire batch fails func (d *DirectTransmission) handleBatchFailure(batch []*types.Event, errorMsg string, logMessage string) { d.Metrics.Increment(d.metricKeys.counterSendErrors) + now := time.Now().UnixMicro() if len(batch) > 0 { - queueTime := time.Now().UnixMicro() - batch[0].EnqueuedUnixMicro + queueTime := now - batch[0].EnqueuedUnixMicro d.handleError(batch[0], 0, queueTime, errorMsg, nil, logMessage) } - for range batch { + for _, ev := range batch { + d.Metrics.Histogram(d.metricKeys.histogramQueueTime, float64(now-ev.EnqueuedUnixMicro)) d.Metrics.Down(d.metricKeys.updownQueuedItems) } } @@ -535,11 +537,7 @@ func (d *DirectTransmission) sendBatch(wholeBatch []*types.Event) { dequeuedAt := d.Clock.Now() if err != nil { - // Network/connection error - affects all events in batch - for _, ev := range subBatch { - queueTime := dequeuedAt.UnixMicro() - ev.EnqueuedUnixMicro - d.handleEventError(ev, 0, queueTime, err.Error(), nil, "") - } + d.handleBatchFailure(subBatch, err.Error(), "") continue } diff --git a/transmit/direct_transmit_test.go b/transmit/direct_transmit_test.go index 2cb185d34e..4013d356b6 100644 --- a/transmit/direct_transmit_test.go +++ b/transmit/direct_transmit_test.go @@ -1208,6 +1208,9 @@ func TestDirectTransmissionRetryLogic(t *testing.T) { if tt.expectSuccess { assert.Contains(t, mockMetrics.CounterIncrements, "libhoney_upstream_response_20x") + } else if tt.statusCode == 0 { + // Network/timeout error: whole batch failed before any response + assert.Contains(t, mockMetrics.CounterIncrements, "libhoney_upstream_send_errors") } else { assert.Contains(t, mockMetrics.CounterIncrements, "libhoney_upstream_response_errors") } From 4abf6a42debb7185cf5c36d6cdde5b01f1a55101 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Fri, 22 May 2026 11:29:07 -0400 Subject: [PATCH 26/35] fix: make sure FieldList is sorted before use it as dynsamplerKey (#1825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Which problem is this PR solving? Dynsampler instances are shared across datasets/environments with identical sampler configs. The sharing key was built from `FieldList` in its original order, so configs with the same fields in different order (e.g. `[status, method]` vs `[method, status]`) were treated as distinct and got separate instances. **User impact**: multiple dynsampler instances were created instead of one shared instance, causing unexpected sampling results — each instance only saw a subset of traffic and made independent decisions, breaking the intended throughput/rate targets. ## Short description of the changes Sort `FieldList` before building the dynsampler map key so field order never affects instance sharing. --- collect/collect_test.go | 5 +-- sample/sample.go | 28 ++++++++++++++--- sample/sample_test.go | 67 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/collect/collect_test.go b/collect/collect_test.go index ffa97cab8b..e9bec4d138 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -477,8 +477,9 @@ func TestDryRunMode(t *testing.T) { transmission := coll.Transmission.(*transmit.MockTransmission) samplerFactory := &sample.SamplerFactory{ - Config: conf, - Logger: &logger.NullLogger{}, + Config: conf, + Logger: &logger.NullLogger{}, + Metrics: &metrics.NullMetrics{}, } sampler := samplerFactory.GetSamplerImplementationForKey("test") coll.SamplerFactory = samplerFactory diff --git a/sample/sample.go b/sample/sample.go index e90b172e4b..3c68fab6d6 100644 --- a/sample/sample.go +++ b/sample/sample.go @@ -3,6 +3,7 @@ package sample import ( "fmt" "os" + "slices" "strings" "sync" @@ -24,6 +25,10 @@ type CanSetGoalThroughputPerSec interface { SetGoalThroughputPerSec(int) } +var samplerFactoryMetrics = []metrics.Metadata{ + {Name: "unique_dynsampler_count", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "Number of unique dynsampler-go samplers created"}, +} + // SamplerFactory is used to create new samplers with common (injected) resources type SamplerFactory struct { Config config.Config `inject:""` @@ -73,6 +78,9 @@ func (s *SamplerFactory) Start() error { if s.Peers != nil { s.Peers.RegisterUpdatedPeersCallback(s.updatePeerCounts) } + for _, metric := range samplerFactoryMetrics { + s.Metrics.Register(metric) + } return nil } @@ -94,6 +102,15 @@ func getSharedDynsampler[ST any, CT any]( return dynsamplerInstance } +// makeDynsamplerKey builds a dynsampler map key with a sorted copy of fieldList so that +// configs with the same fields in different order always map to the same instance. +func makeDynsamplerKey(prefix, samplerType string, rate int64, fieldList []string) string { + sorted := make([]string, len(fieldList)) + copy(sorted, fieldList) + slices.Sort(sorted) + return fmt.Sprintf("%s:%s:%d:%v", prefix, samplerType, rate, sorted) +} + // createSampler creates a sampler with shared dynsamplers based on the config type. // A unique dynsampler is created based on a composite key that includes the keyPrefix // (dataset/environment), sampler type, and configuration parameters (e.g., sample rate @@ -107,17 +124,17 @@ func (s *SamplerFactory) createSampler(c any, keyPrefix string) Sampler { case *config.DeterministicSamplerConfig: sampler = &DeterministicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics} case *config.DynamicSamplerConfig: - dynsamplerKey := fmt.Sprintf("%s:dynamic:%d:%v", keyPrefix, c.SampleRate, c.FieldList) + dynsamplerKey := makeDynsamplerKey(keyPrefix, "dynamic", c.SampleRate, c.FieldList) dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForDynamicSampler) sampler = &DynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} case *config.EMADynamicSamplerConfig: - dynsamplerKey := fmt.Sprintf("%s:emadynamic:%d:%v", keyPrefix, c.GoalSampleRate, c.FieldList) + dynsamplerKey := makeDynsamplerKey(keyPrefix, "emadynamic", int64(c.GoalSampleRate), c.FieldList) dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForEMADynamicSampler) sampler = &EMADynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} case *config.RulesBasedSamplerConfig: sampler = &RulesBasedSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, SamplerFactory: s, samplerPrefix: keyPrefix} case *config.TotalThroughputSamplerConfig: - dynsamplerKey := fmt.Sprintf("%s:totalthroughput:%d:%v", keyPrefix, c.GoalThroughputPerSec, c.FieldList) + dynsamplerKey := makeDynsamplerKey(keyPrefix, "totalthroughput", int64(c.GoalThroughputPerSec), c.FieldList) dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForTotalThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { @@ -127,7 +144,7 @@ func (s *SamplerFactory) createSampler(c any, keyPrefix string) Sampler { } sampler = &TotalThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} case *config.EMAThroughputSamplerConfig: - dynsamplerKey := fmt.Sprintf("%s:emathroughput:%d:%v", keyPrefix, c.GoalThroughputPerSec, c.FieldList) + dynsamplerKey := makeDynsamplerKey(keyPrefix, "emathroughput", int64(c.GoalThroughputPerSec), c.FieldList) dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForEMAThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { @@ -137,7 +154,7 @@ func (s *SamplerFactory) createSampler(c any, keyPrefix string) Sampler { } sampler = &EMAThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} case *config.WindowedThroughputSamplerConfig: - dynsamplerKey := fmt.Sprintf("%s:windowedthroughput:%d:%v", keyPrefix, c.GoalThroughputPerSec, c.FieldList) + dynsamplerKey := makeDynsamplerKey(keyPrefix, "windowedthroughput", int64(c.GoalThroughputPerSec), c.FieldList) dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForWindowedThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { @@ -161,6 +178,7 @@ func (s *SamplerFactory) createSampler(c any, keyPrefix string) Sampler { s.Logger.Debug().WithField("dataset", keyPrefix).Logf("created implementation for sampler type %T", c) // Update peer counts after creating a sampler s.updatePeerCounts() + s.Metrics.Gauge("unique_dynsampler_count", float64(len(s.sharedDynsamplers))) return sampler } diff --git a/sample/sample_test.go b/sample/sample_test.go index 7afbca5ad8..5124e06f91 100644 --- a/sample/sample_test.go +++ b/sample/sample_test.go @@ -462,6 +462,73 @@ func TestDifferentDatasetsShouldNotShareDynsampler(t *testing.T) { assert.Equal(t, prodImpl.dynsampler.GoalThroughputPerSec, dogfoodImpl.dynsampler.GoalThroughputPerSec) } +// TestFieldListOrderDoesNotAffectDynsamplerSharing verifies that two sampler configs with identical +// FieldList entries in different order share the same dynsampler instance. +func TestFieldListOrderDoesNotAffectDynsamplerSharing(t *testing.T) { + fields1 := []string{"service.name", "http.method", "status.code"} + fields2 := []string{"status.code", "service.name", "http.method"} + + newFactory := func() *SamplerFactory { + factory := &SamplerFactory{ + Logger: &logger.NullLogger{}, + Metrics: &metrics.NullMetrics{}, + } + factory.Start() + t.Cleanup(factory.Stop) + return factory + } + + t.Run("DynamicSampler", func(t *testing.T) { + f := newFactory() + s1 := f.createSampler(&config.DynamicSamplerConfig{SampleRate: 10, FieldList: fields1}, "env") + s2 := f.createSampler(&config.DynamicSamplerConfig{SampleRate: 10, FieldList: fields2}, "env") + require.NotNil(t, s1) + require.NotNil(t, s2) + assert.Same(t, s1.(*DynamicSampler).dynsampler, s2.(*DynamicSampler).dynsampler) + assert.Len(t, f.sharedDynsamplers, 1) + }) + + t.Run("EMADynamicSampler", func(t *testing.T) { + f := newFactory() + s1 := f.createSampler(&config.EMADynamicSamplerConfig{GoalSampleRate: 10, FieldList: fields1}, "env") + s2 := f.createSampler(&config.EMADynamicSamplerConfig{GoalSampleRate: 10, FieldList: fields2}, "env") + require.NotNil(t, s1) + require.NotNil(t, s2) + assert.Same(t, s1.(*EMADynamicSampler).dynsampler, s2.(*EMADynamicSampler).dynsampler) + assert.Len(t, f.sharedDynsamplers, 1) + }) + + t.Run("TotalThroughputSampler", func(t *testing.T) { + f := newFactory() + s1 := f.createSampler(&config.TotalThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields1}, "env") + s2 := f.createSampler(&config.TotalThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields2}, "env") + require.NotNil(t, s1) + require.NotNil(t, s2) + assert.Same(t, s1.(*TotalThroughputSampler).dynsampler, s2.(*TotalThroughputSampler).dynsampler) + assert.Len(t, f.sharedDynsamplers, 1) + }) + + t.Run("EMAThroughputSampler", func(t *testing.T) { + f := newFactory() + s1 := f.createSampler(&config.EMAThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields1}, "env") + s2 := f.createSampler(&config.EMAThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields2}, "env") + require.NotNil(t, s1) + require.NotNil(t, s2) + assert.Same(t, s1.(*EMAThroughputSampler).dynsampler, s2.(*EMAThroughputSampler).dynsampler) + assert.Len(t, f.sharedDynsamplers, 1) + }) + + t.Run("WindowedThroughputSampler", func(t *testing.T) { + f := newFactory() + s1 := f.createSampler(&config.WindowedThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields1}, "env") + s2 := f.createSampler(&config.WindowedThroughputSamplerConfig{GoalThroughputPerSec: 10, FieldList: fields2}, "env") + require.NotNil(t, s1) + require.NotNil(t, s2) + assert.Same(t, s1.(*WindowedThroughputSampler).dynsampler, s2.(*WindowedThroughputSampler).dynsampler) + assert.Len(t, f.sharedDynsamplers, 1) + }) +} + // TestClusterSizeUpdatesSamplers verifies that the SamplerFactory properly handles dynamic peer updates // and their impact on throughput-based sampling behavior. func TestClusterSizeUpdatesSamplers(t *testing.T) { From 6d4e1450ad7e7986b41b61f7de17e1e1234999f2 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Fri, 22 May 2026 13:43:12 -0400 Subject: [PATCH 27/35] fix: overcounting dynsampler event_count and request_count (#1826) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Which problem is this PR solving? When multiple sampler configurations share a dynsampler instance (same type + same config), dynsampler metrics like `event_count` and `request_count` were inflated by the number of collector workers — making them N× higher than actual throughput. This made these metrics unreliable for diagnosing sampling behavior. ## Short description of the changes - have `SamplerFactory` be responsible for creating `metricsRecorder` instance. This is the same pattern as with the shared dynsampler instance so they are created and shared atomically --- collect/multi_loop_test.go | 143 +++++++++++++++------------------- sample/dynamic.go | 13 ++-- sample/dynamic_ema.go | 11 +-- sample/ema_throughput.go | 11 +-- sample/sample.go | 64 ++++++++------- sample/totalthroughput.go | 11 +-- sample/windowed_throughput.go | 11 +-- 7 files changed, 130 insertions(+), 134 deletions(-) diff --git a/collect/multi_loop_test.go b/collect/multi_loop_test.go index 85e184cf88..956055aa9a 100644 --- a/collect/multi_loop_test.go +++ b/collect/multi_loop_test.go @@ -534,7 +534,7 @@ func TestCoordinatedReload(t *testing.T) { PeerQueueSize: 3000, WorkerCount: 4, }, - GetSamplerTypeVal: &config.DeterministicSamplerConfig{SampleRate: 1}, + GetSamplerTypeVal: &config.DynamicSamplerConfig{SampleRate: 1, FieldList: []string{"test"}}, ParentIdFieldNames: []string{"trace.parent_id", "parentId"}, TraceIdFieldNames: []string{"trace.trace_id", "traceId"}, SampleCache: config.SampleCacheConfig{ @@ -546,92 +546,71 @@ func TestCoordinatedReload(t *testing.T) { collector := newTestCollector(t, conf) - // Send some test spans to create dataset samplers - processedInitial := int32(0) - for i := 0; i < 10; i++ { - span := &types.Span{ - Event: &types.Event{ - APIHost: "http://api.honeycomb.io", - APIKey: legacyAPIKey, - Dataset: fmt.Sprintf("dataset-%d", i%3), - SampleRate: 1, - Timestamp: time.Now(), - Data: types.Payload{}, - }, - TraceID: fmt.Sprintf("reload-trace-%d", i), - IsRoot: true, - ArrivalTime: time.Now(), - } - if err := collector.AddSpan(span); err == nil { - atomic.AddInt32(&processedInitial, 1) - } + // waitForSamplersCreated waits until at least one worker has a sampler, + // proving traces were actually processed and makeDecision was called. + waitForSamplersCreated := func(msg string) { + t.Helper() + assert.Eventually(t, func() bool { + total := 0 + for _, worker := range collector.workers { + ch := make(chan struct{}) + worker.pause <- ch + total += len(worker.datasetSamplers) + close(ch) + } + return total > 0 + }, 2*time.Second, 10*time.Millisecond, msg) } - // Wait for initial spans to be processed - assert.Eventually(t, func() bool { - return atomic.LoadInt32(&processedInitial) >= 8 - }, 2*time.Second, 10*time.Millisecond, "Initial spans should be processed") - - // Trigger a reload - this should cause workers to recreate their samplers - collector.sendReloadSignal("hash1", "hash2") - - // Give a moment for the reload signal to be processed (reload is async) - // We'll verify the reload worked by checking that spans still get processed - time.Sleep(50 * time.Millisecond) - - // Check that samplers were recreated by sending more spans - processedAfterReload := int32(0) - for i := 0; i < 20; i++ { - span := &types.Span{ - Event: &types.Event{ - APIHost: "http://api.honeycomb.io", - APIKey: legacyAPIKey, - Dataset: "test.reload", - SampleRate: 1, - Timestamp: time.Now(), - Data: types.Payload{}, - }, - TraceID: fmt.Sprintf("after-reload-%d", i), - IsRoot: true, - ArrivalTime: time.Now(), - } - if err := collector.AddSpan(span); err == nil { - atomic.AddInt32(&processedAfterReload, 1) - } + // waitForSamplersCleared waits until all workers have empty datasetSamplers. + waitForSamplersCleared := func(msg string) { + t.Helper() + assert.Eventually(t, func() bool { + for _, worker := range collector.workers { + ch := make(chan struct{}) + worker.pause <- ch + n := len(worker.datasetSamplers) + close(ch) + if n > 0 { + return false + } + } + return true + }, 2*time.Second, 10*time.Millisecond, msg) } - // Verify spans were processed after reload - assert.Eventually(t, func() bool { - return atomic.LoadInt32(&processedAfterReload) >= 15 - }, 2*time.Second, 100*time.Millisecond, "Spans should be processed after reload") - - // Trigger another reload to verify multiple reloads work - collector.sendReloadSignal("hash2", "hash3") - time.Sleep(50 * time.Millisecond) - - // Send more spans to verify system still works - processedAfterSecondReload := int32(0) - for i := 0; i < 20; i++ { - span := &types.Span{ - Event: &types.Event{ - APIHost: "http://api.honeycomb.io", - APIKey: legacyAPIKey, - Dataset: "test.reload2", - SampleRate: 1, - Timestamp: time.Now(), - Data: types.Payload{}, - }, - TraceID: fmt.Sprintf("after-second-reload-%d", i), - IsRoot: true, - ArrivalTime: time.Now(), - } - if err := collector.AddSpan(span); err == nil { - atomic.AddInt32(&processedAfterSecondReload, 1) + sendSpans := func(n int, dataset, traceIDPrefix string) { + for i := 0; i < n; i++ { + span := &types.Span{ + Event: &types.Event{ + APIHost: "http://api.honeycomb.io", + APIKey: legacyAPIKey, + Dataset: fmt.Sprintf("%s", dataset), + SampleRate: 1, + Timestamp: time.Now(), + Data: types.Payload{}, + }, + TraceID: fmt.Sprintf("%s-%d", traceIDPrefix, i), + IsRoot: true, + ArrivalTime: time.Now(), + } + collector.AddSpan(span) //nolint:errcheck } } - // Verify spans were processed after second reload - assert.Eventually(t, func() bool { - return atomic.LoadInt32(&processedAfterSecondReload) >= 15 - }, 2*time.Second, 100*time.Millisecond, "Spans should be processed after second reload") + // Send spans and wait for workers to process them and create samplers. + sendSpans(20, "dataset", "reload-trace") + waitForSamplersCreated("samplers should be created before first reload") + + // Reload and verify all workers clear their samplers. + collector.sendReloadSignal("dataset", "hash2") + waitForSamplersCleared("samplers should be cleared after first reload") + + // Send spans again; samplers must be recreated, proving the system still works. + sendSpans(20, "dataset", "after-reload") + waitForSamplersCreated("samplers should be recreated after first reload") + + // Second reload cycle. + collector.sendReloadSignal("dataset", "hash3") + waitForSamplersCleared("samplers should be cleared after second reload") } diff --git a/sample/dynamic.go b/sample/dynamic.go index 7bb9022a81..7be6816e5a 100644 --- a/sample/dynamic.go +++ b/sample/dynamic.go @@ -41,7 +41,7 @@ type DynamicSampler struct { keyFields, nonRootFields []string dynsampler dynsampler.Sampler - metricsRecorder dynsamplerMetricsRecorder + metricsRecorder *dynsamplerMetricsRecorder } func (d *DynamicSampler) Start() error { @@ -56,12 +56,13 @@ func (d *DynamicSampler) Start() error { d.key = newTraceKey(d.Config.FieldList, d.Config.UseTraceLength) d.keyFields, d.nonRootFields = config.GetKeyFields(d.Config.GetSamplingFields()) - // Register statistics from the dynsampler-go package - d.metricsRecorder = dynsamplerMetricsRecorder{ - met: d.Metrics, - prefix: "dynamic", + if d.metricsRecorder == nil { + d.metricsRecorder = &dynsamplerMetricsRecorder{ + met: d.Metrics, + prefix: "dynamic", + } + d.metricsRecorder.RegisterMetrics(d.dynsampler) } - d.metricsRecorder.RegisterMetrics(d.dynsampler) return nil } diff --git a/sample/dynamic_ema.go b/sample/dynamic_ema.go index 8372923c09..0dd19bf811 100644 --- a/sample/dynamic_ema.go +++ b/sample/dynamic_ema.go @@ -56,12 +56,13 @@ func (d *EMADynamicSampler) Start() error { d.keyFields, d.nonRootFields = config.GetKeyFields(d.Config.GetSamplingFields()) d.key = newTraceKey(d.Config.FieldList, d.Config.UseTraceLength) - // Register statistics this package will produce - d.metricsRecorder = &dynsamplerMetricsRecorder{ - prefix: "emadynamic", - met: d.Metrics, + if d.metricsRecorder == nil { + d.metricsRecorder = &dynsamplerMetricsRecorder{ + prefix: "emadynamic", + met: d.Metrics, + } + d.metricsRecorder.RegisterMetrics(d.dynsampler) } - d.metricsRecorder.RegisterMetrics(d.dynsampler) return nil } diff --git a/sample/ema_throughput.go b/sample/ema_throughput.go index e881933e2c..7af96e9bdb 100644 --- a/sample/ema_throughput.go +++ b/sample/ema_throughput.go @@ -58,12 +58,13 @@ func (d *EMAThroughputSampler) Start() error { d.key = newTraceKey(d.Config.FieldList, d.Config.UseTraceLength) d.keyFields, d.nonRootFields = config.GetKeyFields(d.Config.GetSamplingFields()) - // Register statistics this package will produce - d.metricsRecorder = &dynsamplerMetricsRecorder{ - prefix: "emathroughput", - met: d.Metrics, + if d.metricsRecorder == nil { + d.metricsRecorder = &dynsamplerMetricsRecorder{ + prefix: "emathroughput", + met: d.Metrics, + } + d.metricsRecorder.RegisterMetrics(d.dynsampler) } - d.metricsRecorder.RegisterMetrics(d.dynsampler) return nil } diff --git a/sample/sample.go b/sample/sample.go index 3c68fab6d6..eff69f3abd 100644 --- a/sample/sample.go +++ b/sample/sample.go @@ -25,6 +25,11 @@ type CanSetGoalThroughputPerSec interface { SetGoalThroughputPerSec(int) } +type sharedDynsamplerEntry struct { + dynsampler any + recorder *dynsamplerMetricsRecorder +} + var samplerFactoryMetrics = []metrics.Metadata{ {Name: "unique_dynsampler_count", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "Number of unique dynsampler-go samplers created"}, } @@ -38,8 +43,8 @@ type SamplerFactory struct { peerCount int mutex sync.Mutex - // Shared dynsampler instances to maintain global throughput tracking - sharedDynsamplers map[string]any + // Shared dynsampler instances and their metrics recorders, keyed identically to avoid N×overcounting + sharedDynsamplers map[string]sharedDynsamplerEntry // Store original GoalThroughputPerSec values for cluster size calculations. // We need this to recalculate goal throughput values when the cluster size @@ -60,8 +65,8 @@ func (s *SamplerFactory) updatePeerCounts() { } // Update goal throughput for all throughput-based dynsamplers - for dynsamplerKey, dynsamplerInstance := range s.sharedDynsamplers { - if hasThroughput, ok := dynsamplerInstance.(CanSetGoalThroughputPerSec); ok { + for dynsamplerKey, entry := range s.sharedDynsamplers { + if hasThroughput, ok := entry.dynsampler.(CanSetGoalThroughputPerSec); ok { if cfg, ok := s.goalThroughputConfigs[dynsamplerKey]; ok { // Calculate new throughput based on cluster size newThroughput := max(cfg/s.peerCount, 1) @@ -73,7 +78,7 @@ func (s *SamplerFactory) updatePeerCounts() { func (s *SamplerFactory) Start() error { s.peerCount = 1 - s.sharedDynsamplers = make(map[string]any) + s.sharedDynsamplers = make(map[string]sharedDynsamplerEntry) s.goalThroughputConfigs = make(map[string]int) if s.Peers != nil { s.Peers.RegisterUpdatedPeersCallback(s.updatePeerCounts) @@ -84,22 +89,26 @@ func (s *SamplerFactory) Start() error { return nil } -func getSharedDynsampler[ST any, CT any]( +func getSharedDynsamplerAndRecorder[ST dynsampler.Sampler, CT any]( s *SamplerFactory, dynsamplerKey string, + prefix string, config CT, create func(config CT) ST, -) ST { +) (ST, *dynsamplerMetricsRecorder) { s.mutex.Lock() defer s.mutex.Unlock() - var ok bool - var dynsamplerInstance ST - if dynsamplerInstance, ok = s.sharedDynsamplers[dynsamplerKey].(ST); !ok { - dynsamplerInstance = create(config) - s.sharedDynsamplers[dynsamplerKey] = dynsamplerInstance + if entry, ok := s.sharedDynsamplers[dynsamplerKey]; ok { + if existing, ok := entry.dynsampler.(ST); ok { + return existing, entry.recorder + } } - return dynsamplerInstance + dynsamplerInstance := create(config) + r := &dynsamplerMetricsRecorder{prefix: prefix, met: s.Metrics} + r.RegisterMetrics(dynsamplerInstance) + s.sharedDynsamplers[dynsamplerKey] = sharedDynsamplerEntry{dynsampler: dynsamplerInstance, recorder: r} + return dynsamplerInstance, r } // makeDynsamplerKey builds a dynsampler map key with a sorted copy of fieldList so that @@ -125,44 +134,44 @@ func (s *SamplerFactory) createSampler(c any, keyPrefix string) Sampler { sampler = &DeterministicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics} case *config.DynamicSamplerConfig: dynsamplerKey := makeDynsamplerKey(keyPrefix, "dynamic", c.SampleRate, c.FieldList) - dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForDynamicSampler) - sampler = &DynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} + dynsamplerInstance, recorder := getSharedDynsamplerAndRecorder(s, dynsamplerKey, "dynamic", c, createDynForDynamicSampler) + sampler = &DynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance, metricsRecorder: recorder} case *config.EMADynamicSamplerConfig: dynsamplerKey := makeDynsamplerKey(keyPrefix, "emadynamic", int64(c.GoalSampleRate), c.FieldList) - dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForEMADynamicSampler) - sampler = &EMADynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} + dynsamplerInstance, recorder := getSharedDynsamplerAndRecorder(s, dynsamplerKey, "emadynamic", c, createDynForEMADynamicSampler) + sampler = &EMADynamicSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance, metricsRecorder: recorder} case *config.RulesBasedSamplerConfig: sampler = &RulesBasedSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, SamplerFactory: s, samplerPrefix: keyPrefix} case *config.TotalThroughputSamplerConfig: dynsamplerKey := makeDynsamplerKey(keyPrefix, "totalthroughput", int64(c.GoalThroughputPerSec), c.FieldList) - dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForTotalThroughputSampler) + dynsamplerInstance, recorder := getSharedDynsamplerAndRecorder(s, dynsamplerKey, "totalthroughput", c, createDynForTotalThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { s.mutex.Lock() s.goalThroughputConfigs[dynsamplerKey] = c.GoalThroughputPerSec s.mutex.Unlock() } - sampler = &TotalThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} + sampler = &TotalThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance, metricsRecorder: recorder} case *config.EMAThroughputSamplerConfig: dynsamplerKey := makeDynsamplerKey(keyPrefix, "emathroughput", int64(c.GoalThroughputPerSec), c.FieldList) - dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForEMAThroughputSampler) + dynsamplerInstance, recorder := getSharedDynsamplerAndRecorder(s, dynsamplerKey, "emathroughput", c, createDynForEMAThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { s.mutex.Lock() s.goalThroughputConfigs[dynsamplerKey] = c.GoalThroughputPerSec s.mutex.Unlock() } - sampler = &EMAThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} + sampler = &EMAThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance, metricsRecorder: recorder} case *config.WindowedThroughputSamplerConfig: dynsamplerKey := makeDynsamplerKey(keyPrefix, "windowedthroughput", int64(c.GoalThroughputPerSec), c.FieldList) - dynsamplerInstance := getSharedDynsampler(s, dynsamplerKey, c, createDynForWindowedThroughputSampler) + dynsamplerInstance, recorder := getSharedDynsamplerAndRecorder(s, dynsamplerKey, "windowedthroughput", c, createDynForWindowedThroughputSampler) // only track goal throughput config if we need to recalculate it later based on cluster size if c.UseClusterSize { s.mutex.Lock() s.goalThroughputConfigs[dynsamplerKey] = c.GoalThroughputPerSec s.mutex.Unlock() } - sampler = &WindowedThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance} + sampler = &WindowedThroughputSampler{Config: c, Logger: s.Logger, Metrics: s.Metrics, dynsampler: dynsamplerInstance, metricsRecorder: recorder} default: s.Logger.Error().Logf("unknown sampler type %T. Exiting.", c) os.Exit(1) @@ -229,8 +238,8 @@ func (s *SamplerFactory) ClearDynsamplers() { defer s.mutex.Unlock() // Stop all shared dynsamplers - for _, dynSampler := range s.sharedDynsamplers { - if stopper, ok := dynSampler.(interface{ Stop() }); ok { + for _, entry := range s.sharedDynsamplers { + if stopper, ok := entry.dynsampler.(interface{ Stop() }); ok { stopper.Stop() } } @@ -265,6 +274,7 @@ type internalDysamplerMetric struct { } type dynsamplerMetricsRecorder struct { + mu sync.Mutex prefix string dynPrefix string // Used for accessing metrics from dynsampler-go // Stores the last recorded internal metrics produced by dynsampler-go @@ -276,8 +286,8 @@ type dynsamplerMetricsRecorder struct { // RegisterMetrics registers the metrics that will be recorded by this package. // It initializes the necessary metrics and prepares them for recording. // It MUST be called before any calls to RecordMetrics. +// This function is not concurrency safe. func (d *dynsamplerMetricsRecorder) RegisterMetrics(sampler dynsampler.Sampler) { - // Register statistics this package will produce d.dynPrefix = d.prefix + "_" d.lastMetrics = make(map[string]internalDysamplerMetric) dynInternalMetrics := sampler.GetMetrics(d.dynPrefix) @@ -292,6 +302,7 @@ func (d *dynsamplerMetricsRecorder) RegisterMetrics(sampler dynsampler.Sampler) } func (d *dynsamplerMetricsRecorder) RecordMetrics(sampler dynsampler.Sampler, kept bool, rate uint, numTraceKey int) { + d.mu.Lock() for name, val := range sampler.GetMetrics(d.dynPrefix) { m := d.lastMetrics[name] switch m.metricType { @@ -304,6 +315,7 @@ func (d *dynsamplerMetricsRecorder) RecordMetrics(sampler dynsampler.Sampler, ke d.met.Gauge(name, float64(val)) } } + d.mu.Unlock() if kept { d.met.Increment(d.metricNames.numKept) diff --git a/sample/totalthroughput.go b/sample/totalthroughput.go index c69294a835..686d176eae 100644 --- a/sample/totalthroughput.go +++ b/sample/totalthroughput.go @@ -57,12 +57,13 @@ func (d *TotalThroughputSampler) Start() error { d.key = newTraceKey(d.Config.FieldList, d.Config.UseTraceLength) d.keyFields, d.nonRootFields = config.GetKeyFields(d.Config.GetSamplingFields()) - // Register statistics this package will produce - d.metricsRecorder = &dynsamplerMetricsRecorder{ - prefix: "totalthroughput", - met: d.Metrics, + if d.metricsRecorder == nil { + d.metricsRecorder = &dynsamplerMetricsRecorder{ + prefix: "totalthroughput", + met: d.Metrics, + } + d.metricsRecorder.RegisterMetrics(d.dynsampler) } - d.metricsRecorder.RegisterMetrics(d.dynsampler) return nil } diff --git a/sample/windowed_throughput.go b/sample/windowed_throughput.go index adaf019be4..11a35e49d0 100644 --- a/sample/windowed_throughput.go +++ b/sample/windowed_throughput.go @@ -54,12 +54,13 @@ func (d *WindowedThroughputSampler) Start() error { d.key = newTraceKey(d.Config.FieldList, d.Config.UseTraceLength) d.keyFields, d.nonRootFields = config.GetKeyFields(d.Config.GetSamplingFields()) - // Register statistics this package will produce - d.metricsRecorder = &dynsamplerMetricsRecorder{ - prefix: "windowedthroughput", - met: d.Metrics, + if d.metricsRecorder == nil { + d.metricsRecorder = &dynsamplerMetricsRecorder{ + prefix: "windowedthroughput", + met: d.Metrics, + } + d.metricsRecorder.RegisterMetrics(d.dynsampler) } - d.metricsRecorder.RegisterMetrics(d.dynsampler) return nil } From 1397f37d58792608da1f64915b8dd3faf2ccdef6 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Fri, 22 May 2026 15:10:31 -0700 Subject: [PATCH 28/35] fix(TestDebugAllRules): add omit empty to span counters to fix test --- config/sampler_config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/sampler_config.go b/config/sampler_config.go index fa8f39fbc9..0cc4ce00b5 100644 --- a/config/sampler_config.go +++ b/config/sampler_config.go @@ -174,7 +174,7 @@ func (v *RulesBasedDownstreamSampler) NameMeaningfulRate() string { type V2SamplerConfig struct { RulesVersion int `json:"rulesversion" yaml:"RulesVersion" validate:"required,ge=2"` Samplers map[string]*V2SamplerChoice `json:"samplers" yaml:"Samplers,omitempty" validate:"required"` - SpanCounters []SpanCounter `json:"spancounters" yaml:"SpanCounters,omitempty"` + SpanCounters []SpanCounter `json:"spancounters,omitempty" yaml:"SpanCounters,omitempty" toml:",omitempty"` } type GetSamplingFielder interface { From 9793099b94f17ead66d7e5006b06eb8b18703cef Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Tue, 26 May 2026 11:01:15 -0400 Subject: [PATCH 29/35] fix: copy fields slice before sorting in newTraceKey (#1827) --- sample/trace_key.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sample/trace_key.go b/sample/trace_key.go index 66c42b2eb4..44f617532a 100644 --- a/sample/trace_key.go +++ b/sample/trace_key.go @@ -28,10 +28,13 @@ type traceKey struct { func newTraceKey(fields []string, useTraceLength bool) *traceKey { // always put the field list in sorted order for easier comparison - sort.Strings(fields) - rootOnlyFields := make([]string, 0, len(fields)/2) - nonRootFields := make([]string, 0, len(fields)/2) - for _, field := range fields { + copiedFields := make([]string, len(fields)) + copy(copiedFields, fields) + sort.Strings(copiedFields) + + rootOnlyFields := make([]string, 0, len(copiedFields)/2) + nonRootFields := make([]string, 0, len(copiedFields)/2) + for _, field := range copiedFields { if strings.HasPrefix(field, config.RootPrefix) { rootOnlyFields = append(rootOnlyFields, field[len(config.RootPrefix):]) continue From 64a874adf8f9f10dfbb71add6bd92a8a4bd40242 Mon Sep 17 00:00:00 2001 From: Yingrong Zhao <22300958+VinozzZ@users.noreply.github.com> Date: Tue, 26 May 2026 16:52:33 -0400 Subject: [PATCH 30/35] maint: prepare release v3.2.2 (#1828) ## Summary - Updates `CHANGELOG.md` with v3.2.2 entries - Updates `RELEASE_NOTES.md` with v3.2.2 summary --- CHANGELOG.md | 14 ++++++++++++++ RELEASE_NOTES.md | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d79eea8976..610c996381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Refinery Changelog +## 3.2.2 2026-05-26 + +### 🐛 Fixes + +- fix: validator exits non-zero on YAML parse errors in rules by @VinozzZ in https://github.com/honeycombio/refinery/pull/1820 +- fix: increment send_errors for network errors from request by @VinozzZ in https://github.com/honeycombio/refinery/pull/1823 +- fix: make sure FieldList is sorted before use it as dynsamplerKey by @VinozzZ in https://github.com/honeycombio/refinery/pull/1825 +- fix: overcounting dynsampler event_count and request_count by @VinozzZ in https://github.com/honeycombio/refinery/pull/1826 +- fix: copy fields slice before sorting in newTraceKey by @VinozzZ in https://github.com/honeycombio/refinery/pull/1827 + +### 🛠 Maintenance + +- maint: remove proto/otlp fork reference by @VinozzZ in https://github.com/honeycombio/refinery/pull/1822 + ## 3.2.1 2026-05-04 This release fixes a bug in OTLP JSON ingestion where `traceId` and `spanId` fields were incorrectly treated as base64-encoded. The OTLP JSON spec explicitly requires these fields to be hex-encoded strings, and clients sending data over OTLP HTTP/JSON would receive corrupted ID values as a result. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f42b3075bd..8b05b3241c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,17 @@ While [CHANGELOG.md](./CHANGELOG.md) contains detailed documentation and links to all the source code changes in a given release, this document is intended to be aimed at a more comprehensible version of the contents of the release from the point of view of users of Refinery. +## Version 3.2.2 + +This release fixes dynamic sampling correctness and metrics accuracy when multiple collector workers are enabled. + +### Fixes + +* **Throughput sampler correctness**: Fixed throughput targets not being met. If you set `WorkerCount` to `1` as a workaround, you can now remove that override. +* **Dynsampler metrics accuracy**: Fixed `event_count` and `request_count` being reported higher than actual throughput. +* Fixed `send_errors` not being incremented for network-level transmission errors. +* Fixed the config validator not exiting with a non-zero code on YAML parse errors in rules files. + ## Version 3.2.1 This release fixes a bug where trace and span IDs were corrupted for clients sending data over OTLP HTTP/JSON. From 4f0994c9bec22988ac8ef5ed5396f6d699db88d0 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 1 Jun 2026 11:22:43 -0700 Subject: [PATCH 31/35] test: use testcontainers for redis tests --- .circleci/config.yml | 27 +++++----- Makefile | 15 ++---- app/app_test.go | 42 +++++++++------- go.mod | 37 +++++++++++++- go.sum | 88 ++++++++++++++++++++++++++++++++- internal/peer/peers_test.go | 5 ++ internal/redistest/redistest.go | 64 ++++++++++++++++++++++++ pubsub/pubsub_test.go | 16 +++--- 8 files changed, 242 insertions(+), 52 deletions(-) create mode 100644 internal/redistest/redistest.go diff --git a/.circleci/config.yml b/.circleci/config.yml index e097ce4ea0..7f7f01cb6d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,21 +65,24 @@ commands: jobs: test: - docker: - - image: cimg/go:1.25 - - image: redis:6.2 + # Use the machine executor so testcontainers-go has a local Docker daemon + # to drive — the redis testcontainer needs to be reachable from the test + # process via a host port, which the docker executor cannot provide. + machine: + image: ubuntu-2404:current resource_class: xlarge steps: - checkout - - restore_cache: - keys: - - v1-dockerize-{{ checksum "Makefile" }} - - v1-dockerize- - - run: make dockerize - - save_cache: - key: v1-dockerize-{{ checksum "Makefile" }} - paths: - - dockerize.tar.gz + - run: + name: Install Go and gotestsum + command: | + GO_VERSION=1.25.3 + curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz + sudo rm -rf /usr/local/go + sudo tar -C /usr/local -xzf /tmp/go.tar.gz + echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' >> "$BASH_ENV" + export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH + go install gotest.tools/gotestsum@latest - restore_cache: keys: - v3-go-mod-{{ checksum "go.sum" }} diff --git a/Makefile b/Makefile index bc5b4cf28d..4226762e68 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ test: test_with_race test_all .PHONY: test_with_race #: run only tests tagged with potential race conditions -test_with_race: test_results wait_for_redis +test_with_race: test_results @echo @echo "+++ testing - race conditions?" @echo @@ -18,7 +18,7 @@ test_with_race: test_results wait_for_redis .PHONY: test_all #: run all tests, but with no race condition detection -test_all: test_results wait_for_redis +test_all: test_results @echo @echo "+++ testing - all the tests" @echo @@ -34,15 +34,6 @@ local_image: ko crane ./build-docker.sh docker tag $$(docker images ko.local/refinery --quiet | head -1) ko.local/refinery:local -.PHONY: wait_for_redis -# wait for Redis to become available for test suite -wait_for_redis: dockerize - @echo - @echo "+++ We need a Redis running to run the tests." - @echo - @echo "Checking with dockerize $(shell ./dockerize --version)" - @./dockerize -wait tcp://localhost:6379 -timeout 30s - # You can override this version from an environment variable. HOST_OS := $(shell uname -s | tr A-Z a-z) # You can override this version from an environment variable. @@ -109,7 +100,7 @@ DOCKERIZE_RELEASE_ASSET := dockerize-${HOST_OS}-amd64-${DOCKERIZE_VERSION}.tar.g dockerize.tar.gz: @echo - @echo "+++ Retrieving dockerize tool for Redis readiness check." + @echo "+++ Retrieving dockerize tool for service readiness checks." @echo # make sure that file is available ifeq (, $(shell command -v file)) diff --git a/app/app_test.go b/app/app_test.go index cff02a5e6c..cc4bd18788 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -34,6 +34,7 @@ import ( "github.com/honeycombio/refinery/config" "github.com/honeycombio/refinery/internal/health" "github.com/honeycombio/refinery/internal/peer" + "github.com/honeycombio/refinery/internal/redistest" "github.com/honeycombio/refinery/logger" "github.com/honeycombio/refinery/metrics" "github.com/honeycombio/refinery/pubsub" @@ -235,11 +236,11 @@ func (w *countingTransmission) waitForCount(t testing.TB, n int) { // each test gets a unique port and redisDB. // // by default, every Redis instance supports 16 databases, we use redisDB as a way to separate test data -func defaultConfig(basePort int, redisDB int, apiURL string) *config.MockConfig { - return defaultConfigWithGRPC(basePort, redisDB, apiURL, false) +func defaultConfig(t testing.TB, basePort int, redisDB int, apiURL string) *config.MockConfig { + return defaultConfigWithGRPC(t, basePort, redisDB, apiURL, false) } -func defaultConfigWithGRPC(basePort int, redisDB int, apiURL string, enableGRPC bool) *config.MockConfig { +func defaultConfigWithGRPC(t testing.TB, basePort int, redisDB int, apiURL string, enableGRPC bool) *config.MockConfig { if redisDB >= 16 { panic("redisDB must be less than 16") } @@ -247,6 +248,8 @@ func defaultConfigWithGRPC(basePort int, redisDB int, apiURL string, enableGRPC apiURL = "http://api.honeycomb.io" } + redisHost, redisPort := redistest.Endpoint(t) + cfg := &config.MockConfig{ GetTracesConfigVal: config.TracesConfig{ SendTicker: config.Duration(2 * time.Millisecond), @@ -258,6 +261,7 @@ func defaultConfigWithGRPC(basePort int, redisDB int, apiURL string, enableGRPC AddRuleReasonToTrace: true, PeerManagementType: "redis", GetRedisPeerManagementVal: config.RedisPeerManagementConfig{ + Host: redisHost + ":" + redisPort, Prefix: "refinery-app-test", Timeout: config.Duration(1 * time.Second), Database: redisDB, @@ -441,7 +445,7 @@ func TestAppIntegration(t *testing.T) { redisDB := 2 testServer := newTestAPIServer(t) - cfg := defaultConfig(port, redisDB, testServer.server.URL) + cfg := defaultConfig(t, port, redisDB, testServer.server.URL) app, graph := newStartedApp(t, nil, nil, cfg) // Send a root span, it should be sent in short order. @@ -688,7 +692,7 @@ func TestAppIntegrationSendKey(t *testing.T) { redisDB := 1 + i testServer := newTestAPIServer(t) - cfg := defaultConfig(port, redisDB, testServer.server.URL) + cfg := defaultConfig(t, port, redisDB, testServer.server.URL) cfg.GetAccessKeyConfigVal = config.AccessKeyConfig{ SendKey: tt.sendKey, SendKeyMode: tt.sendKeyMode, @@ -902,7 +906,7 @@ func TestAppIntegrationWithNonLegacyKey(t *testing.T) { redisDB := 3 testServer := newTestAPIServer(t) - cfg := defaultConfig(port, redisDB, testServer.server.URL) + cfg := defaultConfig(t, port, redisDB, testServer.server.URL) a, graph := newStartedApp(t, nil, nil, cfg) a.IncomingRouter.SetEnvironmentCache(time.Second, func(s string) (string, error) { return "test", nil }) a.PeerRouter.SetEnvironmentCache(time.Second, func(s string) (string, error) { return "test", nil }) @@ -942,7 +946,7 @@ func TestAppIntegrationEmptyEvent(t *testing.T) { port := 19010 redisDB := 8 - cfg := defaultConfig(port, redisDB, "") + cfg := defaultConfig(t, port, redisDB, "") _, graph := newStartedApp(t, nil, nil, cfg) tt := []struct { @@ -1005,7 +1009,7 @@ func TestPeerRouting(t *testing.T) { senders[i] = &transmit.MockTransmission{} peers := peer.NewMockPeers(peerList, peerList[i]) redisDB := 5 + i - cfg := defaultConfig(basePort, redisDB, "") + cfg := defaultConfig(t, basePort, redisDB, "") apps[i], graph = newStartedApp(t, senders[i], peers, cfg) defer startstop.Stop(graph.Objects(), nil) @@ -1080,7 +1084,7 @@ func TestHostMetadataSpanAdditions(t *testing.T) { redisDB := 7 testServer := newTestAPIServer(t) - cfg := defaultConfig(port, redisDB, testServer.server.URL) + cfg := defaultConfig(t, port, redisDB, testServer.server.URL) cfg.AddHostMetadataToTrace = true app, graph := newStartedApp(t, nil, nil, cfg) @@ -1134,7 +1138,7 @@ func TestEventsEndpoint(t *testing.T) { peers := peer.NewMockPeers(peerList, peerList[i]) redisDB := 8 + i - cfg := defaultConfig(basePort, redisDB, "") + cfg := defaultConfig(t, basePort, redisDB, "") apps[i], graph = newStartedApp(t, senders[i], peers, cfg) defer startstop.Stop(graph.Objects(), nil) } @@ -1230,7 +1234,7 @@ func TestEventsEndpointWithNonLegacyKey(t *testing.T) { peers := peer.NewMockPeers(peerList, peerList[i]) redisDB := 10 + i - cfg := defaultConfig(basePort, redisDB, "") + cfg := defaultConfig(t, basePort, redisDB, "") app, graph := newStartedApp(t, senders[i], peers, cfg) app.IncomingRouter.SetEnvironmentCache(time.Second, func(s string) (string, error) { return "test", nil }) @@ -1318,7 +1322,7 @@ func TestOTLPProtobufIntegration(t *testing.T) { redisDB := 14 testServer := newTestAPIServer(t) - cfg := defaultConfigWithGRPC(port, redisDB, testServer.server.URL, true) + cfg := defaultConfigWithGRPC(t, port, redisDB, testServer.server.URL, true) app, graph := newStartedApp(t, nil, nil, cfg) // Create OTLP protobuf request @@ -1421,7 +1425,7 @@ func TestOTLPGRPCConcurrency(t *testing.T) { redisDB := 15 testServer := newTestAPIServer(t) - cfg := defaultConfigWithGRPC(port, redisDB, testServer.server.URL, true) + cfg := defaultConfigWithGRPC(t, port, redisDB, testServer.server.URL, true) _, graph := newStartedApp(t, nil, nil, cfg) // Connect to gRPC server @@ -1660,7 +1664,7 @@ func createBenchmarkOTLPRequest() *collectortrace.ExportTraceServiceRequest { func BenchmarkTracesOTLP(b *testing.B) { sender := &countingTransmission{} redisDB := 15 - cfg := defaultConfigWithGRPC(18000, redisDB, "", true) + cfg := defaultConfigWithGRPC(b, 18000, redisDB, "", true) _, graph := newStartedApp(b, sender, nil, cfg) defer func() { err := startstop.Stop(graph.Objects(), nil) @@ -1773,7 +1777,7 @@ func BenchmarkTracesOTLP(b *testing.B) { func BenchmarkTraces(b *testing.B) { sender := &countingTransmission{} redisDB := 1 - cfg := defaultConfig(11000, redisDB, "") + cfg := defaultConfig(b, 11000, redisDB, "") _, graph := newStartedApp(b, sender, nil, cfg) defer func() { err := startstop.Stop(graph.Objects(), nil) @@ -1813,8 +1817,8 @@ func BenchmarkTraces(b *testing.B) { } // createRulesBasedConfig creates a mock config with rules-based sampler containing downstream samplers -func createRulesBasedConfig(port, redisDB int, apiURL string, throughputGoal int) *config.MockConfig { - cfg := defaultConfig(port, redisDB, apiURL) +func createRulesBasedConfig(t testing.TB, port, redisDB int, apiURL string, throughputGoal int) *config.MockConfig { + cfg := defaultConfig(t, port, redisDB, apiURL) // Configure rules-based sampler with selective rules cfg.GetSamplerTypeVal = &config.RulesBasedSamplerConfig{ @@ -1899,7 +1903,7 @@ func TestRulesBasedSamplerWithDownstreamAndClusterChanges(t *testing.T) { // Phase 1: Initial setup with single-node cluster mockPeers := peer.NewMockPeers([]string{"http://localhost:20001"}, "http://localhost:20001") - cfg := createRulesBasedConfig(port, redisDB, testServer.server.URL, 100) + cfg := createRulesBasedConfig(t, port, redisDB, testServer.server.URL, 100) _, graph := newStartedApp(t, nil, mockPeers, cfg) defer startstop.Stop(graph.Objects(), nil) @@ -2189,7 +2193,7 @@ func BenchmarkDistributedTraces(b *testing.B) { peers := peer.NewMockPeers(peerList, peerList[i]) redisDB := 2 + i - cfg := defaultConfig(basePort, redisDB, "") + cfg := defaultConfig(b, basePort, redisDB, "") apps[i], graph = newStartedApp(b, sender, peers, cfg) defer startstop.Stop(graph.Objects(), nil) diff --git a/go.mod b/go.mod index cf27698972..87a3e21a5a 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 github.com/tidwall/gjson v1.18.0 github.com/tinylib/msgp v1.6.4 github.com/valyala/fastjson v1.6.10 @@ -54,22 +55,55 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-licenses/v2 v2.0.1 // indirect github.com/google/licenseclassifier/v2 v2.0.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/go-version v1.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0 // indirect github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/otiai10/copy v1.10.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/sergi/go-diff v1.2.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/testcontainers/testcontainers-go v0.42.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/collector/featuregate v1.57.0 // indirect @@ -78,6 +112,7 @@ require ( go.opentelemetry.io/proto/otlp/profiles/v1development v0.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/sync v0.20.0 // indirect gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect k8s.io/klog/v2 v2.90.1 // indirect diff --git a/go.sum b/go.sum index 100f3687bc..994fb80a16 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= @@ -21,7 +29,19 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,6 +54,14 @@ github.com/dgryski/go-wyhash v0.0.0-20191203203029-c4841ae36371 h1:bz5ApY1kzFBvw github.com/dgryski/go-wyhash v0.0.0-20191203203029-c4841ae36371/go.mod h1:/ENMIO1SQeJ5YQeUWWpbX8f+bS8INHrrhFjXgEqi4LA= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -64,6 +92,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -89,6 +119,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -147,8 +178,32 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 h1:Q8asukpmyrEheocd+R+6YEI4jcm62sHHalgTMG+LoLw= github.com/michel-laterman/proxy-connect-dialer-go v0.1.0/go.mod h1:HTlVkRAqzTRPYbWxgAiwMT9HRZMOqP3Mx7+toa3yJjc= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -167,6 +222,10 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0 github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.151.0/go.mod h1:Ln3K9yJgPAwEUXqCoR8htVs6bk3cyj6zIPOyM/LhiPo= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0 h1:lFowWhr/qx5Gm2X8H0BbG87xZh/e+4S0PQw8HQO5D4Y= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling v0.142.0/go.mod h1:JybcaNLHHzJQh690eSp+KDbLrxB1+AhKNLlibqrogt4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/otiai10/copy v1.10.0 h1:znyI7l134wNg/wDktoVQPxPkgvhDfGCYUasey+h0rDQ= github.com/otiai10/copy v1.10.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww= github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks= @@ -181,6 +240,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -202,6 +263,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -213,8 +276,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -222,6 +285,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 h1:id/6LH8ZeDrtAUVSuNvZUAJ1kVpb82y1pr9yweAWsRg= +github.com/testcontainers/testcontainers-go/modules/redis v0.42.0/go.mod h1:uF0jI8FITagQpBNOgweGBmPf6rP4K0SeL1XFPbsZSSY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -231,12 +298,18 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -293,6 +366,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= @@ -318,9 +393,14 @@ golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= @@ -373,7 +453,11 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/peer/peers_test.go b/internal/peer/peers_test.go index 344c794eb8..3fca0e9f3c 100644 --- a/internal/peer/peers_test.go +++ b/internal/peer/peers_test.go @@ -12,6 +12,7 @@ import ( "github.com/facebookgo/inject" "github.com/facebookgo/startstop" "github.com/honeycombio/refinery/config" + "github.com/honeycombio/refinery/internal/redistest" "github.com/honeycombio/refinery/logger" "github.com/honeycombio/refinery/metrics" "github.com/honeycombio/refinery/pubsub" @@ -87,10 +88,14 @@ func newPeers(c config.Config) (Peers, error) { } func TestPeerShutdown(t *testing.T) { + host, port := redistest.Endpoint(t) c := &config.MockConfig{ GetPeerListenAddrVal: "0.0.0.0:8081", PeerManagementType: "redis", PeerTimeout: 5 * time.Second, + GetRedisPeerManagementVal: config.RedisPeerManagementConfig{ + Host: host + ":" + port, + }, } p, err := newPeers(c) diff --git a/internal/redistest/redistest.go b/internal/redistest/redistest.go new file mode 100644 index 0000000000..c67463935e --- /dev/null +++ b/internal/redistest/redistest.go @@ -0,0 +1,64 @@ +// Package redistest provides a shared Redis testcontainer for tests that need +// a real Redis instance. One container is started per test binary on first +// call and reused across tests; the testcontainers Reaper cleans it up when +// the process exits. +package redistest + +import ( + "context" + "net" + "net/url" + "sync" + "testing" + "time" + + "github.com/testcontainers/testcontainers-go/modules/redis" +) + +const image = "redis:6.2" + +var ( + once sync.Once + sharedHost string + sharedPort string + startup error +) + +// Endpoint returns the host and port of a shared Redis container, starting it +// on first call. The container lives for the duration of the test process. +func Endpoint(t testing.TB) (host, port string) { + t.Helper() + once.Do(start) + if startup != nil { + t.Fatalf("redistest: failed to start Redis container: %v", startup) + } + return sharedHost, sharedPort +} + +func start() { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + c, err := redis.Run(ctx, image) + if err != nil { + startup = err + return + } + conn, err := c.ConnectionString(ctx) + if err != nil { + startup = err + return + } + u, err := url.Parse(conn) + if err != nil { + startup = err + return + } + h, p, err := net.SplitHostPort(u.Host) + if err != nil { + startup = err + return + } + sharedHost = h + sharedPort = p +} diff --git a/pubsub/pubsub_test.go b/pubsub/pubsub_test.go index 333ce78b68..32dbaa6a95 100644 --- a/pubsub/pubsub_test.go +++ b/pubsub/pubsub_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/honeycombio/refinery/config" + "github.com/honeycombio/refinery/internal/redistest" "github.com/honeycombio/refinery/logger" "github.com/honeycombio/refinery/metrics" "github.com/honeycombio/refinery/pubsub" @@ -22,17 +23,20 @@ var types = []string{ "local", } -func newPubSub(typ string) pubsub.PubSub { +func newPubSub(t testing.TB, typ string) pubsub.PubSub { + t.Helper() var ps pubsub.PubSub m := &metrics.NullMetrics{} m.Start() tracer := noop.NewTracerProvider().Tracer("test") switch typ { case "goredis": + host, port := redistest.Endpoint(t) ps = &pubsub.GoRedisPubSub{ Config: &config.MockConfig{ GetRedisPeerManagementVal: config.RedisPeerManagementConfig{ ClusterName: "test", + Host: host + ":" + port, }, }, Metrics: m, @@ -71,7 +75,7 @@ func TestPubSubBasics(t *testing.T) { ctx := context.Background() for _, typ := range types { t.Run(typ, func(t *testing.T) { - ps := newPubSub(typ) + ps := newPubSub(t, typ) l1 := &pubsubListener{} @@ -105,7 +109,7 @@ func TestPubSubMultiSubscriber(t *testing.T) { ctx := context.Background() for _, typ := range types { t.Run(typ, func(t *testing.T) { - ps := newPubSub(typ) + ps := newPubSub(t, typ) l1 := &pubsubListener{} l2 := &pubsubListener{} topic := ps.FormatTopic("topic") @@ -138,7 +142,7 @@ func TestPubSubMultiTopic(t *testing.T) { ctx := context.Background() for _, typ := range types { t.Run(typ, func(t *testing.T) { - ps := newPubSub(typ) + ps := newPubSub(t, typ) time.Sleep(500 * time.Millisecond) topics := make([]string, topicCount) listeners := make([]*pubsubListener, topicCount) @@ -190,7 +194,7 @@ func TestPubSubLatency(t *testing.T) { ctx := context.Background() for _, typ := range types { t.Run(typ, func(t *testing.T) { - ps := newPubSub(typ) + ps := newPubSub(t, typ) var count, total, tmin, tmax int64 mut := sync.Mutex{} @@ -252,7 +256,7 @@ func BenchmarkPubSub(b *testing.B) { ctx := context.Background() for _, typ := range types { b.Run(typ, func(b *testing.B) { - ps := newPubSub(typ) + ps := newPubSub(b, typ) time.Sleep(100 * time.Millisecond) li := &pubsubListener{} From 96b10348b415057dd32331cc680ba197323b2f0e Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 1 Jun 2026 11:24:31 -0700 Subject: [PATCH 32/35] feat(SpanCounter): add ScopeConditions for per-anchor subtree counts Extends SpanCounter so users can scope counts to user-defined anchor spans, with per-anchor subtree counts written to each matching anchor. Default behavior (no ScopeConditions) is unchanged: the trace-wide total is written to the root via a single linear pass. When at least one counter has ScopeConditions, a parent->children index is built and counts are aggregated via iterative post-order DFS with cycle defense. Also adds IDFieldsConfig.SpanNames so the algorithm can resolve each span's own ID, plus validation rules for the new fields (Key uniqueness, reserved meta.refinery. namespace, has-root-span rejection inside ScopeConditions). Co-Authored-By: Claude Opus 4.7 (1M context) --- collect/collect.go | 251 ++++++++- collect/collect_test.go | 716 +++++++++++++++++++++++++- config.md | 13 +- config/config.go | 2 + config/file_config.go | 8 + config/metadata/configMeta.yaml | 15 + config/metadata/rulesMeta.yaml | 54 +- config/mock.go | 8 + config/span_counter_config.go | 52 +- config/span_counter_config_test.go | 152 ++++++ config/validate.go | 63 +++ refinery_config.md | 11 + refinery_rules.md | 32 +- rules.md | 50 +- tools/convert/configDataNames.txt | 4 +- tools/convert/minimal_config.yaml | 6 +- tools/convert/templates/configV2.tmpl | 12 +- 17 files changed, 1400 insertions(+), 49 deletions(-) diff --git a/collect/collect.go b/collect/collect.go index 0ce0be596b..58d21c4115 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -164,6 +164,7 @@ var inMemCollectorMetrics = []metrics.Metadata{ {Name: "collector_outgoing_queue", Type: metrics.Histogram, Unit: metrics.Dimensionless, Description: "number of traces waiting to be send to upstream"}, {Name: "collector_cache_eviction", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of times cache eviction has occurred"}, {Name: "collector_num_workers", Type: metrics.Gauge, Unit: metrics.Dimensionless, Description: "number of collector workers"}, + {Name: "span_counter_id_collision", Type: metrics.Counter, Unit: metrics.Dimensionless, Description: "number of times two spans in the same trace share a span ID while computing scoped SpanCounters"}, } func (i *InMemCollector) Start() error { @@ -743,9 +744,25 @@ func findSuitableRootSpan(t sendableTrace) *types.Span { return best } -// computeCustomCounts computes each counter's value by iterating all spans in the trace -// and attaches the results to the root span. -// Returns nil, nil if there are no counters configured or no suitable target span. +// customCountWrite is a single counter-keyed write destined for one span. +type customCountWrite struct { + key string + value int64 +} + +// computeCustomCounts computes each configured SpanCounter and returns the +// per-span attribute writes the caller should apply. +// +// Returns nil if there are no counters configured or no spans to count. +// +// Fast path: when no counter has ScopeConditions, run a single linear scan +// over the spans and write the trace-wide total to the root span — identical +// to the original behavior, with no index, DFS, or per-span storage. +// +// Scoped path (engaged when at least one counter has ScopeConditions): build a +// parent->children index, run iterative post-order DFS from each forest root +// (and any unvisited orphan island) to compute per-span subtree counts, then +// emit per-anchor writes plus an optional trace-wide total on the root. // // Stress relief note: this runs inside sendTraces(), the sole consumer of the // tracesToSend channel. Work is O(N×M) — N spans × M counters — so large @@ -756,31 +773,228 @@ func findSuitableRootSpan(t sendableTrace) *types.Span { // processed via ProcessSpanImmediately (the stress-relief fast path) bypass the // trace buffer entirely and never reach sendTraces, so custom counts are not // computed or attached to stress-sampled traces. -func (i *InMemCollector) computeCustomCounts(t sendableTrace) (*types.Span, map[string]int64) { +func (i *InMemCollector) computeCustomCounts(t sendableTrace) map[*types.Span][]customCountWrite { i.mutex.RLock() counters := i.spanCounters i.mutex.RUnlock() if len(counters) == 0 { - return nil, nil + return nil } - targetSpan := findSuitableRootSpan(t) - if targetSpan == nil { - return nil, nil + spans := t.GetSpans() + if len(spans) == 0 { + return nil } - var rootData config.SpanData = &targetSpan.Data - counts := make(map[string]int64, len(counters)) - for _, sp := range t.GetSpans() { - for _, counter := range counters { + rootSpan := findSuitableRootSpan(t) + var rootData config.SpanData + if rootSpan != nil { + rootData = &rootSpan.Data + } + + anyScoped := false + for _, c := range counters { + if len(c.ScopeConditions) > 0 { + anyScoped = true + break + } + } + if !anyScoped { + return computeCustomCountsLinear(spans, counters, rootSpan, rootData) + } + + spanIDFields := i.Config.GetSpanIdFieldNames() + parentIDFields := i.Config.GetParentIdFieldNames() + memoFields := make([]string, 0, len(spanIDFields)+len(parentIDFields)) + memoFields = append(memoFields, spanIDFields...) + memoFields = append(memoFields, parentIDFields...) + for _, sp := range spans { + sp.Data.MemoizeFields(memoFields...) + } + + childrenByIndex, forestRoots := buildSpanIndex(spans, spanIDFields, parentIDFields, i.Metrics) + + M := len(counters) + counts := make([]int64, len(spans)*M) + visited := make([]bool, len(spans)) + + for _, ri := range forestRoots { + aggregateSubtree(ri, spans, childrenByIndex, counters, rootData, counts, visited, M) + } + for idx := range visited { + if !visited[idx] { + aggregateSubtree(idx, spans, childrenByIndex, counters, rootData, counts, visited, M) + } + } + + emissions := make(map[*types.Span][]customCountWrite) + for c, counter := range counters { + if len(counter.ScopeConditions) > 0 { + for si, sp := range spans { + if counter.MatchesScope(&sp.Data, rootData) { + emissions[sp] = append(emissions[sp], customCountWrite{counter.Key, counts[si*M+c]}) + } + } + } + if counter.ShouldEmitTotalOnRoot() && rootSpan != nil { + var total int64 + if len(counter.ScopeConditions) > 0 { + for _, ri := range forestRoots { + total += counts[ri*M+c] + } + } else { + for si := range spans { + total += counts[si*M+c] + } + } + emissions[rootSpan] = append(emissions[rootSpan], customCountWrite{counter.Key, total}) + } + } + + return emissions +} + +// computeCustomCountsLinear implements the unscoped fast path: a single linear +// pass over spans accumulating one int64 per counter, written to the root. +func computeCustomCountsLinear(spans []*types.Span, counters []config.SpanCounter, rootSpan *types.Span, rootData config.SpanData) map[*types.Span][]customCountWrite { + if rootSpan == nil { + return nil + } + totals := make([]int64, len(counters)) + for _, sp := range spans { + for c, counter := range counters { if counter.MatchesSpan(&sp.Data, rootData) { - counts[counter.Key]++ + totals[c]++ } } } + writes := make([]customCountWrite, 0, len(counters)) + for c, counter := range counters { + writes = append(writes, customCountWrite{counter.Key, totals[c]}) + } + return map[*types.Span][]customCountWrite{rootSpan: writes} +} + +// spanIDFromPayload reads the first present configured ID field and narrows +// it to a string. Returns ("", false) for missing/empty IDs and for types +// other than string/[]byte — such spans become leaf-only in the index. +func spanIDFromPayload(p config.SpanData, fields []string) (string, bool) { + for _, f := range fields { + if !p.Exists(f) { + continue + } + switch v := p.Get(f).(type) { + case string: + if v == "" { + return "", false + } + return v, true + case []byte: + if len(v) == 0 { + return "", false + } + return string(v), true + } + return "", false + } + return "", false +} + +// buildSpanIndex constructs the parent->children index used by the scoped +// aggregation pass. childrenByIndex[i] holds the span indices of span i's +// children, indexed for O(1) DFS lookups (no string keys in the hot path). +// forestRoots holds the indices of spans with no parent or whose parent ID +// is not present in this trace; self-loops are also routed through +// forestRoots so the cycle-defense pass guarantees visitation. Collisions +// on span ID are logged via the span_counter_id_collision metric and +// resolved last-write-wins. +func buildSpanIndex(spans []*types.Span, spanIDFields, parentIDFields []string, m metrics.Metrics) ([][]int, []int) { + idToIndex := make(map[string]int, len(spans)) + for i, sp := range spans { + id, ok := spanIDFromPayload(&sp.Data, spanIDFields) + if !ok { + continue + } + if _, exists := idToIndex[id]; exists { + if m != nil { + m.Increment("span_counter_id_collision") + } + } + idToIndex[id] = i + } + + childrenByIndex := make([][]int, len(spans)) + var forestRoots []int + for i, sp := range spans { + parentID, parentOk := spanIDFromPayload(&sp.Data, parentIDFields) + if !parentOk { + forestRoots = append(forestRoots, i) + continue + } + parentIdx, parentInTrace := idToIndex[parentID] + if !parentInTrace || parentIdx == i { + forestRoots = append(forestRoots, i) + continue + } + childrenByIndex[parentIdx] = append(childrenByIndex[parentIdx], i) + } + return childrenByIndex, forestRoots +} + +// aggregateSubtree runs iterative post-order DFS from rootIndex, populating +// counts[span*M+c] with each counter's subtree count (children's sums plus 1 +// for each counter whose Conditions match the span itself). visited gates +// re-entry so cycles terminate; M is the per-span counter stride. +func aggregateSubtree( + rootIndex int, + spans []*types.Span, + childrenByIndex [][]int, + counters []config.SpanCounter, + rootData config.SpanData, + counts []int64, + visited []bool, + M int, +) { + if visited[rootIndex] { + return + } + type frame struct { + spanIndex int + childCursor int + } + stack := []frame{{spanIndex: rootIndex, childCursor: 0}} + visited[rootIndex] = true + + for len(stack) > 0 { + top := &stack[len(stack)-1] + children := childrenByIndex[top.spanIndex] + if top.childCursor < len(children) { + childIdx := children[top.childCursor] + top.childCursor++ + if visited[childIdx] { + continue + } + visited[childIdx] = true + stack = append(stack, frame{spanIndex: childIdx, childCursor: 0}) + continue + } - return targetSpan, counts + sp := spans[top.spanIndex] + base := top.spanIndex * M + for c, counter := range counters { + if counter.MatchesSpan(&sp.Data, rootData) { + counts[base+c] = 1 + } + } + for _, childIdx := range children { + cbase := childIdx * M + for c := range counters { + counts[base+c] += counts[cbase+c] + } + } + stack = stack[:len(stack)-1] + } } func (i *InMemCollector) sendTraces() { @@ -790,7 +1004,7 @@ func (i *InMemCollector) sendTraces() { i.Metrics.Histogram("collector_outgoing_queue", float64(len(i.tracesToSend))) _, span := otelutil.StartSpanMulti(context.Background(), i.Tracer, "sendTrace", map[string]interface{}{"num_spans": t.DescendantCount(), "tracesToSend_size": len(i.tracesToSend)}) - customCountTarget, customCounts := i.computeCustomCounts(t) + customCounts := i.computeCustomCounts(t) for _, sp := range t.GetSpans() { @@ -815,11 +1029,8 @@ func (i *InMemCollector) sendTraces() { } } - // set custom span counts on the target span (root if present, else best fallback) - if sp == customCountTarget { - for k, v := range customCounts { - sp.Data.Set(k, v) - } + for _, w := range customCounts[sp] { + sp.Data.Set(w.key, w.value) } isDryRun := i.Config.GetIsDryRun() diff --git a/collect/collect_test.go b/collect/collect_test.go index e0a7b97f34..a9e8b71339 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -1918,11 +1918,12 @@ func customCountConf(counters []config.SpanCounter) *config.MockConfig { GetSamplerTypeVal: &config.DeterministicSamplerConfig{SampleRate: 1}, TraceIdFieldNames: []string{"trace.trace_id", "traceId"}, ParentIdFieldNames: []string{"trace.parent_id", "parentId"}, + SpanIdFieldNames: []string{"trace.span_id", "spanId"}, GetCollectionConfigVal: config.CollectionConfig{ WorkerCount: 2, ShutdownDelay: config.Duration(1 * time.Millisecond), - IncomingQueueSize: 10, - PeerQueueSize: 10, + IncomingQueueSize: 1000, + PeerQueueSize: 1000, }, SpanCounters: counters, } @@ -2162,6 +2163,572 @@ func TestCustomSpanCounts_NoRootSpan(t *testing.T) { assert.Equal(t, int64(2), counted[0].Data.Get("all_spans"), "both spans should be counted") } +// addPeerSpan is a tiny helper for the scoped SpanCounter tests: it constructs +// a non-root span with the given data and pushes it via AddSpanFromPeer. +func addPeerSpan(t *testing.T, coll *InMemCollector, traceID string, data map[string]any) { + t.Helper() + coll.AddSpanFromPeer(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, data), + APIKey: legacyAPIKey, + }, + }) +} + +// addRootSpan is a tiny helper for the scoped SpanCounter tests: it constructs +// a root span with the given data and pushes it via AddSpan. +func addRootSpan(t *testing.T, coll *InMemCollector, traceID string, data map[string]any) { + t.Helper() + coll.AddSpan(&types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{ + Dataset: "test", + Data: types.NewPayload(coll.Config, data), + APIKey: legacyAPIKey, + }, + }) +} + +// findEventBySpanID returns the first event whose span ID matches. +func findEventBySpanID(events []*types.Event, id string) *types.Event { + for _, ev := range events { + if ev.Data.Get("trace.span_id") == id { + return ev + } + } + return nil +} + +// TestCustomSpanCounts_Scoped_MultipleAnchors verifies that a single +// ScopeConditions-equipped counter writes per-anchor subtree counts. +// +// Trace shape (5 resolver anchors, each with 2 db.query descendants): +// +// root (s0) +// ├── r1 ── db1a, db1b +// ├── r2 ── db2a, db2b +// ├── r3 ── db3a, db3b +// ├── r4 ── db4a, db4b +// └── r5 ── db5a, db5b +func TestCustomSpanCounts_Scoped_MultipleAnchors(t *testing.T) { + emitFalse := false + counters := []config.SpanCounter{{ + Key: "db_call_count", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + EmitTotalOnRoot: &emitFalse, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "scoped-many-anchors" + + for r := 1; r <= 5; r++ { + resolverID := fmt.Sprintf("r%d", r) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": resolverID, + "trace.parent_id": "s0", + "graphql.operation.name": fmt.Sprintf("Query%d", r), + }) + for d := 0; d < 2; d++ { + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": fmt.Sprintf("db%d%d", r, d), + "trace.parent_id": resolverID, + "name": "db.query", + }) + } + } + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(16) + require.Equal(t, 16, len(events)) + + for r := 1; r <= 5; r++ { + ev := findEventBySpanID(events, fmt.Sprintf("r%d", r)) + require.NotNil(t, ev, "resolver r%d missing", r) + assert.Equal(t, int64(2), ev.Data.Get("db_call_count"), "resolver r%d", r) + } + + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Nil(t, root.Data.Get("db_call_count"), "EmitTotalOnRoot=false → no root write") + for r := 1; r <= 5; r++ { + for d := 0; d < 2; d++ { + ev := findEventBySpanID(events, fmt.Sprintf("db%d%d", r, d)) + require.NotNil(t, ev) + assert.Nil(t, ev.Data.Get("db_call_count"), "leaf spans should not be written to") + } + } +} + +// TestCustomSpanCounts_Scoped_EmitTotalOnRoot verifies that when +// EmitTotalOnRoot=true, the root also receives the trace-wide total along +// with per-anchor counts. +func TestCustomSpanCounts_Scoped_EmitTotalOnRoot(t *testing.T) { + emitTrue := true + counters := []config.SpanCounter{{ + Key: "db_call_count", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + EmitTotalOnRoot: &emitTrue, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "scoped-with-total" + for r := 1; r <= 3; r++ { + resolverID := fmt.Sprintf("r%d", r) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": resolverID, + "trace.parent_id": "s0", + "graphql.operation.name": fmt.Sprintf("Query%d", r), + }) + for d := 0; d < 4; d++ { + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": fmt.Sprintf("db%d%d", r, d), + "trace.parent_id": resolverID, + "name": "db.query", + }) + } + } + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(16) + require.Equal(t, 16, len(events)) + + for r := 1; r <= 3; r++ { + ev := findEventBySpanID(events, fmt.Sprintf("r%d", r)) + require.NotNil(t, ev) + assert.Equal(t, int64(4), ev.Data.Get("db_call_count")) + } + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(12), root.Data.Get("db_call_count"), "root should get trace-wide total") +} + +// TestCustomSpanCounts_Scoped_NestedAnchors verifies that an outer anchor's +// count includes the inner anchor's subtree (no special-casing of nested +// anchors). +// +// root (s0) +// └── outer (anchor) ── db_outer +// └── inner (anchor) ── db_inner1, db_inner2 +func TestCustomSpanCounts_Scoped_NestedAnchors(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "db_calls", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "anchor", Operator: config.EQ, Value: true}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "nested" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "outer", + "trace.parent_id": "s0", + "anchor": true, + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "db_outer", + "trace.parent_id": "outer", + "name": "db.query", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "inner", + "trace.parent_id": "outer", + "anchor": true, + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "db_inner1", + "trace.parent_id": "inner", + "name": "db.query", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "db_inner2", + "trace.parent_id": "inner", + "name": "db.query", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(6) + require.Equal(t, 6, len(events)) + + outer := findEventBySpanID(events, "outer") + inner := findEventBySpanID(events, "inner") + require.NotNil(t, outer) + require.NotNil(t, inner) + assert.Equal(t, int64(3), outer.Data.Get("db_calls"), "outer subtree: db_outer + db_inner1 + db_inner2") + assert.Equal(t, int64(2), inner.Data.Get("db_calls"), "inner subtree: db_inner1 + db_inner2") +} + +// TestCustomSpanCounts_Scoped_AnchorMatchesRoot verifies the root span can +// itself be an anchor, in which case the count appears on it once. +func TestCustomSpanCounts_Scoped_AnchorMatchesRoot(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "all", + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "kind", Operator: config.EQ, Value: "server"}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "anchor-is-root" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c1", + "trace.parent_id": "s0", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c2", + "trace.parent_id": "s0", + }) + addRootSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "s0", + "kind": "server", + }) + + events := transmission.GetBlock(3) + require.Equal(t, 3, len(events)) + + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(3), root.Data.Get("all"), "anchor-as-root counts whole subtree") + for _, id := range []string{"c1", "c2"} { + ev := findEventBySpanID(events, id) + require.NotNil(t, ev) + assert.Nil(t, ev.Data.Get("all")) + } +} + +// TestCustomSpanCounts_Scoped_AnchorMatchesNothing verifies that when no span +// matches ScopeConditions, no anchor writes occur; with EmitTotalOnRoot=true +// the root still receives the trace-wide total. +func TestCustomSpanCounts_Scoped_AnchorMatchesNothing(t *testing.T) { + emitTrue := true + counters := []config.SpanCounter{{ + Key: "errs", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "error", Operator: config.EQ, Value: true}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "no-such-anchor", Operator: config.Exists}, + }, + EmitTotalOnRoot: &emitTrue, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "anchor-zero" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c1", + "trace.parent_id": "s0", + "error": true, + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c2", + "trace.parent_id": "s0", + "error": true, + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(3) + require.Equal(t, 3, len(events)) + + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(2), root.Data.Get("errs"), "EmitTotalOnRoot=true → root has trace-wide total") +} + +// TestCustomSpanCounts_Scoped_AnchorMatchesEverySpan verifies a permissive +// scope (everything is an anchor) — every span receives its own subtree count. +func TestCustomSpanCounts_Scoped_AnchorMatchesEverySpan(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "subtree", + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "trace.span_id", Operator: config.Exists}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "all-anchors" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c1", + "trace.parent_id": "s0", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c2", + "trace.parent_id": "c1", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(3) + require.Equal(t, 3, len(events)) + + assert.Equal(t, int64(3), findEventBySpanID(events, "s0").Data.Get("subtree")) + assert.Equal(t, int64(2), findEventBySpanID(events, "c1").Data.Get("subtree")) + assert.Equal(t, int64(1), findEventBySpanID(events, "c2").Data.Get("subtree")) +} + +// TestCustomSpanCounts_Scoped_MultiForestEmitTotal verifies that a trace with +// two forest roots (a missing intermediate span — e.g., a load balancer not +// in Refinery's view) produces a correct trace-wide total when +// EmitTotalOnRoot=true. The total sums each forest's subtree counts. +func TestCustomSpanCounts_Scoped_MultiForestEmitTotal(t *testing.T) { + emitTrue := true + counters := []config.SpanCounter{{ + Key: "db_call_count", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + EmitTotalOnRoot: &emitTrue, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "multi-forest" + // Forest A: parent points to a missing "missing-lb" span. + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "a1", + "trace.parent_id": "missing-lb", + "graphql.operation.name": "QueryA", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "a_db1", + "trace.parent_id": "a1", + "name": "db.query", + }) + // Forest B (with the root span the chooser will pick). + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "b1", + "trace.parent_id": "s0", + "graphql.operation.name": "QueryB", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "b_db1", + "trace.parent_id": "b1", + "name": "db.query", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "b_db2", + "trace.parent_id": "b1", + "name": "db.query", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(6) + require.Equal(t, 6, len(events)) + + assert.Equal(t, int64(1), findEventBySpanID(events, "a1").Data.Get("db_call_count")) + assert.Equal(t, int64(2), findEventBySpanID(events, "b1").Data.Get("db_call_count")) + assert.Equal(t, int64(3), findEventBySpanID(events, "s0").Data.Get("db_call_count"), + "trace-wide total must sum across both forest roots") +} + +// TestCustomSpanCounts_Scoped_MultiForestNoTotal verifies that with +// EmitTotalOnRoot=false, anchors in disjoint forests still get correct +// per-anchor counts and no root write happens. +func TestCustomSpanCounts_Scoped_MultiForestNoTotal(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "db_call_count", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "multi-forest-no-total" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "a1", + "trace.parent_id": "missing-lb", + "graphql.operation.name": "QueryA", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "a_db", + "trace.parent_id": "a1", + "name": "db.query", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "b1", + "trace.parent_id": "s0", + "graphql.operation.name": "QueryB", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "b_db", + "trace.parent_id": "b1", + "name": "db.query", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(5) + require.Equal(t, 5, len(events)) + + assert.Equal(t, int64(1), findEventBySpanID(events, "a1").Data.Get("db_call_count")) + assert.Equal(t, int64(1), findEventBySpanID(events, "b1").Data.Get("db_call_count")) + assert.Nil(t, findEventBySpanID(events, "s0").Data.Get("db_call_count"), + "EmitTotalOnRoot defaults to false when scope set") +} + +// TestCustomSpanCounts_Scoped_TwoCycleDefense verifies that a parent-ID cycle +// (X.parent=Y, Y.parent=X), neither a forest root, does not cause an infinite +// loop and that both spans get a count via the unvisited-island pass. +func TestCustomSpanCounts_Scoped_TwoCycleDefense(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "self", + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "trace.span_id", Operator: config.Exists}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "two-cycle" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "x", + "trace.parent_id": "y", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "y", + "trace.parent_id": "x", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(3) + require.Equal(t, 3, len(events)) + + // Both cycle members are visited and each has a count, even though they + // are not reachable from the forest root. + x := findEventBySpanID(events, "x") + y := findEventBySpanID(events, "y") + require.NotNil(t, x) + require.NotNil(t, y) + // At least one of x/y must have a count > 0 — the unvisited-island pass + // picks a starting node and treats the cycle as its own tree. + xCount, _ := x.Data.Get("self").(int64) + yCount, _ := y.Data.Get("self").(int64) + assert.GreaterOrEqual(t, xCount, int64(1)) + assert.GreaterOrEqual(t, yCount, int64(1)) +} + +// TestCustomSpanCounts_Scoped_SelfLoopDefense verifies that a span whose +// parent ID equals its own span ID is treated as a forest root and counted +// once (its own contribution). +func TestCustomSpanCounts_Scoped_SelfLoopDefense(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "self", + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "trace.span_id", Operator: config.Exists}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "self-loop" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "loopy", + "trace.parent_id": "loopy", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(2) + require.Equal(t, 2, len(events)) + assert.Equal(t, int64(1), findEventBySpanID(events, "loopy").Data.Get("self")) +} + +// TestCustomSpanCounts_Scoped_SpanIDCollision verifies that two spans with +// the same span ID don't panic and the span_counter_id_collision metric is +// incremented. +func TestCustomSpanCounts_Scoped_SpanIDCollision(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "subtree", + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "trace.span_id", Operator: config.Exists}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + m := coll.Metrics.(*metrics.MockMetrics) + + traceID := "id-collision" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "dup", + "trace.parent_id": "s0", + }) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "dup", // same ID as the previous one + "trace.parent_id": "s0", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(3) + require.Equal(t, 3, len(events)) + assert.GreaterOrEqual(t, m.CounterIncrements["span_counter_id_collision"], int64(1), + "collision metric should be incremented at least once") +} + +// TestCustomSpanCounts_BackwardsCompat_FastPath verifies that with no +// ScopeConditions configured the fast path emits the same trace-wide total +// on the root span as before — bit-for-bit identical to the original +// behavior. +func TestCustomSpanCounts_BackwardsCompat_FastPath(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "db", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "backwards-compat" + for i := 0; i < 3; i++ { + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": fmt.Sprintf("c%d", i), + "trace.parent_id": "s0", + "name": "db.query", + }) + } + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c3", + "trace.parent_id": "s0", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(5) + require.Equal(t, 5, len(events)) + + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(3), root.Data.Get("db")) + for _, id := range []string{"c0", "c1", "c2", "c3"} { + ev := findEventBySpanID(events, id) + require.NotNil(t, ev) + assert.Nil(t, ev.Data.Get("db"), "child span should not carry the counter on the fast path") + } +} + // BenchmarkCollectorWithSamplers runs benchmarks for different sampler configurations. // This is a tricky benchmark to interpret because just setting up the input data // can easily be more expensive than the collector's routing code. The goal is to @@ -2479,3 +3046,148 @@ func (c *mockSender) waitForCount(target int) { } } } + +// makeBenchmarkTrace builds a synthetic 2,868-span trace shaped like a typical +// resolver-heavy graphql workload: 1 root, `anchorCount` resolver spans that +// satisfy the scoped tests' ScopeConditions, and the remainder distributed as +// "db.query" children under the resolvers (plus filler). +// +// The Refinery payload is constructed via types.NewPayload so MemoizeFields +// behaves the same as in production. +func makeBenchmarkTrace(cfg config.Config, totalSpans, anchorCount int) *types.Trace { + traceID := "bench" + trace := &types.Trace{ + TraceID: traceID, + Dataset: "bench", + APIKey: legacyAPIKey, + ArrivalTime: time.Now(), + } + + root := &types.Span{ + TraceID: traceID, + IsRoot: true, + Event: &types.Event{ + Dataset: "bench", + Data: types.NewPayload(cfg, map[string]any{ + "trace.span_id": "s0", + }), + APIKey: legacyAPIKey, + }, + } + trace.AddSpan(root) + trace.RootSpan = root + + anchors := make([]string, 0, anchorCount) + for a := 0; a < anchorCount; a++ { + anchorID := fmt.Sprintf("a%d", a) + anchors = append(anchors, anchorID) + trace.AddSpan(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "bench", + Data: types.NewPayload(cfg, map[string]any{ + "trace.span_id": anchorID, + "trace.parent_id": "s0", + "graphql.operation.name": fmt.Sprintf("Query%d", a), + }), + APIKey: legacyAPIKey, + }, + }) + } + + added := 1 + anchorCount + i := 0 + for added < totalSpans { + anchorID := anchors[i%len(anchors)] + trace.AddSpan(&types.Span{ + TraceID: traceID, + Event: &types.Event{ + Dataset: "bench", + Data: types.NewPayload(cfg, map[string]any{ + "trace.span_id": fmt.Sprintf("d%d", added), + "trace.parent_id": anchorID, + "name": "db.query", + }), + APIKey: legacyAPIKey, + }, + }) + added++ + i++ + } + return trace +} + +// makeBenchmarkCollector constructs a minimal InMemCollector bypassing the +// usual Start() machinery — we only need the fields touched by +// computeCustomCounts. Counters are initialized in place. +func makeBenchmarkCollector(b *testing.B, counters []config.SpanCounter) *InMemCollector { + for j := range counters { + require.NoError(b, counters[j].Init()) + } + conf := customCountConf(counters) + m := &metrics.MockMetrics{} + m.Start() + c := &InMemCollector{ + Config: conf, + Metrics: m, + spanCounters: counters, + } + return c +} + +func benchmarkComputeCustomCounts(b *testing.B, counters []config.SpanCounter) { + c := makeBenchmarkCollector(b, counters) + trace := makeBenchmarkTrace(c.Config, 2868, 5) + st := sendableTrace{Trace: trace} + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = c.computeCustomCounts(st) + } +} + +// BenchmarkComputeCustomCounts_NoScope exercises the fast path: a single +// unscoped counter on a 2,868-span trace. Should track today's +// implementation's cost (no DFS, no index). +func BenchmarkComputeCustomCounts_NoScope(b *testing.B) { + counters := []config.SpanCounter{{ + Key: "db", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + }} + benchmarkComputeCustomCounts(b, counters) +} + +// BenchmarkComputeCustomCounts_Scoped exercises the scoped path: one scoped +// counter with 5 anchors on a 2,868-span trace. +func BenchmarkComputeCustomCounts_Scoped(b *testing.B) { + counters := []config.SpanCounter{{ + Key: "db", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + }} + benchmarkComputeCustomCounts(b, counters) +} + +// BenchmarkComputeCustomCounts_ScopedMulti exercises the per-counter loop in +// the write pass: 5 scoped counters, each with 5 anchors, on a 2,868-span +// trace. +func BenchmarkComputeCustomCounts_ScopedMulti(b *testing.B) { + counters := make([]config.SpanCounter, 5) + for i := range counters { + counters[i] = config.SpanCounter{ + Key: fmt.Sprintf("k%d", i), + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + } + } + benchmarkComputeCustomCounts(b, counters) +} diff --git a/config.md b/config.md index 23178b357d..adbd705b89 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-04-09 at 22:21:32 UTC. +It was automatically generated on 2026-05-27 at 17:33:16 UTC. ## The Config file @@ -1099,6 +1099,17 @@ A trace without a `parent_id` is assumed to be a root span. - Type: `stringarray` - Example: `trace.parent_id,parentId` +### `SpanNames` + +SpanNames is the list of field names to use for the span ID. + +The first field in the list that is present on a span will be used as that span's ID. +This is required for `SpanCounters` entries that set `ScopeConditions` (per-anchor subtree counting), which must resolve each span's parent ID to a span ID in the same trace. + +- Eligible for live reload. +- Type: `stringarray` +- Example: `trace.span_id,spanId` + ## gRPC Server Parameters `GRPCServerParameters` controls the parameters of the gRPC server used to receive OpenTelemetry data in gRPC format. diff --git a/config/config.go b/config/config.go index a709cf1b41..02e45efa93 100644 --- a/config/config.go +++ b/config/config.go @@ -165,6 +165,8 @@ type Config interface { GetParentIdFieldNames() []string + GetSpanIdFieldNames() []string + GetOpAMPConfig() OpAMPConfig } diff --git a/config/file_config.go b/config/file_config.go index c4aacc0d0c..be6e3e122c 100644 --- a/config/file_config.go +++ b/config/file_config.go @@ -381,6 +381,7 @@ type SpecializedConfig struct { type IDFieldsConfig struct { TraceNames []string `yaml:"TraceNames" default:"[\"trace.trace_id\",\"traceId\"]"` ParentNames []string `yaml:"ParentNames" default:"[\"trace.parent_id\",\"parentId\"]"` + SpanNames []string `yaml:"SpanNames" default:"[\"trace.span_id\",\"spanId\"]"` } // GRPCServerParameters allow you to configure the GRPC ServerParameters used @@ -1162,6 +1163,13 @@ func (f *fileConfig) GetParentIdFieldNames() []string { return f.mainConfig.IDFieldNames.ParentNames } +func (f *fileConfig) GetSpanIdFieldNames() []string { + f.mux.RLock() + defer f.mux.RUnlock() + + return f.mainConfig.IDFieldNames.SpanNames +} + func (f *fileConfig) GetConfigMetadata() []ConfigMetadata { ret := make([]ConfigMetadata, 2) ret[0] = ConfigMetadata{ diff --git a/config/metadata/configMeta.yaml b/config/metadata/configMeta.yaml index 32c4ff1aed..5907c23b3e 100644 --- a/config/metadata/configMeta.yaml +++ b/config/metadata/configMeta.yaml @@ -1768,6 +1768,21 @@ groups: as the parent ID. A trace without a `parent_id` is assumed to be a root span. + - name: SpanNames + type: stringarray + valuetype: stringarray + example: "trace.span_id,spanId" + reload: true + validations: + - type: elementType + arg: string + summary: is the list of field names to use for the span ID. + description: > + The first field in the list that is present on a span will be used + as that span's ID. This is required for `SpanCounters` entries that + set `ScopeConditions` (per-anchor subtree counting), which must + resolve each span's parent ID to a span ID in the same trace. + - name: GRPCServerParameters title: "gRPC Server Parameters" description: > diff --git a/config/metadata/rulesMeta.yaml b/config/metadata/rulesMeta.yaml index a4a9a5c823..a7c651187b 100644 --- a/config/metadata/rulesMeta.yaml +++ b/config/metadata/rulesMeta.yaml @@ -744,21 +744,28 @@ groups: sortorder: 80 description: > Defines a single custom span counter. Each counter has a Key that names - the field written to the root span, and an optional list of Conditions - that must all match for a span to be counted. Spans are counted when - all of the entry's Conditions match. If Conditions is empty, every span - in the trace is counted. The counter value is written to the root span - under the key specified by `Key`. If no root span exists when the trace - is sent, the counter is written to the first non-annotation span instead. + the field written to a target span and an optional list of Conditions + that must all match for a span to be counted. By default the trace-wide + count is written to the root span under Key. When ScopeConditions is + set, every span matching ScopeConditions instead receives the count of + matching descendant spans in its own subtree, and EmitTotalOnRoot + controls whether the trace-wide total is additionally written to the + root. If no root span exists when the trace is sent, root writes go to + the first non-annotation span instead. fields: - name: Key type: string validations: - type: notempty - summary: is the field name written to the root span with the counter value. + summary: is the field name written to the target span with the counter value. description: > - The name of the field that will be added to the root span. Must not - be empty. + The name of the field that will be added to each target span. Must + not be empty. Keys in the `meta.refinery.` namespace are reserved + for Refinery's own metadata and are rejected at validation. Keys + starting with `meta.` produce a warning, because int fields with a + value of `0` cannot be distinguished from a missing field on the + wire — meaning zero-count anchors will appear absent to downstream + queries. - name: Conditions type: objectarray @@ -766,4 +773,31 @@ groups: description: > All conditions must match for a span to be counted. If empty, every span in the trace is counted. Uses the same condition format as - rules-based sampler conditions. + rules-based sampler conditions. An anchor span (one matching + `ScopeConditions`) is also tested against Conditions like any + other span — if it matches, it counts itself. + + - name: ScopeConditions + type: objectarray + summary: is an optional list of conditions selecting per-anchor target spans. + description: > + When set, each span satisfying all of these conditions becomes an + "anchor" and receives the count of matching descendant spans in its + own subtree (including the anchor span itself when it matches + `Conditions`). When omitted, the counter writes a single trace-wide + total to the root span — the original SpanCounter behavior. Nested + anchors are not special-cased: an outer anchor's count includes + the descendant subtree even if it crosses an inner anchor. Uses + the same condition format as rules-based sampler conditions; the + trace-level `has-root-span` operator is rejected at validation. + + - name: EmitTotalOnRoot + type: bool + summary: controls whether the trace-wide total is also written to the root span. + description: > + When ScopeConditions is empty this defaults to `true` (today's + behavior — the trace-wide total is written to the root). When + ScopeConditions is non-empty this defaults to `false` (only the + per-anchor counts are written). Setting it explicitly overrides + the default. Setting `false` with no ScopeConditions disables all + writes for the counter and produces a validation warning. diff --git a/config/mock.go b/config/mock.go index 58660a281f..4ed6565e2a 100644 --- a/config/mock.go +++ b/config/mock.go @@ -60,6 +60,7 @@ type MockConfig struct { AdditionalHeaders map[string]string TraceIdFieldNames []string ParentIdFieldNames []string + SpanIdFieldNames []string CfgMetadata []ConfigMetadata CfgHash string RulesHash string @@ -455,6 +456,13 @@ func (f *MockConfig) GetParentIdFieldNames() []string { return f.ParentIdFieldNames } +func (f *MockConfig) GetSpanIdFieldNames() []string { + f.Mux.RLock() + defer f.Mux.RUnlock() + + return f.SpanIdFieldNames +} + func (f *MockConfig) GetConfigMetadata() []ConfigMetadata { f.Mux.RLock() defer f.Mux.RUnlock() diff --git a/config/span_counter_config.go b/config/span_counter_config.go index 1eb6c1130d..3a86901b8f 100644 --- a/config/span_counter_config.go +++ b/config/span_counter_config.go @@ -9,11 +9,20 @@ type SpanData interface { Exists(key string) bool } -// SpanCounter defines a custom span count to be computed and added to -// the root span under Key. Spans are counted if they satisfy all Conditions. +// SpanCounter defines a custom span count to be computed and emitted. +// +// By default (no ScopeConditions), Spans are counted if they satisfy all +// Conditions, and the trace-wide total is written to the root span under Key. +// +// When ScopeConditions is set, the counter is computed per-anchor: every span +// matching ScopeConditions receives the count of matching descendant spans in +// its own subtree (including itself if it matches Conditions). EmitTotalOnRoot +// controls whether the trace-wide total is additionally written to the root. type SpanCounter struct { - Key string `yaml:"Key"` - Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` + Key string `yaml:"Key"` + Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` + ScopeConditions []*RulesBasedSamplerCondition `yaml:"ScopeConditions,omitempty"` + EmitTotalOnRoot *bool `yaml:"EmitTotalOnRoot,omitempty"` } // Init initializes all conditions. Must be called before MatchesSpan. @@ -23,13 +32,44 @@ func (c *SpanCounter) Init() error { return err } } + for _, cond := range c.ScopeConditions { + if err := cond.Init(); err != nil { + return err + } + } return nil } -// MatchesSpan returns true if the span satisfies all conditions. +// MatchesSpan returns true if the span satisfies all Conditions. // span is the span being tested; root is the root span's data (may be nil). func (c *SpanCounter) MatchesSpan(span SpanData, root SpanData) bool { - for _, cond := range c.Conditions { + return evaluateConditions(c.Conditions, span, root) +} + +// MatchesScope returns true if the span satisfies all ScopeConditions. +// Returns false if ScopeConditions is empty (an unscoped counter has no +// per-anchor anchors). span is the span being tested; root is the root +// span's data (may be nil). +func (c *SpanCounter) MatchesScope(span SpanData, root SpanData) bool { + if len(c.ScopeConditions) == 0 { + return false + } + return evaluateConditions(c.ScopeConditions, span, root) +} + +// ShouldEmitTotalOnRoot reports whether the trace-wide total should be +// written to the root span. Defaults to true when ScopeConditions is empty +// (today's behavior) and false when ScopeConditions is set, unless an +// explicit EmitTotalOnRoot value overrides. +func (c *SpanCounter) ShouldEmitTotalOnRoot() bool { + if c.EmitTotalOnRoot != nil { + return *c.EmitTotalOnRoot + } + return len(c.ScopeConditions) == 0 +} + +func evaluateConditions(conditions []*RulesBasedSamplerCondition, span SpanData, root SpanData) bool { + for _, cond := range conditions { var value any var exists bool for _, field := range cond.Fields { diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go index 287e438aa2..25bd079284 100644 --- a/config/span_counter_config_test.go +++ b/config/span_counter_config_test.go @@ -1,9 +1,11 @@ package config import ( + "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // spanData is a simple map-backed implementation of SpanData for tests. @@ -300,3 +302,153 @@ func TestMatchesSpan_ExistsAndNotExists(t *testing.T) { assert.False(t, notExists.MatchesSpan(withField, nil)) assert.True(t, notExists.MatchesSpan(without, nil)) } + +// ---------------------------------------------------------------------------- +// SpanCounter.MatchesScope / ShouldEmitTotalOnRoot +// ---------------------------------------------------------------------------- + +func TestMatchesScope_EmptyScopeNeverMatches(t *testing.T) { + counter := SpanCounter{Key: "k"} + assert.False(t, counter.MatchesScope(spanData{"foo": "bar"}, nil)) + assert.False(t, counter.MatchesScope(spanData{}, nil)) +} + +func TestMatchesScope_AllConditionsMustMatch(t *testing.T) { + counter := SpanCounter{ + Key: "k", + ScopeConditions: []*RulesBasedSamplerCondition{ + cond("graphql.operation.name", Exists, nil), + cond("kind", EQ, "server"), + }, + } + assert.True(t, counter.MatchesScope(spanData{"graphql.operation.name": "Q", "kind": "server"}, nil)) + assert.False(t, counter.MatchesScope(spanData{"graphql.operation.name": "Q"}, nil)) + assert.False(t, counter.MatchesScope(spanData{"kind": "server"}, nil)) +} + +func TestMatchesScope_RootPrefixSupported(t *testing.T) { + counter := SpanCounter{ + Key: "k", + ScopeConditions: []*RulesBasedSamplerCondition{ + cond("root.service.name", EQ, "api"), + }, + } + assert.True(t, counter.MatchesScope(spanData{}, spanData{"service.name": "api"})) + assert.False(t, counter.MatchesScope(spanData{}, spanData{"service.name": "worker"})) + assert.False(t, counter.MatchesScope(spanData{}, nil)) +} + +func TestShouldEmitTotalOnRoot_Defaults(t *testing.T) { + // No ScopeConditions, no override → true (today's behavior). + unscoped := SpanCounter{Key: "k"} + assert.True(t, unscoped.ShouldEmitTotalOnRoot()) + + // ScopeConditions set, no override → false (per-anchor-only). + scoped := SpanCounter{ + Key: "k", + ScopeConditions: []*RulesBasedSamplerCondition{ + cond("anchor", Exists, nil), + }, + } + assert.False(t, scoped.ShouldEmitTotalOnRoot()) +} + +func TestShouldEmitTotalOnRoot_ExplicitOverride(t *testing.T) { + tr := true + fa := false + + // Override true with no scope. + c := SpanCounter{Key: "k", EmitTotalOnRoot: &tr} + assert.True(t, c.ShouldEmitTotalOnRoot()) + + // Override false with no scope (no-op). + c = SpanCounter{Key: "k", EmitTotalOnRoot: &fa} + assert.False(t, c.ShouldEmitTotalOnRoot()) + + // Override true with scope. + c = SpanCounter{ + Key: "k", + EmitTotalOnRoot: &tr, + ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, + } + assert.True(t, c.ShouldEmitTotalOnRoot()) + + // Override false with scope (matches default). + c = SpanCounter{ + Key: "k", + EmitTotalOnRoot: &fa, + ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, + } + assert.False(t, c.ShouldEmitTotalOnRoot()) +} + +// ---------------------------------------------------------------------------- +// validateSpanCounterEntry (custom rules) +// ---------------------------------------------------------------------------- + +func TestValidateSpanCounterEntry_DuplicateKey(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{"Key": "k"}, seen) + assert.Empty(t, results) + results = validateSpanCounterEntry(1, map[string]any{"Key": "k"}, seen) + require.Len(t, results, 1) + assert.Equal(t, Error, results[0].Severity) + assert.Contains(t, results[0].Message, "duplicate Key") +} + +func TestValidateSpanCounterEntry_ReservedNamespace(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{"Key": "meta.refinery.reserved"}, seen) + require.Len(t, results, 1) + assert.Equal(t, Error, results[0].Severity) + assert.Contains(t, results[0].Message, "reserved") +} + +func TestValidateSpanCounterEntry_MetaNamespaceWarning(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{"Key": "meta.custom"}, seen) + require.Len(t, results, 1) + assert.Equal(t, Warning, results[0].Severity) + assert.Contains(t, results[0].Message, "meta.") +} + +func TestValidateSpanCounterEntry_NoopWarning(t *testing.T) { + seen := map[string]int{} + // EmitTotalOnRoot=false with no ScopeConditions → warning. + results := validateSpanCounterEntry(0, map[string]any{ + "Key": "k", + "EmitTotalOnRoot": false, + }, seen) + require.Len(t, results, 1) + assert.Equal(t, Warning, results[0].Severity) + assert.Contains(t, results[0].Message, "disables all writes") + + // EmitTotalOnRoot=false with ScopeConditions present → no warning (per-anchor still writes). + seen = map[string]int{} + results = validateSpanCounterEntry(0, map[string]any{ + "Key": "k2", + "EmitTotalOnRoot": false, + "ScopeConditions": []any{ + map[string]any{"Field": "x", "Operator": "exists"}, + }, + }, seen) + assert.Empty(t, results) +} + +func TestValidateSpanCounterEntry_HasRootSpanInScope(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{ + "Key": "k", + "ScopeConditions": []any{ + map[string]any{"Operator": HasRootSpan}, + }, + }, seen) + require.NotEmpty(t, results) + var sawErr bool + for _, r := range results { + if r.Severity == Error && strings.Contains(r.Message, HasRootSpan) { + sawErr = true + } + } + assert.True(t, sawErr, "must reject HasRootSpan in ScopeConditions") +} diff --git a/config/validate.go b/config/validate.go index 92f89f5c33..d2170dc86c 100644 --- a/config/validate.go +++ b/config/validate.go @@ -660,6 +660,7 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { Severity: Error, }) } else { + seenKeys := make(map[string]int, len(arr)) for i, entry := range arr { if entryMap, ok := entry.(map[string]any); ok { rulesmap := map[string]any{"SpanCounters": entryMap} @@ -670,6 +671,7 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { Severity: result.Severity, }) } + results = append(results, validateSpanCounterEntry(i, entryMap, seenKeys)...) } else { results = append(results, ValidationResult{ Message: fmt.Sprintf("SpanCounters[%d] must be an object, but %v is %T", i, entry, entry), @@ -722,3 +724,64 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { return results } + +// validateSpanCounterEntry runs the custom-rule validations on a single +// SpanCounter entry: no-op detection (empty ScopeConditions with +// EmitTotalOnRoot=false), Key uniqueness, reserved-namespace check on Key, +// and rejection of the trace-level HasRootSpan operator inside +// ScopeConditions. seenKeys tracks Keys already observed in this list and +// is updated in place. +func validateSpanCounterEntry(idx int, entry map[string]any, seenKeys map[string]int) ValidationResults { + var results ValidationResults + + keyStr, _ := entry["Key"].(string) + if keyStr != "" { + if prev, exists := seenKeys[keyStr]; exists { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: duplicate Key %q (also declared at SpanCounters[%d])", idx, keyStr, prev), + Severity: Error, + }) + } else { + seenKeys[keyStr] = idx + } + if strings.HasPrefix(keyStr, "meta.refinery.") { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: Key %q uses the reserved meta.refinery. namespace", idx, keyStr), + Severity: Error, + }) + } else if strings.HasPrefix(keyStr, "meta.") { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: Key %q starts with meta.; int fields with value 0 cannot be distinguished from missing", idx, keyStr), + Severity: Warning, + }) + } + } + + scope, hasScope := entry["ScopeConditions"] + scopeArr, _ := scope.([]any) + scopeIsEmpty := !hasScope || len(scopeArr) == 0 + + if v, ok := entry["EmitTotalOnRoot"]; ok { + if emit, ok := v.(bool); ok && !emit && scopeIsEmpty { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: EmitTotalOnRoot=false with no ScopeConditions disables all writes for this counter", idx), + Severity: Warning, + }) + } + } + + for ci, cond := range scopeArr { + condMap, ok := cond.(map[string]any) + if !ok { + continue + } + if op, ok := condMap["Operator"].(string); ok && op == HasRootSpan { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d].ScopeConditions[%d]: operator %q is trace-level and cannot be used as a span filter", idx, ci, HasRootSpan), + Severity: Error, + }) + } + } + + return results +} diff --git a/refinery_config.md b/refinery_config.md index 47d67bd338..6da3c7a63f 100644 --- a/refinery_config.md +++ b/refinery_config.md @@ -1086,6 +1086,17 @@ A trace without a `parent_id` is assumed to be a root span. - Type: `stringarray` - Example: `trace.parent_id,parentId` +### `SpanNames` + +`SpanNames` is the list of field names to use for the span ID. + +The first field in the list that is present on a span will be used as that span's ID. +This is required for `SpanCounters` entries that set `ScopeConditions` (per-anchor subtree counting), which must resolve each span's parent ID to a span ID in the same trace. + +- Eligible for live reload. +- Type: `stringarray` +- Example: `trace.span_id,spanId` + ## gRPC Server Parameters `GRPCServerParameters` controls the parameters of the gRPC server used to receive OpenTelemetry data in gRPC format. diff --git a/refinery_rules.md b/refinery_rules.md index 6a22a5d98c..09e343c946 100644 --- a/refinery_rules.md +++ b/refinery_rules.md @@ -674,16 +674,17 @@ If your traces are consistent lengths and changes in trace length is a useful in ## Custom Span Count Configuration Defines a single custom span counter. -Each counter has a Key that names the field written to the root span, and an optional list of Conditions that must all match for a span to be counted. -Spans are counted when all of the entry's Conditions match. -If Conditions is empty, every span in the trace is counted. -The counter value is written to the root span under the key specified by `Key`. -If no root span exists when the trace is sent, the counter is written to the first non-annotation span instead. +Each counter has a Key that names the field written to a target span and an optional list of Conditions that must all match for a span to be counted. +By default the trace-wide count is written to the root span under Key. +When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree, and EmitTotalOnRoot controls whether the trace-wide total is additionally written to the root. +If no root span exists when the trace is sent, root writes go to the first non-annotation span instead. ### `Key` -The name of the field that will be added to the root span. +The name of the field that will be added to each target span. Must not be empty. +Keys in the `meta.refinery.` namespace are reserved for Refinery's own metadata and are rejected at validation. +Keys starting with `meta.` produce a warning, because int fields with a value of `0` cannot be distinguished from a missing field on the wire — meaning zero-count anchors will appear absent to downstream queries. - Type: `string` @@ -692,6 +693,25 @@ Must not be empty. All conditions must match for a span to be counted. If empty, every span in the trace is counted. Uses the same condition format as rules-based sampler conditions. +An anchor span (one matching `ScopeConditions`) is also tested against Conditions like any other span — if it matches, it counts itself. - Type: `objectarray` +### `ScopeConditions` + +When set, each span satisfying all of these conditions becomes an "anchor" and receives the count of matching descendant spans in its own subtree (including the anchor span itself when it matches `Conditions`). +When omitted, the counter writes a single trace-wide total to the root span — the original SpanCounter behavior. +Nested anchors are not special-cased: an outer anchor's count includes the descendant subtree even if it crosses an inner anchor. +Uses the same condition format as rules-based sampler conditions; the trace-level `has-root-span` operator is rejected at validation. + +- Type: `objectarray` + +### `EmitTotalOnRoot` + +When ScopeConditions is empty this defaults to `true` (today's behavior — the trace-wide total is written to the root). +When ScopeConditions is non-empty this defaults to `false` (only the per-anchor counts are written). +Setting it explicitly overrides the default. +Setting `false` with no ScopeConditions disables all writes for the counter and produces a validation warning. + +- Type: `bool` + diff --git a/rules.md b/rules.md index a401c882d1..ec3fcba714 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-04-09 at 22:21:32 UTC. +It was automatically generated on 2026-05-27 at 17:33:09 UTC. ## The Rules file @@ -55,6 +55,7 @@ The remainder of this document describes the samplers that can be used within th - [Rules for Rules-based Samplers](#rules-for-rules-based-samplers) - [Conditions for the Rules in Rules-based Samplers](#conditions-for-the-rules-in-rules-based-samplers) - [Total Throughput Sampler](#total-throughput-sampler) +- [Custom Span Count Configuration](#custom-span-count-configuration) --- ## Deterministic Sampler @@ -715,3 +716,50 @@ If your traces are consistent lengths and changes in trace length is a useful in Type: `bool` +--- +## Custom Span Count Configuration + +### Name: `SpanCounters` + +Defines a single custom span counter. +Each counter has a Key that names the field written to a target span and an optional list of Conditions that must all match for a span to be counted. +By default the trace-wide count is written to the root span under Key. +When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree, and EmitTotalOnRoot controls whether the trace-wide total is additionally written to the root. +If no root span exists when the trace is sent, root writes go to the first non-annotation span instead. + +### `Key` + +The name of the field that will be added to each target span. +Must not be empty. +Keys in the `meta.refinery.` namespace are reserved for Refinery's own metadata and are rejected at validation. +Keys starting with `meta.` produce a warning, because int fields with a value of `0` cannot be distinguished from a missing field on the wire — meaning zero-count anchors will appear absent to downstream queries. + +Type: `string` + +### `Conditions` + +All conditions must match for a span to be counted. +If empty, every span in the trace is counted. +Uses the same condition format as rules-based sampler conditions. +An anchor span (one matching `ScopeConditions`) is also tested against Conditions like any other span — if it matches, it counts itself. + +Type: `objectarray` + +### `ScopeConditions` + +When set, each span satisfying all of these conditions becomes an "anchor" and receives the count of matching descendant spans in its own subtree (including the anchor span itself when it matches `Conditions`). +When omitted, the counter writes a single trace-wide total to the root span — the original SpanCounter behavior. +Nested anchors are not special-cased: an outer anchor's count includes the descendant subtree even if it crosses an inner anchor. +Uses the same condition format as rules-based sampler conditions; the trace-level `has-root-span` operator is rejected at validation. + +Type: `objectarray` + +### `EmitTotalOnRoot` + +When ScopeConditions is empty this defaults to `true` (today's behavior — the trace-wide total is written to the root). +When ScopeConditions is non-empty this defaults to `false` (only the per-anchor counts are written). +Setting it explicitly overrides the default. +Setting `false` with no ScopeConditions disables all writes for the counter and produces a validation warning. + +Type: `bool` + diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 06cdaaa88e..0e82059873 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-04-09 at 22:21:29 UTC. +# Automatically generated on 2026-05-27 at 17:33:22 UTC. General: - ConfigurationVersion @@ -250,6 +250,8 @@ IDFields: - ParentNames + - SpanNames + GRPCServerParameters: - Enabled diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index db18ce37f7..4815bab726 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-04-09 at 22:21:30 UTC +# automatically generated on 2026-05-27 at 17:33:21 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" @@ -128,6 +128,10 @@ IDFields: - "trace.parent_id" - parentId + SpanNames: + - "trace.span_id" + - spanId + GRPCServerParameters: Enabled: true ListenAddr: "" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 4546348b92..754864fbb8 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-04-09 at 22:21:28 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-05-27 at 17:33:21 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of @@ -1165,6 +1165,16 @@ IDFields: ## Eligible for live reload. {{ renderStringarray .Data "ParentNames" "ParentNames" "trace.parent_id,parentId" }} + ## SpanNames is the list of field names to use for the span ID. + ## + ## The first field in the list that is present on a span will be used as + ## that span's ID. This is required for `SpanCounters` entries that set + ## `ScopeConditions` (per-anchor subtree counting), which must resolve + ## each span's parent ID to a span ID in the same trace. + ## + ## Eligible for live reload. + {{ renderStringarray .Data "SpanNames" "SpanNames" "trace.span_id,spanId" }} + ############################ ## gRPC Server Parameters ## ############################ From 5db9c336567c8f9d20338302f343b9bc1896070c Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 1 Jun 2026 11:47:51 -0700 Subject: [PATCH 33/35] feat(SpanCounter): add RootKey to separate per-anchor and trace-wide field names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ScopeConditions and RootKey are both set, per-anchor counts are written under Key and the trace-wide total on the root span is written under RootKey. This lets the two counts land on different attribute names so they can be queried independently in Honeycomb (e.g. resolver_db_count on each resolver, trace_db_count on the root). RootKey is ignored when ScopeConditions is empty — unscoped counters always write Key to the root, preserving today's behavior. Validation warns if RootKey is set without a scope, and treats RootKey as a separate field for cross-counter uniqueness checks. Co-Authored-By: Claude Opus 4.7 (1M context) --- collect/collect.go | 2 +- collect/collect_test.go | 84 +++++++++++++++++++++++++++ config.md | 2 +- config/metadata/rulesMeta.yaml | 27 +++++++-- config/span_counter_config.go | 13 +++++ config/span_counter_config_test.go | 78 ++++++++++++++++++++++++- config/validate.go | 77 +++++++++++++++--------- refinery_rules.md | 10 ++++ rules.md | 12 +++- tools/convert/configDataNames.txt | 2 +- tools/convert/minimal_config.yaml | 2 +- tools/convert/templates/configV2.tmpl | 2 +- 12 files changed, 271 insertions(+), 40 deletions(-) diff --git a/collect/collect.go b/collect/collect.go index 58d21c4115..5975274fe0 100644 --- a/collect/collect.go +++ b/collect/collect.go @@ -848,7 +848,7 @@ func (i *InMemCollector) computeCustomCounts(t sendableTrace) map[*types.Span][] total += counts[si*M+c] } } - emissions[rootSpan] = append(emissions[rootSpan], customCountWrite{counter.Key, total}) + emissions[rootSpan] = append(emissions[rootSpan], customCountWrite{counter.EffectiveRootKey(), total}) } } diff --git a/collect/collect_test.go b/collect/collect_test.go index a9e8b71339..b439d464a3 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -2317,6 +2317,90 @@ func TestCustomSpanCounts_Scoped_EmitTotalOnRoot(t *testing.T) { assert.Equal(t, int64(12), root.Data.Get("db_call_count"), "root should get trace-wide total") } +// TestCustomSpanCounts_Scoped_RootKey verifies that when both +// ScopeConditions and RootKey are set, the per-anchor writes use Key and +// the root's trace-wide total uses RootKey — landing on a separate field +// so the two counts can be queried independently. +func TestCustomSpanCounts_Scoped_RootKey(t *testing.T) { + emitTrue := true + counters := []config.SpanCounter{{ + Key: "resolver_db_count", + RootKey: "trace_db_count", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + ScopeConditions: []*config.RulesBasedSamplerCondition{ + {Field: "graphql.operation.name", Operator: config.Exists}, + }, + EmitTotalOnRoot: &emitTrue, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "rootkey" + for r := 1; r <= 2; r++ { + resolverID := fmt.Sprintf("r%d", r) + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": resolverID, + "trace.parent_id": "s0", + "graphql.operation.name": fmt.Sprintf("Query%d", r), + }) + for d := 0; d < 3; d++ { + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": fmt.Sprintf("db%d%d", r, d), + "trace.parent_id": resolverID, + "name": "db.query", + }) + } + } + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(9) + require.Equal(t, 9, len(events)) + + for r := 1; r <= 2; r++ { + ev := findEventBySpanID(events, fmt.Sprintf("r%d", r)) + require.NotNil(t, ev) + assert.Equal(t, int64(3), ev.Data.Get("resolver_db_count"), "anchor gets Key") + assert.Nil(t, ev.Data.Get("trace_db_count"), "anchor does not get RootKey") + } + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(6), root.Data.Get("trace_db_count"), "root gets RootKey for total") + assert.Nil(t, root.Data.Get("resolver_db_count"), "root does not get Key when RootKey overrides") +} + +// TestCustomSpanCounts_Unscoped_RootKeyIgnored verifies that RootKey on an +// unscoped counter (no ScopeConditions) is ignored — the root still gets +// Key, preserving today's behavior. +func TestCustomSpanCounts_Unscoped_RootKeyIgnored(t *testing.T) { + counters := []config.SpanCounter{{ + Key: "k", + RootKey: "rk", + Conditions: []*config.RulesBasedSamplerCondition{ + {Field: "name", Operator: config.EQ, Value: "db.query"}, + }, + }} + coll := newTestCollector(t, customCountConf(counters)) + transmission := coll.Transmission.(*transmit.MockTransmission) + + traceID := "rootkey-ignored" + addPeerSpan(t, coll, traceID, map[string]any{ + "trace.span_id": "c1", + "trace.parent_id": "s0", + "name": "db.query", + }) + addRootSpan(t, coll, traceID, map[string]any{"trace.span_id": "s0"}) + + events := transmission.GetBlock(2) + require.Equal(t, 2, len(events)) + + root := findEventBySpanID(events, "s0") + require.NotNil(t, root) + assert.Equal(t, int64(1), root.Data.Get("k"), "unscoped counter writes Key to root") + assert.Nil(t, root.Data.Get("rk"), "RootKey is ignored when ScopeConditions is empty") +} + // TestCustomSpanCounts_Scoped_NestedAnchors verifies that an outer anchor's // count includes the inner anchor's subtree (no special-casing of nested // anchors). diff --git a/config.md b/config.md index adbd705b89..d1ffda314a 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-05-27 at 17:33:16 UTC. +It was automatically generated on 2026-06-01 at 18:47:40 UTC. ## The Config file diff --git a/config/metadata/rulesMeta.yaml b/config/metadata/rulesMeta.yaml index a7c651187b..9aa79f067d 100644 --- a/config/metadata/rulesMeta.yaml +++ b/config/metadata/rulesMeta.yaml @@ -760,12 +760,27 @@ groups: summary: is the field name written to the target span with the counter value. description: > The name of the field that will be added to each target span. Must - not be empty. Keys in the `meta.refinery.` namespace are reserved - for Refinery's own metadata and are rejected at validation. Keys - starting with `meta.` produce a warning, because int fields with a - value of `0` cannot be distinguished from a missing field on the - wire — meaning zero-count anchors will appear absent to downstream - queries. + not be empty. When `ScopeConditions` is set, this is the field + written to each anchor span; when unset (the original behavior), + this is the field written to the root span. Keys in the + `meta.refinery.` namespace are reserved for Refinery's own metadata + and are rejected at validation. Keys starting with `meta.` produce + a warning, because int fields with a value of `0` cannot be + distinguished from a missing field on the wire — meaning + zero-count anchors will appear absent to downstream queries. + + - name: RootKey + type: string + summary: optional override for the field name written to the root span. + description: > + When set together with `ScopeConditions`, the trace-wide total on + the root span is written under `RootKey` instead of `Key`. This + lets per-anchor counts and the trace-wide total land on different + attribute names so they can be queried independently. Ignored + (with a validation warning) when `ScopeConditions` is empty — + unscoped counters always write `Key` to the root. Subject to the + same reserved-namespace rules as `Key` and counts as a separate + field for cross-counter uniqueness checks. - name: Conditions type: objectarray diff --git a/config/span_counter_config.go b/config/span_counter_config.go index 3a86901b8f..53919a2dce 100644 --- a/config/span_counter_config.go +++ b/config/span_counter_config.go @@ -20,6 +20,7 @@ type SpanData interface { // controls whether the trace-wide total is additionally written to the root. type SpanCounter struct { Key string `yaml:"Key"` + RootKey string `yaml:"RootKey,omitempty"` Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` ScopeConditions []*RulesBasedSamplerCondition `yaml:"ScopeConditions,omitempty"` EmitTotalOnRoot *bool `yaml:"EmitTotalOnRoot,omitempty"` @@ -68,6 +69,18 @@ func (c *SpanCounter) ShouldEmitTotalOnRoot() bool { return len(c.ScopeConditions) == 0 } +// EffectiveRootKey returns the field name to use when writing the trace-wide +// total to the root span. When ScopeConditions is set and RootKey is +// non-empty, RootKey is used so the per-anchor and per-trace counts land on +// separate field names. Otherwise (unscoped, or scoped with no RootKey +// override) the root write uses Key, preserving today's behavior. +func (c *SpanCounter) EffectiveRootKey() string { + if len(c.ScopeConditions) > 0 && c.RootKey != "" { + return c.RootKey + } + return c.Key +} + func evaluateConditions(conditions []*RulesBasedSamplerCondition, span SpanData, root SpanData) bool { for _, cond := range conditions { var value any diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go index 25bd079284..1d2e79186e 100644 --- a/config/span_counter_config_test.go +++ b/config/span_counter_config_test.go @@ -382,6 +382,31 @@ func TestShouldEmitTotalOnRoot_ExplicitOverride(t *testing.T) { assert.False(t, c.ShouldEmitTotalOnRoot()) } +func TestEffectiveRootKey(t *testing.T) { + // Unscoped + no RootKey → Key (today's behavior). + c := SpanCounter{Key: "k"} + assert.Equal(t, "k", c.EffectiveRootKey()) + + // Unscoped + RootKey set → still Key; RootKey is ignored when unscoped. + c = SpanCounter{Key: "k", RootKey: "rk"} + assert.Equal(t, "k", c.EffectiveRootKey()) + + // Scoped + no RootKey → Key (per-anchor and root share the same name). + c = SpanCounter{ + Key: "k", + ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, + } + assert.Equal(t, "k", c.EffectiveRootKey()) + + // Scoped + RootKey set → RootKey overrides the root write. + c = SpanCounter{ + Key: "anchor_count", + RootKey: "trace_count", + ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, + } + assert.Equal(t, "trace_count", c.EffectiveRootKey()) +} + // ---------------------------------------------------------------------------- // validateSpanCounterEntry (custom rules) // ---------------------------------------------------------------------------- @@ -393,7 +418,58 @@ func TestValidateSpanCounterEntry_DuplicateKey(t *testing.T) { results = validateSpanCounterEntry(1, map[string]any{"Key": "k"}, seen) require.Len(t, results, 1) assert.Equal(t, Error, results[0].Severity) - assert.Contains(t, results[0].Message, "duplicate Key") + assert.Contains(t, results[0].Message, "collides") +} + +func TestValidateSpanCounterEntry_RootKeyCollidesWithKey(t *testing.T) { + seen := map[string]int{} + // Counter 0 declares Key="shared". + results := validateSpanCounterEntry(0, map[string]any{"Key": "shared"}, seen) + assert.Empty(t, results) + // Counter 1 declares RootKey="shared" → collision with counter 0's Key. + results = validateSpanCounterEntry(1, map[string]any{ + "Key": "other", + "RootKey": "shared", + "ScopeConditions": []any{ + map[string]any{"Field": "x", "Operator": "exists"}, + }, + }, seen) + require.NotEmpty(t, results) + var sawErr bool + for _, r := range results { + if r.Severity == Error && strings.Contains(r.Message, "RootKey") && strings.Contains(r.Message, "collides") { + sawErr = true + } + } + assert.True(t, sawErr, "RootKey colliding with another counter's Key must error") +} + +func TestValidateSpanCounterEntry_RootKeyWithoutScopeWarns(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{ + "Key": "k", + "RootKey": "rk", + }, seen) + require.NotEmpty(t, results) + var sawWarn bool + for _, r := range results { + if r.Severity == Warning && strings.Contains(r.Message, "RootKey") && strings.Contains(r.Message, "ignored") { + sawWarn = true + } + } + assert.True(t, sawWarn, "RootKey without ScopeConditions must warn") +} + +func TestValidateSpanCounterEntry_RootKeyEqualToKeyIsNoop(t *testing.T) { + seen := map[string]int{} + results := validateSpanCounterEntry(0, map[string]any{ + "Key": "k", + "RootKey": "k", + "ScopeConditions": []any{ + map[string]any{"Field": "x", "Operator": "exists"}, + }, + }, seen) + assert.Empty(t, results, "RootKey == Key is harmless and emits no diagnostics") } func TestValidateSpanCounterEntry_ReservedNamespace(t *testing.T) { diff --git a/config/validate.go b/config/validate.go index d2170dc86c..7ec49ea280 100644 --- a/config/validate.go +++ b/config/validate.go @@ -727,40 +727,33 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { // validateSpanCounterEntry runs the custom-rule validations on a single // SpanCounter entry: no-op detection (empty ScopeConditions with -// EmitTotalOnRoot=false), Key uniqueness, reserved-namespace check on Key, -// and rejection of the trace-level HasRootSpan operator inside -// ScopeConditions. seenKeys tracks Keys already observed in this list and -// is updated in place. +// EmitTotalOnRoot=false), Key/RootKey uniqueness across all written field +// names, reserved-namespace checks on Key/RootKey, RootKey-without-scope +// warning, and rejection of the trace-level HasRootSpan operator inside +// ScopeConditions. seenKeys tracks every written field name already seen in +// this counter list and is updated in place. func validateSpanCounterEntry(idx int, entry map[string]any, seenKeys map[string]int) ValidationResults { var results ValidationResults - keyStr, _ := entry["Key"].(string) - if keyStr != "" { - if prev, exists := seenKeys[keyStr]; exists { - results = append(results, ValidationResult{ - Message: fmt.Sprintf("SpanCounters[%d]: duplicate Key %q (also declared at SpanCounters[%d])", idx, keyStr, prev), - Severity: Error, - }) - } else { - seenKeys[keyStr] = idx - } - if strings.HasPrefix(keyStr, "meta.refinery.") { - results = append(results, ValidationResult{ - Message: fmt.Sprintf("SpanCounters[%d]: Key %q uses the reserved meta.refinery. namespace", idx, keyStr), - Severity: Error, - }) - } else if strings.HasPrefix(keyStr, "meta.") { - results = append(results, ValidationResult{ - Message: fmt.Sprintf("SpanCounters[%d]: Key %q starts with meta.; int fields with value 0 cannot be distinguished from missing", idx, keyStr), - Severity: Warning, - }) - } - } - scope, hasScope := entry["ScopeConditions"] scopeArr, _ := scope.([]any) scopeIsEmpty := !hasScope || len(scopeArr) == 0 + keyStr, _ := entry["Key"].(string) + rootKeyStr, _ := entry["RootKey"].(string) + + results = append(results, validateSpanCounterFieldName(idx, "Key", keyStr, seenKeys)...) + if rootKeyStr != "" && rootKeyStr != keyStr { + results = append(results, validateSpanCounterFieldName(idx, "RootKey", rootKeyStr, seenKeys)...) + } + + if rootKeyStr != "" && scopeIsEmpty { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: RootKey is set but ScopeConditions is empty; RootKey is ignored — the root write uses Key", idx), + Severity: Warning, + }) + } + if v, ok := entry["EmitTotalOnRoot"]; ok { if emit, ok := v.(bool); ok && !emit && scopeIsEmpty { results = append(results, ValidationResult{ @@ -785,3 +778,33 @@ func validateSpanCounterEntry(idx int, entry map[string]any, seenKeys map[string return results } + +// validateSpanCounterFieldName checks one written-field-name (Key or +// RootKey) for cross-counter uniqueness and reserved-namespace violations. +// seenKeys is updated in place. +func validateSpanCounterFieldName(idx int, fieldLabel, name string, seenKeys map[string]int) ValidationResults { + if name == "" { + return nil + } + var results ValidationResults + if prev, exists := seenKeys[name]; exists && prev != idx { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: %s %q collides with a Key or RootKey already declared at SpanCounters[%d]", idx, fieldLabel, name, prev), + Severity: Error, + }) + } else { + seenKeys[name] = idx + } + if strings.HasPrefix(name, "meta.refinery.") { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: %s %q uses the reserved meta.refinery. namespace", idx, fieldLabel, name), + Severity: Error, + }) + } else if strings.HasPrefix(name, "meta.") { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d]: %s %q starts with meta.; int fields with value 0 cannot be distinguished from missing", idx, fieldLabel, name), + Severity: Warning, + }) + } + return results +} diff --git a/refinery_rules.md b/refinery_rules.md index 09e343c946..3af0f5150c 100644 --- a/refinery_rules.md +++ b/refinery_rules.md @@ -683,11 +683,21 @@ If no root span exists when the trace is sent, root writes go to the first non-a The name of the field that will be added to each target span. Must not be empty. +When `ScopeConditions` is set, this is the field written to each anchor span; when unset (the original behavior), this is the field written to the root span. Keys in the `meta.refinery.` namespace are reserved for Refinery's own metadata and are rejected at validation. Keys starting with `meta.` produce a warning, because int fields with a value of `0` cannot be distinguished from a missing field on the wire — meaning zero-count anchors will appear absent to downstream queries. - Type: `string` +### `RootKey` + +When set together with `ScopeConditions`, the trace-wide total on the root span is written under `RootKey` instead of `Key`. +This lets per-anchor counts and the trace-wide total land on different attribute names so they can be queried independently. +Ignored (with a validation warning) when `ScopeConditions` is empty — unscoped counters always write `Key` to the root. +Subject to the same reserved-namespace rules as `Key` and counts as a separate field for cross-counter uniqueness checks. + +- Type: `string` + ### `Conditions` All conditions must match for a span to be counted. diff --git a/rules.md b/rules.md index ec3fcba714..a3582b01b4 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-05-27 at 17:33:09 UTC. +It was automatically generated on 2026-06-01 at 18:46:18 UTC. ## The Rules file @@ -731,11 +731,21 @@ If no root span exists when the trace is sent, root writes go to the first non-a The name of the field that will be added to each target span. Must not be empty. +When `ScopeConditions` is set, this is the field written to each anchor span; when unset (the original behavior), this is the field written to the root span. Keys in the `meta.refinery.` namespace are reserved for Refinery's own metadata and are rejected at validation. Keys starting with `meta.` produce a warning, because int fields with a value of `0` cannot be distinguished from a missing field on the wire — meaning zero-count anchors will appear absent to downstream queries. Type: `string` +### `RootKey` + +When set together with `ScopeConditions`, the trace-wide total on the root span is written under `RootKey` instead of `Key`. +This lets per-anchor counts and the trace-wide total land on different attribute names so they can be queried independently. +Ignored (with a validation warning) when `ScopeConditions` is empty — unscoped counters always write `Key` to the root. +Subject to the same reserved-namespace rules as `Key` and counts as a separate field for cross-counter uniqueness checks. + +Type: `string` + ### `Conditions` All conditions must match for a span to be counted. diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 0e82059873..93169f6c9d 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-05-27 at 17:33:22 UTC. +# Automatically generated on 2026-06-01 at 18:46:19 UTC. General: - ConfigurationVersion diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index 4815bab726..cd2ef8e80c 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-05-27 at 17:33:21 UTC +# automatically generated on 2026-06-01 at 18:46:19 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index 754864fbb8..d5f32bf04d 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-05-27 at 17:33:21 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-06-01 at 18:46:19 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of From ce050be8996728d08dbeadd4cf48c83d0db1f004 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 1 Jun 2026 12:01:32 -0700 Subject: [PATCH 34/35] feat(SpanCounter): drop EmitTotalOnRoot; use RootKey to opt into root total Setting RootKey alongside ScopeConditions now opts the root span into receiving the trace-wide total under RootKey. An empty RootKey on a scoped counter means no root write happens. This collapses the previous two-field design (EmitTotalOnRoot + RootKey) into a single field whose presence carries the opt-in meaning. Unscoped counters are unchanged: they continue to write Key to the root (today's behavior). RootKey set on an unscoped counter is still ignored with a validation warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- collect/collect_test.go | 54 ++++++++++---------- config.md | 2 +- config/metadata/rulesMeta.yaml | 51 ++++++++----------- config/span_counter_config.go | 29 ++++++----- config/span_counter_config_test.go | 72 ++++----------------------- config/validate.go | 9 ---- refinery_rules.md | 17 ++----- rules.md | 19 +++---- tools/convert/configDataNames.txt | 2 +- tools/convert/minimal_config.yaml | 2 +- tools/convert/templates/configV2.tmpl | 2 +- 11 files changed, 87 insertions(+), 172 deletions(-) diff --git a/collect/collect_test.go b/collect/collect_test.go index 3f3061d264..42ff43c636 100644 --- a/collect/collect_test.go +++ b/collect/collect_test.go @@ -2215,7 +2215,6 @@ func findEventBySpanID(events []*types.Event, id string) *types.Event { // ├── r4 ── db4a, db4b // └── r5 ── db5a, db5b func TestCustomSpanCounts_Scoped_MultipleAnchors(t *testing.T) { - emitFalse := false counters := []config.SpanCounter{{ Key: "db_call_count", Conditions: []*config.RulesBasedSamplerCondition{ @@ -2224,7 +2223,6 @@ func TestCustomSpanCounts_Scoped_MultipleAnchors(t *testing.T) { ScopeConditions: []*config.RulesBasedSamplerCondition{ {Field: "graphql.operation.name", Operator: config.Exists}, }, - EmitTotalOnRoot: &emitFalse, }} coll := newTestCollector(t, customCountConf(counters)) transmission := coll.Transmission.(*transmit.MockTransmission) @@ -2259,7 +2257,7 @@ func TestCustomSpanCounts_Scoped_MultipleAnchors(t *testing.T) { root := findEventBySpanID(events, "s0") require.NotNil(t, root) - assert.Nil(t, root.Data.Get("db_call_count"), "EmitTotalOnRoot=false → no root write") + assert.Nil(t, root.Data.Get("db_call_count"), "no RootKey set → no root write") for r := 1; r <= 5; r++ { for d := 0; d < 2; d++ { ev := findEventBySpanID(events, fmt.Sprintf("db%d%d", r, d)) @@ -2269,20 +2267,19 @@ func TestCustomSpanCounts_Scoped_MultipleAnchors(t *testing.T) { } } -// TestCustomSpanCounts_Scoped_EmitTotalOnRoot verifies that when -// EmitTotalOnRoot=true, the root also receives the trace-wide total along -// with per-anchor counts. -func TestCustomSpanCounts_Scoped_EmitTotalOnRoot(t *testing.T) { - emitTrue := true +// TestCustomSpanCounts_Scoped_RootTotalViaRootKey verifies that setting +// RootKey alongside ScopeConditions causes the root to receive the +// trace-wide total under RootKey, while anchors still get Key. +func TestCustomSpanCounts_Scoped_RootTotalViaRootKey(t *testing.T) { counters := []config.SpanCounter{{ - Key: "db_call_count", + Key: "db_call_count", + RootKey: "db_call_total", Conditions: []*config.RulesBasedSamplerCondition{ {Field: "name", Operator: config.EQ, Value: "db.query"}, }, ScopeConditions: []*config.RulesBasedSamplerCondition{ {Field: "graphql.operation.name", Operator: config.Exists}, }, - EmitTotalOnRoot: &emitTrue, }} coll := newTestCollector(t, customCountConf(counters)) transmission := coll.Transmission.(*transmit.MockTransmission) @@ -2312,10 +2309,12 @@ func TestCustomSpanCounts_Scoped_EmitTotalOnRoot(t *testing.T) { ev := findEventBySpanID(events, fmt.Sprintf("r%d", r)) require.NotNil(t, ev) assert.Equal(t, int64(4), ev.Data.Get("db_call_count")) + assert.Nil(t, ev.Data.Get("db_call_total")) } root := findEventBySpanID(events, "s0") require.NotNil(t, root) - assert.Equal(t, int64(12), root.Data.Get("db_call_count"), "root should get trace-wide total") + assert.Equal(t, int64(12), root.Data.Get("db_call_total"), "root should get trace-wide total under RootKey") + assert.Nil(t, root.Data.Get("db_call_count"), "root does not receive Key when RootKey is set") } // TestCustomSpanCounts_Scoped_RootKey verifies that when both @@ -2323,7 +2322,6 @@ func TestCustomSpanCounts_Scoped_EmitTotalOnRoot(t *testing.T) { // the root's trace-wide total uses RootKey — landing on a separate field // so the two counts can be queried independently. func TestCustomSpanCounts_Scoped_RootKey(t *testing.T) { - emitTrue := true counters := []config.SpanCounter{{ Key: "resolver_db_count", RootKey: "trace_db_count", @@ -2333,7 +2331,6 @@ func TestCustomSpanCounts_Scoped_RootKey(t *testing.T) { ScopeConditions: []*config.RulesBasedSamplerCondition{ {Field: "graphql.operation.name", Operator: config.Exists}, }, - EmitTotalOnRoot: &emitTrue, }} coll := newTestCollector(t, customCountConf(counters)) transmission := coll.Transmission.(*transmit.MockTransmission) @@ -2501,19 +2498,18 @@ func TestCustomSpanCounts_Scoped_AnchorMatchesRoot(t *testing.T) { } // TestCustomSpanCounts_Scoped_AnchorMatchesNothing verifies that when no span -// matches ScopeConditions, no anchor writes occur; with EmitTotalOnRoot=true -// the root still receives the trace-wide total. +// matches ScopeConditions, no anchor writes occur. RootKey is set so the root +// still receives the trace-wide total. func TestCustomSpanCounts_Scoped_AnchorMatchesNothing(t *testing.T) { - emitTrue := true counters := []config.SpanCounter{{ - Key: "errs", + Key: "errs", + RootKey: "err_total", Conditions: []*config.RulesBasedSamplerCondition{ {Field: "error", Operator: config.EQ, Value: true}, }, ScopeConditions: []*config.RulesBasedSamplerCondition{ {Field: "no-such-anchor", Operator: config.Exists}, }, - EmitTotalOnRoot: &emitTrue, }} coll := newTestCollector(t, customCountConf(counters)) transmission := coll.Transmission.(*transmit.MockTransmission) @@ -2536,7 +2532,8 @@ func TestCustomSpanCounts_Scoped_AnchorMatchesNothing(t *testing.T) { root := findEventBySpanID(events, "s0") require.NotNil(t, root) - assert.Equal(t, int64(2), root.Data.Get("errs"), "EmitTotalOnRoot=true → root has trace-wide total") + assert.Equal(t, int64(2), root.Data.Get("err_total"), "RootKey set → root has trace-wide total") + assert.Nil(t, root.Data.Get("errs"), "anchor Key not written to root") } // TestCustomSpanCounts_Scoped_AnchorMatchesEverySpan verifies a permissive @@ -2572,19 +2569,18 @@ func TestCustomSpanCounts_Scoped_AnchorMatchesEverySpan(t *testing.T) { // TestCustomSpanCounts_Scoped_MultiForestEmitTotal verifies that a trace with // two forest roots (a missing intermediate span — e.g., a load balancer not -// in Refinery's view) produces a correct trace-wide total when -// EmitTotalOnRoot=true. The total sums each forest's subtree counts. +// in Refinery's view) produces a correct trace-wide total when RootKey is +// set. The total sums each forest's subtree counts. func TestCustomSpanCounts_Scoped_MultiForestEmitTotal(t *testing.T) { - emitTrue := true counters := []config.SpanCounter{{ - Key: "db_call_count", + Key: "db_call_count", + RootKey: "db_call_total", Conditions: []*config.RulesBasedSamplerCondition{ {Field: "name", Operator: config.EQ, Value: "db.query"}, }, ScopeConditions: []*config.RulesBasedSamplerCondition{ {Field: "graphql.operation.name", Operator: config.Exists}, }, - EmitTotalOnRoot: &emitTrue, }} coll := newTestCollector(t, customCountConf(counters)) transmission := coll.Transmission.(*transmit.MockTransmission) @@ -2624,13 +2620,13 @@ func TestCustomSpanCounts_Scoped_MultiForestEmitTotal(t *testing.T) { assert.Equal(t, int64(1), findEventBySpanID(events, "a1").Data.Get("db_call_count")) assert.Equal(t, int64(2), findEventBySpanID(events, "b1").Data.Get("db_call_count")) - assert.Equal(t, int64(3), findEventBySpanID(events, "s0").Data.Get("db_call_count"), + assert.Equal(t, int64(3), findEventBySpanID(events, "s0").Data.Get("db_call_total"), "trace-wide total must sum across both forest roots") } -// TestCustomSpanCounts_Scoped_MultiForestNoTotal verifies that with -// EmitTotalOnRoot=false, anchors in disjoint forests still get correct -// per-anchor counts and no root write happens. +// TestCustomSpanCounts_Scoped_MultiForestNoTotal verifies that with no +// RootKey set, anchors in disjoint forests still get correct per-anchor +// counts and no root write happens. func TestCustomSpanCounts_Scoped_MultiForestNoTotal(t *testing.T) { counters := []config.SpanCounter{{ Key: "db_call_count", @@ -2673,7 +2669,7 @@ func TestCustomSpanCounts_Scoped_MultiForestNoTotal(t *testing.T) { assert.Equal(t, int64(1), findEventBySpanID(events, "a1").Data.Get("db_call_count")) assert.Equal(t, int64(1), findEventBySpanID(events, "b1").Data.Get("db_call_count")) assert.Nil(t, findEventBySpanID(events, "s0").Data.Get("db_call_count"), - "EmitTotalOnRoot defaults to false when scope set") + "no RootKey set → no root write") } // TestCustomSpanCounts_Scoped_TwoCycleDefense verifies that a parent-ID cycle diff --git a/config.md b/config.md index d1ffda314a..57796cfc62 100644 --- a/config.md +++ b/config.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Configuration Documentation This is the documentation for the configuration file for Honeycomb's Refinery. -It was automatically generated on 2026-06-01 at 18:47:40 UTC. +It was automatically generated on 2026-06-01 at 19:01:21 UTC. ## The Config file diff --git a/config/metadata/rulesMeta.yaml b/config/metadata/rulesMeta.yaml index 9aa79f067d..0d10217bad 100644 --- a/config/metadata/rulesMeta.yaml +++ b/config/metadata/rulesMeta.yaml @@ -748,10 +748,10 @@ groups: that must all match for a span to be counted. By default the trace-wide count is written to the root span under Key. When ScopeConditions is set, every span matching ScopeConditions instead receives the count of - matching descendant spans in its own subtree, and EmitTotalOnRoot - controls whether the trace-wide total is additionally written to the - root. If no root span exists when the trace is sent, root writes go to - the first non-annotation span instead. + matching descendant spans in its own subtree; setting RootKey + alongside additionally writes the trace-wide total to the root span + under RootKey. If no root span exists when the trace is sent, root + writes go to the first non-annotation span instead. fields: - name: Key type: string @@ -771,16 +771,18 @@ groups: - name: RootKey type: string - summary: optional override for the field name written to the root span. - description: > - When set together with `ScopeConditions`, the trace-wide total on - the root span is written under `RootKey` instead of `Key`. This - lets per-anchor counts and the trace-wide total land on different - attribute names so they can be queried independently. Ignored - (with a validation warning) when `ScopeConditions` is empty — - unscoped counters always write `Key` to the root. Subject to the - same reserved-namespace rules as `Key` and counts as a separate - field for cross-counter uniqueness checks. + summary: optional field name written to the root span with the trace-wide total. + description: > + Only meaningful when `ScopeConditions` is set. Setting `RootKey` + opts the root span into receiving the trace-wide total, written + under this field name (which is typically different from `Key` + so per-anchor counts and the trace-wide total can be queried + independently). If `RootKey` is left empty on a scoped counter + the root receives no write. Ignored (with a validation warning) + when `ScopeConditions` is empty — unscoped counters always write + `Key` to the root. Subject to the same reserved-namespace rules + as `Key` and counts as a separate field for cross-counter + uniqueness checks. - name: Conditions type: objectarray @@ -800,19 +802,10 @@ groups: "anchor" and receives the count of matching descendant spans in its own subtree (including the anchor span itself when it matches `Conditions`). When omitted, the counter writes a single trace-wide - total to the root span — the original SpanCounter behavior. Nested - anchors are not special-cased: an outer anchor's count includes - the descendant subtree even if it crosses an inner anchor. Uses - the same condition format as rules-based sampler conditions; the + total to the root span — the original SpanCounter behavior. Set + `RootKey` alongside `ScopeConditions` to additionally emit the + trace-wide total on the root. Nested anchors are not + special-cased: an outer anchor's count includes the descendant + subtree even if it crosses an inner anchor. Uses the same + condition format as rules-based sampler conditions; the trace-level `has-root-span` operator is rejected at validation. - - - name: EmitTotalOnRoot - type: bool - summary: controls whether the trace-wide total is also written to the root span. - description: > - When ScopeConditions is empty this defaults to `true` (today's - behavior — the trace-wide total is written to the root). When - ScopeConditions is non-empty this defaults to `false` (only the - per-anchor counts are written). Setting it explicitly overrides - the default. Setting `false` with no ScopeConditions disables all - writes for the counter and produces a validation warning. diff --git a/config/span_counter_config.go b/config/span_counter_config.go index 53919a2dce..f2a5f6ff23 100644 --- a/config/span_counter_config.go +++ b/config/span_counter_config.go @@ -11,19 +11,19 @@ type SpanData interface { // SpanCounter defines a custom span count to be computed and emitted. // -// By default (no ScopeConditions), Spans are counted if they satisfy all +// By default (no ScopeConditions), spans are counted if they satisfy all // Conditions, and the trace-wide total is written to the root span under Key. // // When ScopeConditions is set, the counter is computed per-anchor: every span // matching ScopeConditions receives the count of matching descendant spans in -// its own subtree (including itself if it matches Conditions). EmitTotalOnRoot -// controls whether the trace-wide total is additionally written to the root. +// its own subtree (including itself if it matches Conditions). Setting +// RootKey alongside ScopeConditions additionally writes the trace-wide total +// to the root span under RootKey. type SpanCounter struct { Key string `yaml:"Key"` RootKey string `yaml:"RootKey,omitempty"` Conditions []*RulesBasedSamplerCondition `yaml:"Conditions,omitempty"` ScopeConditions []*RulesBasedSamplerCondition `yaml:"ScopeConditions,omitempty"` - EmitTotalOnRoot *bool `yaml:"EmitTotalOnRoot,omitempty"` } // Init initializes all conditions. Must be called before MatchesSpan. @@ -59,23 +59,22 @@ func (c *SpanCounter) MatchesScope(span SpanData, root SpanData) bool { } // ShouldEmitTotalOnRoot reports whether the trace-wide total should be -// written to the root span. Defaults to true when ScopeConditions is empty -// (today's behavior) and false when ScopeConditions is set, unless an -// explicit EmitTotalOnRoot value overrides. +// written to the root span. Unscoped counters always do (today's behavior: +// the only output is a root total under Key). Scoped counters do only when +// RootKey is explicitly set — opting in by naming the root's field. func (c *SpanCounter) ShouldEmitTotalOnRoot() bool { - if c.EmitTotalOnRoot != nil { - return *c.EmitTotalOnRoot + if len(c.ScopeConditions) == 0 { + return true } - return len(c.ScopeConditions) == 0 + return c.RootKey != "" } // EffectiveRootKey returns the field name to use when writing the trace-wide -// total to the root span. When ScopeConditions is set and RootKey is -// non-empty, RootKey is used so the per-anchor and per-trace counts land on -// separate field names. Otherwise (unscoped, or scoped with no RootKey -// override) the root write uses Key, preserving today's behavior. +// total to the root span. When ScopeConditions is set, the root write uses +// RootKey (which is also what opts the root into receiving a write at all); +// otherwise (unscoped) it uses Key, preserving today's behavior. func (c *SpanCounter) EffectiveRootKey() string { - if len(c.ScopeConditions) > 0 && c.RootKey != "" { + if len(c.ScopeConditions) > 0 { return c.RootKey } return c.Key diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go index 1d2e79186e..2cf110ca40 100644 --- a/config/span_counter_config_test.go +++ b/config/span_counter_config_test.go @@ -338,48 +338,20 @@ func TestMatchesScope_RootPrefixSupported(t *testing.T) { assert.False(t, counter.MatchesScope(spanData{}, nil)) } -func TestShouldEmitTotalOnRoot_Defaults(t *testing.T) { - // No ScopeConditions, no override → true (today's behavior). - unscoped := SpanCounter{Key: "k"} - assert.True(t, unscoped.ShouldEmitTotalOnRoot()) +func TestShouldEmitTotalOnRoot(t *testing.T) { + // Unscoped → always emit on root (today's behavior). + assert.True(t, (&SpanCounter{Key: "k"}).ShouldEmitTotalOnRoot()) - // ScopeConditions set, no override → false (per-anchor-only). + // Scoped, no RootKey → per-anchor only, no root write. scoped := SpanCounter{ - Key: "k", - ScopeConditions: []*RulesBasedSamplerCondition{ - cond("anchor", Exists, nil), - }, - } - assert.False(t, scoped.ShouldEmitTotalOnRoot()) -} - -func TestShouldEmitTotalOnRoot_ExplicitOverride(t *testing.T) { - tr := true - fa := false - - // Override true with no scope. - c := SpanCounter{Key: "k", EmitTotalOnRoot: &tr} - assert.True(t, c.ShouldEmitTotalOnRoot()) - - // Override false with no scope (no-op). - c = SpanCounter{Key: "k", EmitTotalOnRoot: &fa} - assert.False(t, c.ShouldEmitTotalOnRoot()) - - // Override true with scope. - c = SpanCounter{ Key: "k", - EmitTotalOnRoot: &tr, ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, } - assert.True(t, c.ShouldEmitTotalOnRoot()) + assert.False(t, scoped.ShouldEmitTotalOnRoot()) - // Override false with scope (matches default). - c = SpanCounter{ - Key: "k", - EmitTotalOnRoot: &fa, - ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, - } - assert.False(t, c.ShouldEmitTotalOnRoot()) + // Scoped + RootKey set → emit total on root. + scoped.RootKey = "rk" + assert.True(t, scoped.ShouldEmitTotalOnRoot()) } func TestEffectiveRootKey(t *testing.T) { @@ -391,12 +363,13 @@ func TestEffectiveRootKey(t *testing.T) { c = SpanCounter{Key: "k", RootKey: "rk"} assert.Equal(t, "k", c.EffectiveRootKey()) - // Scoped + no RootKey → Key (per-anchor and root share the same name). + // Scoped + no RootKey → empty string; ShouldEmitTotalOnRoot is false so + // nothing is written to the root and this value isn't consulted. c = SpanCounter{ Key: "k", ScopeConditions: []*RulesBasedSamplerCondition{cond("anchor", Exists, nil)}, } - assert.Equal(t, "k", c.EffectiveRootKey()) + assert.Equal(t, "", c.EffectiveRootKey()) // Scoped + RootKey set → RootKey overrides the root write. c = SpanCounter{ @@ -488,29 +461,6 @@ func TestValidateSpanCounterEntry_MetaNamespaceWarning(t *testing.T) { assert.Contains(t, results[0].Message, "meta.") } -func TestValidateSpanCounterEntry_NoopWarning(t *testing.T) { - seen := map[string]int{} - // EmitTotalOnRoot=false with no ScopeConditions → warning. - results := validateSpanCounterEntry(0, map[string]any{ - "Key": "k", - "EmitTotalOnRoot": false, - }, seen) - require.Len(t, results, 1) - assert.Equal(t, Warning, results[0].Severity) - assert.Contains(t, results[0].Message, "disables all writes") - - // EmitTotalOnRoot=false with ScopeConditions present → no warning (per-anchor still writes). - seen = map[string]int{} - results = validateSpanCounterEntry(0, map[string]any{ - "Key": "k2", - "EmitTotalOnRoot": false, - "ScopeConditions": []any{ - map[string]any{"Field": "x", "Operator": "exists"}, - }, - }, seen) - assert.Empty(t, results) -} - func TestValidateSpanCounterEntry_HasRootSpanInScope(t *testing.T) { seen := map[string]int{} results := validateSpanCounterEntry(0, map[string]any{ diff --git a/config/validate.go b/config/validate.go index 7ec49ea280..b92d6ef42c 100644 --- a/config/validate.go +++ b/config/validate.go @@ -754,15 +754,6 @@ func validateSpanCounterEntry(idx int, entry map[string]any, seenKeys map[string }) } - if v, ok := entry["EmitTotalOnRoot"]; ok { - if emit, ok := v.(bool); ok && !emit && scopeIsEmpty { - results = append(results, ValidationResult{ - Message: fmt.Sprintf("SpanCounters[%d]: EmitTotalOnRoot=false with no ScopeConditions disables all writes for this counter", idx), - Severity: Warning, - }) - } - } - for ci, cond := range scopeArr { condMap, ok := cond.(map[string]any) if !ok { diff --git a/refinery_rules.md b/refinery_rules.md index 3af0f5150c..7cca7b1aa9 100644 --- a/refinery_rules.md +++ b/refinery_rules.md @@ -676,7 +676,7 @@ If your traces are consistent lengths and changes in trace length is a useful in Defines a single custom span counter. Each counter has a Key that names the field written to a target span and an optional list of Conditions that must all match for a span to be counted. By default the trace-wide count is written to the root span under Key. -When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree, and EmitTotalOnRoot controls whether the trace-wide total is additionally written to the root. +When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree; setting RootKey alongside additionally writes the trace-wide total to the root span under RootKey. If no root span exists when the trace is sent, root writes go to the first non-annotation span instead. ### `Key` @@ -691,8 +691,9 @@ Keys starting with `meta.` produce a warning, because int fields with a value of ### `RootKey` -When set together with `ScopeConditions`, the trace-wide total on the root span is written under `RootKey` instead of `Key`. -This lets per-anchor counts and the trace-wide total land on different attribute names so they can be queried independently. +Only meaningful when `ScopeConditions` is set. +Setting `RootKey` opts the root span into receiving the trace-wide total, written under this field name (which is typically different from `Key` so per-anchor counts and the trace-wide total can be queried independently). +If `RootKey` is left empty on a scoped counter the root receives no write. Ignored (with a validation warning) when `ScopeConditions` is empty — unscoped counters always write `Key` to the root. Subject to the same reserved-namespace rules as `Key` and counts as a separate field for cross-counter uniqueness checks. @@ -711,17 +712,9 @@ An anchor span (one matching `ScopeConditions`) is also tested against Condition When set, each span satisfying all of these conditions becomes an "anchor" and receives the count of matching descendant spans in its own subtree (including the anchor span itself when it matches `Conditions`). When omitted, the counter writes a single trace-wide total to the root span — the original SpanCounter behavior. +Set `RootKey` alongside `ScopeConditions` to additionally emit the trace-wide total on the root. Nested anchors are not special-cased: an outer anchor's count includes the descendant subtree even if it crosses an inner anchor. Uses the same condition format as rules-based sampler conditions; the trace-level `has-root-span` operator is rejected at validation. - Type: `objectarray` -### `EmitTotalOnRoot` - -When ScopeConditions is empty this defaults to `true` (today's behavior — the trace-wide total is written to the root). -When ScopeConditions is non-empty this defaults to `false` (only the per-anchor counts are written). -Setting it explicitly overrides the default. -Setting `false` with no ScopeConditions disables all writes for the counter and produces a validation warning. - -- Type: `bool` - diff --git a/rules.md b/rules.md index a3582b01b4..dcf40e3b46 100644 --- a/rules.md +++ b/rules.md @@ -3,7 +3,7 @@ # Honeycomb Refinery Rules Documentation This is the documentation for the rules configuration for Honeycomb's Refinery. -It was automatically generated on 2026-06-01 at 18:46:18 UTC. +It was automatically generated on 2026-06-01 at 19:01:21 UTC. ## The Rules file @@ -724,7 +724,7 @@ Type: `bool` Defines a single custom span counter. Each counter has a Key that names the field written to a target span and an optional list of Conditions that must all match for a span to be counted. By default the trace-wide count is written to the root span under Key. -When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree, and EmitTotalOnRoot controls whether the trace-wide total is additionally written to the root. +When ScopeConditions is set, every span matching ScopeConditions instead receives the count of matching descendant spans in its own subtree; setting RootKey alongside additionally writes the trace-wide total to the root span under RootKey. If no root span exists when the trace is sent, root writes go to the first non-annotation span instead. ### `Key` @@ -739,8 +739,9 @@ Type: `string` ### `RootKey` -When set together with `ScopeConditions`, the trace-wide total on the root span is written under `RootKey` instead of `Key`. -This lets per-anchor counts and the trace-wide total land on different attribute names so they can be queried independently. +Only meaningful when `ScopeConditions` is set. +Setting `RootKey` opts the root span into receiving the trace-wide total, written under this field name (which is typically different from `Key` so per-anchor counts and the trace-wide total can be queried independently). +If `RootKey` is left empty on a scoped counter the root receives no write. Ignored (with a validation warning) when `ScopeConditions` is empty — unscoped counters always write `Key` to the root. Subject to the same reserved-namespace rules as `Key` and counts as a separate field for cross-counter uniqueness checks. @@ -759,17 +760,9 @@ Type: `objectarray` When set, each span satisfying all of these conditions becomes an "anchor" and receives the count of matching descendant spans in its own subtree (including the anchor span itself when it matches `Conditions`). When omitted, the counter writes a single trace-wide total to the root span — the original SpanCounter behavior. +Set `RootKey` alongside `ScopeConditions` to additionally emit the trace-wide total on the root. Nested anchors are not special-cased: an outer anchor's count includes the descendant subtree even if it crosses an inner anchor. Uses the same condition format as rules-based sampler conditions; the trace-level `has-root-span` operator is rejected at validation. Type: `objectarray` -### `EmitTotalOnRoot` - -When ScopeConditions is empty this defaults to `true` (today's behavior — the trace-wide total is written to the root). -When ScopeConditions is non-empty this defaults to `false` (only the per-anchor counts are written). -Setting it explicitly overrides the default. -Setting `false` with no ScopeConditions disables all writes for the counter and produces a validation warning. - -Type: `bool` - diff --git a/tools/convert/configDataNames.txt b/tools/convert/configDataNames.txt index 93169f6c9d..56b980702d 100644 --- a/tools/convert/configDataNames.txt +++ b/tools/convert/configDataNames.txt @@ -1,5 +1,5 @@ # Names of groups and fields in the new config file format. -# Automatically generated on 2026-06-01 at 18:46:19 UTC. +# Automatically generated on 2026-06-01 at 19:01:21 UTC. General: - ConfigurationVersion diff --git a/tools/convert/minimal_config.yaml b/tools/convert/minimal_config.yaml index cd2ef8e80c..6e43f8df48 100644 --- a/tools/convert/minimal_config.yaml +++ b/tools/convert/minimal_config.yaml @@ -1,5 +1,5 @@ # sample uncommented config file containing all possible fields -# automatically generated on 2026-06-01 at 18:46:19 UTC +# automatically generated on 2026-06-01 at 19:01:21 UTC General: ConfigurationVersion: 2 MinRefineryVersion: "v2.0" diff --git a/tools/convert/templates/configV2.tmpl b/tools/convert/templates/configV2.tmpl index d5f32bf04d..b5d937fe5f 100644 --- a/tools/convert/templates/configV2.tmpl +++ b/tools/convert/templates/configV2.tmpl @@ -2,7 +2,7 @@ ## Honeycomb Refinery Configuration ## ###################################### # -# created {{ now }} from {{ .Input }} using a template generated on 2026-06-01 at 18:46:19 UTC +# created {{ now }} from {{ .Input }} using a template generated on 2026-06-01 at 19:01:21 UTC # This file contains a configuration for the Honeycomb Refinery. It is in YAML # format, organized into named groups, each of which contains a set of From 0c68b6ca2392952ea2637a7a44b8d43f655e6f69 Mon Sep 17 00:00:00 2001 From: Zaq? Question Date: Mon, 1 Jun 2026 13:19:17 -0700 Subject: [PATCH 35/35] fix(SpanCounter): route ScopeConditions through Conditions group at validate The metadata-driven validator splits a field's qualified name and looks up the trailing segment as a group; for SpanCounters.ScopeConditions that resolved to a non-existent "ScopeConditions" group, producing "unknown group" and "unknown field" errors for every entry in real rules files. Validate ScopeConditions entries explicitly against the existing "Conditions" group instead, and strip the field from the entry map before the recursive metadata Validate call so the broken walk never happens. Added a regression test that runs a representative rules document through ValidateRules end to end and confirms a bogus Operator inside ScopeConditions still fails the choice validation. Co-Authored-By: Claude Opus 4.7 (1M context) --- config/span_counter_config_test.go | 58 ++++++++++++++++++++++++++++++ config/validate.go | 40 +++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/config/span_counter_config_test.go b/config/span_counter_config_test.go index 2cf110ca40..cde4f91e37 100644 --- a/config/span_counter_config_test.go +++ b/config/span_counter_config_test.go @@ -461,6 +461,64 @@ func TestValidateSpanCounterEntry_MetaNamespaceWarning(t *testing.T) { assert.Contains(t, results[0].Message, "meta.") } +// TestValidateRules_ScopeConditionsThroughMetadata exercises the full +// ValidateRules path with a realistic rules document that uses +// ScopeConditions. This is a regression test for a bug where the +// metadata-driven walker tried to resolve ScopeConditions against a +// non-existent "ScopeConditions" group and produced "unknown group" / +// "unknown field" errors for every condition entry. +func TestValidateRules_ScopeConditionsThroughMetadata(t *testing.T) { + m, err := LoadRulesMetadata() + require.NoError(t, err) + + rules := map[string]any{ + "RulesVersion": 2, + "Samplers": map[string]any{ + "__default__": map[string]any{ + "DeterministicSampler": map[string]any{ + "SampleRate": 1, + }, + }, + }, + "SpanCounters": []any{ + map[string]any{ + "Key": "graphql.db_call_count", + "RootKey": "trace.db_call_total", + "ScopeConditions": []any{ + map[string]any{"Field": "graphql.field", "Operator": "exists"}, + }, + "Conditions": []any{ + map[string]any{"Field": "db.system", "Operator": "=", "Value": "postgresql", "Datatype": "string"}, + }, + }, + }, + } + + results := m.ValidateRules(rules) + for _, r := range results { + assert.NotContains(t, r.Message, "unknown group ScopeConditions", + "metadata walker should not look up ScopeConditions as a group") + assert.NotContains(t, r.Message, "unknown field ScopeConditions.", + "metadata walker should not try to validate ScopeConditions.* directly") + } + + // Same shape but with a bogus operator inside ScopeConditions — the + // metadata-driven "choice" validation on Operator should still catch + // this even though we routed ScopeConditions entries through the + // "Conditions" group manually. + rules["SpanCounters"].([]any)[0].(map[string]any)["ScopeConditions"] = []any{ + map[string]any{"Field": "graphql.field", "Operator": "nonsense-op"}, + } + results = m.ValidateRules(rules) + var sawBadOp bool + for _, r := range results { + if r.Severity == Error && strings.Contains(r.Message, "nonsense-op") { + sawBadOp = true + } + } + assert.True(t, sawBadOp, "bogus Operator inside ScopeConditions must still fail metadata validation") +} + func TestValidateSpanCounterEntry_HasRootSpanInScope(t *testing.T) { seen := map[string]int{} results := validateSpanCounterEntry(0, map[string]any{ diff --git a/config/validate.go b/config/validate.go index b92d6ef42c..d64917308b 100644 --- a/config/validate.go +++ b/config/validate.go @@ -663,6 +663,42 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { seenKeys := make(map[string]int, len(arr)) for i, entry := range arr { if entryMap, ok := entry.(map[string]any); ok { + // ScopeConditions reuses the structure of Conditions but + // the metadata-driven walker would look up a group named + // "ScopeConditions" (which doesn't exist). Validate + // ScopeConditions entries directly against the + // "Conditions" group and remove the key before the + // recursive Validate call. + scopeKey := "ScopeConditions" + scope, hasScope := entryMap[scopeKey] + if hasScope { + delete(entryMap, scopeKey) + if scopeArr, ok := scope.([]any); ok { + for ci, cond := range scopeArr { + condMap, ok := cond.(map[string]any) + if !ok { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d].ScopeConditions[%d] must be an object, but %v is %T", i, ci, cond, cond), + Severity: Error, + }) + continue + } + subresults := m.Validate(map[string]any{"Conditions": condMap}) + for _, result := range subresults { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("Within SpanCounters[%d].ScopeConditions[%d]: %s", i, ci, result.Message), + Severity: result.Severity, + }) + } + } + } else { + results = append(results, ValidationResult{ + Message: fmt.Sprintf("SpanCounters[%d].ScopeConditions must be an array, but %v is %T", i, scope, scope), + Severity: Error, + }) + } + } + rulesmap := map[string]any{"SpanCounters": entryMap} subresults := m.Validate(rulesmap) for _, result := range subresults { @@ -671,6 +707,10 @@ func (m *Metadata) ValidateRules(data map[string]any) ValidationResults { Severity: result.Severity, }) } + + if hasScope { + entryMap[scopeKey] = scope + } results = append(results, validateSpanCounterEntry(i, entryMap, seenKeys)...) } else { results = append(results, ValidationResult{