From cc3946f58d29c461a923f2c0348c036c1601946f Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:59:23 -0400 Subject: [PATCH 1/4] Add Beholder default metric filter views plumbing. Introduce metricviews.DefaultViews() with an empty denylist by default, Config.metricViews/metricOptions methods, and CL_TELEMETRY_METRIC_VIEWS_DISABLED wiring through loop env and server. Caller views precede defaults so histogram-bucket overrides are not shadowed. Co-authored-by: Cursor --- pkg/beholder/config.go | 3 + pkg/beholder/config_test.go | 1 + pkg/beholder/meter_provider.go | 29 ++++++++-- pkg/beholder/meter_provider_test.go | 32 +++++++++++ pkg/beholder/metricviews/views.go | 27 +++++++++ pkg/beholder/metricviews/views_test.go | 67 +++++++++++++++++++++++ pkg/beholder/testdata/config-example.json | 1 + pkg/loop/config.go | 8 +++ pkg/loop/config_test.go | 44 +++++++++++++++ pkg/loop/server.go | 1 + 10 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 pkg/beholder/metricviews/views.go create mode 100644 pkg/beholder/metricviews/views_test.go diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index e5454e62e3..5b560a94cb 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -40,6 +40,9 @@ type Config struct { MetricReaderInterval time.Duration MetricRetryConfig *RetryConfig MetricViews []metric.View + // MetricViewsDisabled skips metricviews.DefaultViews() attribute filtering. + // Caller-supplied MetricViews are still applied. Set via CL_TELEMETRY_METRIC_VIEWS_DISABLED. + MetricViewsDisabled bool // MetricCardinalityLimit sets the SDK per-instrument attribute-set limit (0 = disabled). // DefaultConfig uses 100000 as a production safety valve for high-cardinality workloads. MetricCardinalityLimit int diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index 5f998616ca..b2e72cc8b6 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -55,6 +55,7 @@ func TestConfig(t *testing.T) { // Metric MetricReaderInterval: 1 * time.Second, MetricCompressor: "gzip", + MetricViewsDisabled: false, MetricCardinalityLimit: 0, // OTel metric exporter retry config MetricRetryConfig: nil, diff --git a/pkg/beholder/meter_provider.go b/pkg/beholder/meter_provider.go index 4b4eba4c17..abdddce719 100644 --- a/pkg/beholder/meter_provider.go +++ b/pkg/beholder/meter_provider.go @@ -2,15 +2,34 @@ package beholder import ( sdkmetric "go.opentelemetry.io/otel/sdk/metric" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder/metricviews" ) -// metricOptions returns cfg-derived sdkmetric.Option values (caller MetricViews -// and the SDK per-instrument cardinality limit). Callers append reader/resource -// options at the call site. +// metricViews returns the view list passed to sdkmetric.WithView. +// +// Final order: +// 1. cfg.MetricViews — caller overrides (e.g. chainlink metricViews() histogram buckets) +// 2. metricviews.DefaultViews() — attribute filters for high-cardinality labels +// +// Caller views must come first: when multiple views match an instrument and +// resolve to the same output stream (same name/description/unit/kind), the SDK +// keeps only the first in registration order and drops the rest. +func (cfg Config) metricViews() []sdkmetric.View { + if cfg.MetricViewsDisabled { + return cfg.MetricViews + } + return append(cfg.MetricViews, metricviews.DefaultViews()...) +} + +// metricOptions returns cfg-derived sdkmetric.Option values (metric views and the +// SDK per-instrument cardinality limit). Callers append reader/resource options +// at the call site. func (cfg Config) metricOptions() []sdkmetric.Option { + views := cfg.metricViews() var opts []sdkmetric.Option - if len(cfg.MetricViews) > 0 { - opts = append(opts, sdkmetric.WithView(cfg.MetricViews...)) + if len(views) > 0 { + opts = append(opts, sdkmetric.WithView(views...)) } if cfg.MetricCardinalityLimit > 0 { opts = append(opts, sdkmetric.WithCardinalityLimit(cfg.MetricCardinalityLimit)) diff --git a/pkg/beholder/meter_provider_test.go b/pkg/beholder/meter_provider_test.go index fb9fa6f0df..f9e5561c2c 100644 --- a/pkg/beholder/meter_provider_test.go +++ b/pkg/beholder/meter_provider_test.go @@ -10,6 +10,8 @@ import ( "go.opentelemetry.io/otel/metric" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder/metricviews" ) func TestConfig_metricOptions_cardinalityLimit(t *testing.T) { @@ -23,6 +25,7 @@ func TestConfig_metricOptions_cardinalityLimit(t *testing.T) { reader := sdkmetric.NewManualReader() cfg := DefaultConfig() cfg.MetricCardinalityLimit = limit + cfg.MetricViewsDisabled = true mpOpts := append(cfg.metricOptions(), sdkmetric.WithReader(reader)) mp := sdkmetric.NewMeterProvider(mpOpts...) @@ -48,3 +51,32 @@ func TestConfig_metricOptions_cardinalityLimit(t *testing.T) { } assert.Equal(t, int64(uniqueAttributes), total) } + +func TestConfig_metricViews_appendsDefaultsAfterCallerViews(t *testing.T) { + t.Parallel() + + callerView := sdkmetric.NewView( + sdkmetric.Instrument{Name: "custom_metric"}, + sdkmetric.Stream{}, + ) + cfg := DefaultConfig() + cfg.MetricViews = []sdkmetric.View{callerView} + + views := cfg.metricViews() + require.GreaterOrEqual(t, len(views), len(metricviews.DefaultViews())+1) +} + +func TestConfig_metricViews_disabledSkipsDefaults(t *testing.T) { + t.Parallel() + + callerView := sdkmetric.NewView( + sdkmetric.Instrument{Name: "custom_metric"}, + sdkmetric.Stream{}, + ) + cfg := DefaultConfig() + cfg.MetricViews = []sdkmetric.View{callerView} + cfg.MetricViewsDisabled = true + + views := cfg.metricViews() + require.Len(t, views, 1) +} diff --git a/pkg/beholder/metricviews/views.go b/pkg/beholder/metricviews/views.go new file mode 100644 index 0000000000..0007f748e6 --- /dev/null +++ b/pkg/beholder/metricviews/views.go @@ -0,0 +1,27 @@ +// Package metricviews defines Beholder's default OTel metric views for +// cardinality-limiting attribute filters. +// +// Callers (e.g. chainlink core/cmd/shell.go via beholder.Config.MetricViews) +// may supply additional views—typically histogram bucket Aggregation overrides +// for specific instrument names. Beholder merges caller views before these +// defaults (see beholder.Config.metricViews); callers do not need to invoke +// DefaultViews themselves. +// +// An instrument may match multiple views. When several matching views resolve +// to the same output stream (same name/description/unit/kind), the SDK keeps +// only the first in registration order and logs a duplicate-stream warning for +// the rest; attribute filters and aggregations do not compose across them. +// Because Beholder registers caller views ahead of these defaults, a matching +// caller view wins and the default attribute filter for that stream is dropped. +package metricviews + +import ( + sdkmetric "go.opentelemetry.io/otel/sdk/metric" +) + +// DefaultViews returns deny-only attribute-filter views appended after +// caller-supplied MetricViews by beholder.Config.metricViews. The denylist is +// empty by default so no attributes are stripped until keys are configured. +func DefaultViews() []sdkmetric.View { + return nil +} diff --git a/pkg/beholder/metricviews/views_test.go b/pkg/beholder/metricviews/views_test.go new file mode 100644 index 0000000000..dd38891222 --- /dev/null +++ b/pkg/beholder/metricviews/views_test.go @@ -0,0 +1,67 @@ +package metricviews_test + +import ( + "context" + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + + "github.com/smartcontractkit/chainlink-common/pkg/beholder/metricviews" +) + +func TestDefaultViews_emptyByDefault(t *testing.T) { + t.Parallel() + assert.Empty(t, metricviews.DefaultViews()) + + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(reader), + sdkmetric.WithView(metricviews.DefaultViews()...), + ) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + meter := mp.Meter("test") + counter, err := meter.Int64Counter("some_metric_total") + require.NoError(t, err) + + counter.Add(context.Background(), 1, + metric.WithAttributes( + attribute.String("service", "foo"), + attribute.String("event_id", "ev-1"), + attribute.String("workflow_execution_id", "exec-1"), + ), + ) + + var rm metricdata.ResourceMetrics + require.NoError(t, reader.Collect(context.Background(), &rm)) + + keys := attributeKeysFromSum(t, rm) + assert.Contains(t, keys, attribute.Key("service")) + assert.Contains(t, keys, attribute.Key("event_id")) + assert.Contains(t, keys, attribute.Key("workflow_execution_id")) +} + +func attributeKeysFromSum(t *testing.T, rm metricdata.ResourceMetrics) []attribute.Key { + t.Helper() + require.Len(t, rm.ScopeMetrics, 1) + require.Len(t, rm.ScopeMetrics[0].Metrics, 1) + sum, ok := rm.ScopeMetrics[0].Metrics[0].Data.(metricdata.Sum[int64]) + require.True(t, ok) + require.Len(t, sum.DataPoints, 1) + return keysFromSet(sum.DataPoints[0].Attributes) +} + +func keysFromSet(set attribute.Set) []attribute.Key { + keys := make([]attribute.Key, 0, set.Len()) + for _, kv := range set.ToSlice() { + keys = append(keys, kv.Key) + } + slices.Sort(keys) + return keys +} diff --git a/pkg/beholder/testdata/config-example.json b/pkg/beholder/testdata/config-example.json index a3cf77c6c0..db2a37a1d7 100644 --- a/pkg/beholder/testdata/config-example.json +++ b/pkg/beholder/testdata/config-example.json @@ -32,6 +32,7 @@ "MetricReaderInterval": 1000000000, "MetricRetryConfig": null, "MetricViews": null, + "MetricViewsDisabled": false, "MetricCardinalityLimit": 0, "MetricCompressor": "gzip", "MetricProducers": null, diff --git a/pkg/loop/config.go b/pkg/loop/config.go index e3e448e768..e0fcd7118e 100644 --- a/pkg/loop/config.go +++ b/pkg/loop/config.go @@ -83,6 +83,7 @@ const ( envTelemetryTraceCompressor = "CL_TELEMETRY_TRACE_COMPRESSOR" envTelemetryMetricCompressor = "CL_TELEMETRY_METRIC_COMPRESSOR" envTelemetryMetricCardinalityLimit = "CL_TELEMETRY_METRIC_CARDINALITY_LIMIT" + envTelemetryMetricViewsDisabled = "CL_TELEMETRY_METRIC_VIEWS_DISABLED" envTelemetryPrometheusBridgeEnabled = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" envTelemetryPrometheusBridgePrefixes = "CL_TELEMETRY_PROMETHEUS_BRIDGE_PREFIXES" envTelemetryLogCompressor = "CL_TELEMETRY_LOG_COMPRESSOR" @@ -181,6 +182,8 @@ type EnvConfig struct { // distinguish "no opinion" (child applies its own default) from an // explicit 0, which disables the limit. TelemetryMetricCardinalityLimit *int + // TelemetryMetricViewsDisabled skips metricviews.DefaultViews() in the beholder client. + TelemetryMetricViewsDisabled bool TelemetryPrometheusBridgeEnabled bool TelemetryPrometheusBridgePrefixes []string TelemetryLogCompressor string @@ -298,6 +301,7 @@ func (e *EnvConfig) AsCmdEnv() (env []string) { if e.TelemetryMetricCardinalityLimit != nil { add(envTelemetryMetricCardinalityLimit, strconv.Itoa(*e.TelemetryMetricCardinalityLimit)) } + add(envTelemetryMetricViewsDisabled, strconv.FormatBool(e.TelemetryMetricViewsDisabled)) add(envTelemetryPrometheusBridgeEnabled, strconv.FormatBool(e.TelemetryPrometheusBridgeEnabled)) add(envTelemetryPrometheusBridgePrefixes, strings.Join(e.TelemetryPrometheusBridgePrefixes, ",")) add(envTelemetryLogCompressor, e.TelemetryLogCompressor) @@ -551,6 +555,10 @@ func (e *EnvConfig) parse() error { defaultLimit := 100000 e.TelemetryMetricCardinalityLimit = &defaultLimit } + e.TelemetryMetricViewsDisabled, err = getBool(envTelemetryMetricViewsDisabled) + if err != nil { + return fmt.Errorf("failed to parse %s: %w", envTelemetryMetricViewsDisabled, err) + } e.TelemetryPrometheusBridgeEnabled, err = getBool(envTelemetryPrometheusBridgeEnabled) if err != nil { return fmt.Errorf("failed to parse %s: %w", envTelemetryPrometheusBridgeEnabled, err) diff --git a/pkg/loop/config_test.go b/pkg/loop/config_test.go index f263c272d3..830ca416c6 100644 --- a/pkg/loop/config_test.go +++ b/pkg/loop/config_test.go @@ -215,6 +215,7 @@ var envCfgFull = EnvConfig{ TelemetryEmitterMaxQueueSize: 1000, TelemetryLogStreamingEnabled: false, TelemetryMetricCardinalityLimit: new(100000), + TelemetryMetricViewsDisabled: false, TelemetryPrometheusBridgeEnabled: true, TelemetryPrometheusBridgePrefixes: []string{"foo", "bar"}, MeterRecordsEnabled: true, @@ -289,6 +290,7 @@ func TestEnvConfig_AsCmdEnv(t *testing.T) { assert.Equal(t, "1000", got[envTelemetryEmitterMaxQueueSize]) assert.Equal(t, "false", got[envTelemetryLogStreamingEnabled]) assert.Equal(t, "100000", got[envTelemetryMetricCardinalityLimit]) + assert.Equal(t, "false", got[envTelemetryMetricViewsDisabled]) assert.Equal(t, "true", got[envTelemetryPrometheusBridgeEnabled]) assert.Equal(t, "foo,bar", got[envTelemetryPrometheusBridgePrefixes]) assert.Equal(t, "true", got[envMeterRecordsEnabled]) @@ -361,6 +363,48 @@ func TestEnvConfig_MetricCardinalityLimit_RoundTrip(t *testing.T) { }) } +// TestEnvConfig_MetricViewsDisabled_RoundTrip verifies that an explicit +// disable survives an AsCmdEnv -> parse round trip and that leaving the field +// unset keeps default views enabled. +func TestEnvConfig_MetricViewsDisabled_RoundTrip(t *testing.T) { + setEnvFromCmdEnv := func(t *testing.T, env []string) { + t.Helper() + for _, kv := range env { + pair := strings.SplitN(kv, "=", 2) + require.Len(t, pair, 2) + t.Setenv(pair[0], pair[1]) + } + } + + t.Run("explicit disable propagates as true", func(t *testing.T) { + cfg := envCfgFull + cfg.TelemetryMetricViewsDisabled = true + setEnvFromCmdEnv(t, cfg.AsCmdEnv()) + + var parsed EnvConfig + require.NoError(t, parsed.parse()) + assert.True(t, parsed.TelemetryMetricViewsDisabled) + }) + + t.Run("unset keeps default views enabled", func(t *testing.T) { + cfg := envCfgFull + cfg.TelemetryMetricViewsDisabled = false + env := cfg.AsCmdEnv() + for _, kv := range env { + pair := strings.SplitN(kv, "=", 2) + require.Len(t, pair, 2) + if pair[0] == envTelemetryMetricViewsDisabled { + continue + } + t.Setenv(pair[0], pair[1]) + } + + var parsed EnvConfig + require.NoError(t, parsed.parse()) + assert.False(t, parsed.TelemetryMetricViewsDisabled) + }) +} + func TestGetMap(t *testing.T) { t.Setenv("TEST_PREFIX_KEY1", "value1") t.Setenv("TEST_PREFIX_KEY2", "value2") diff --git a/pkg/loop/server.go b/pkg/loop/server.go index b2d495eeaf..4da7a6bde8 100644 --- a/pkg/loop/server.go +++ b/pkg/loop/server.go @@ -192,6 +192,7 @@ func (s *Server) start(opts ...ServerOpt) error { ChipIngressLogger: s.Logger, MetricCompressor: s.EnvConfig.TelemetryMetricCompressor, MetricCardinalityLimit: *s.EnvConfig.TelemetryMetricCardinalityLimit, + MetricViewsDisabled: s.EnvConfig.TelemetryMetricViewsDisabled, } if s.EnvConfig.TelemetryPrometheusBridgeEnabled { From acfed5067ec9731c036a455c8a0322a12bd3bcc4 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:05:12 -0400 Subject: [PATCH 2/4] Add configurable metric views attribute denylist. Expose MetricViewsAttributeBlacklist on beholder.Config and wire CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_BLACKLIST through loop env and server. DefaultViews builds a global deny view when keys are configured. # Conflicts: # pkg/loop/config_test.go --- pkg/beholder/config.go | 3 ++ pkg/beholder/config_test.go | 5 ++- pkg/beholder/meter_provider.go | 2 +- pkg/beholder/meter_provider_test.go | 13 ++++++- pkg/beholder/metricviews/views.go | 21 ++++++++-- pkg/beholder/metricviews/views_test.go | 37 ++++++++++++++++-- pkg/beholder/testdata/config-example.json | 1 + pkg/loop/config.go | 29 ++++++++++++-- pkg/loop/config_test.go | 47 +++++++++++++++++++++-- pkg/loop/server.go | 1 + 10 files changed, 141 insertions(+), 18 deletions(-) diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 5b560a94cb..86efa2f2f7 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -43,6 +43,9 @@ type Config struct { // MetricViewsDisabled skips metricviews.DefaultViews() attribute filtering. // Caller-supplied MetricViews are still applied. Set via CL_TELEMETRY_METRIC_VIEWS_DISABLED. MetricViewsDisabled bool + // MetricViewsAttributeBlacklist lists attribute keys dropped by the default + // global deny view (e.g. event_id). Empty means no keys are stripped. + MetricViewsAttributeBlacklist []string // MetricCardinalityLimit sets the SDK per-instrument attribute-set limit (0 = disabled). // DefaultConfig uses 100000 as a production safety valve for high-cardinality workloads. MetricCardinalityLimit int diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index b2e72cc8b6..e6e5d29fc7 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -55,8 +55,9 @@ func TestConfig(t *testing.T) { // Metric MetricReaderInterval: 1 * time.Second, MetricCompressor: "gzip", - MetricViewsDisabled: false, - MetricCardinalityLimit: 0, + MetricViewsDisabled: false, + MetricViewsAttributeBlacklist: nil, + MetricCardinalityLimit: 0, // OTel metric exporter retry config MetricRetryConfig: nil, // Log diff --git a/pkg/beholder/meter_provider.go b/pkg/beholder/meter_provider.go index abdddce719..e47be568ba 100644 --- a/pkg/beholder/meter_provider.go +++ b/pkg/beholder/meter_provider.go @@ -19,7 +19,7 @@ func (cfg Config) metricViews() []sdkmetric.View { if cfg.MetricViewsDisabled { return cfg.MetricViews } - return append(cfg.MetricViews, metricviews.DefaultViews()...) + return append(cfg.MetricViews, metricviews.DefaultViews(cfg.MetricViewsAttributeBlacklist)...) } // metricOptions returns cfg-derived sdkmetric.Option values (metric views and the diff --git a/pkg/beholder/meter_provider_test.go b/pkg/beholder/meter_provider_test.go index f9e5561c2c..e4d4ae019f 100644 --- a/pkg/beholder/meter_provider_test.go +++ b/pkg/beholder/meter_provider_test.go @@ -63,7 +63,18 @@ func TestConfig_metricViews_appendsDefaultsAfterCallerViews(t *testing.T) { cfg.MetricViews = []sdkmetric.View{callerView} views := cfg.metricViews() - require.GreaterOrEqual(t, len(views), len(metricviews.DefaultViews())+1) + require.GreaterOrEqual(t, len(views), len(metricviews.DefaultViews(nil))+1) +} + +func TestConfig_metricViews_includesBlacklistDefaultView(t *testing.T) { + t.Parallel() + + cfg := DefaultConfig() + cfg.MetricViewsAttributeBlacklist = []string{"event_id"} + + views := cfg.metricViews() + require.Len(t, views, len(metricviews.DefaultViews(cfg.MetricViewsAttributeBlacklist))) + require.NotEmpty(t, views) } func TestConfig_metricViews_disabledSkipsDefaults(t *testing.T) { diff --git a/pkg/beholder/metricviews/views.go b/pkg/beholder/metricviews/views.go index 0007f748e6..b5e1561480 100644 --- a/pkg/beholder/metricviews/views.go +++ b/pkg/beholder/metricviews/views.go @@ -16,12 +16,25 @@ package metricviews import ( + "go.opentelemetry.io/otel/attribute" sdkmetric "go.opentelemetry.io/otel/sdk/metric" ) // DefaultViews returns deny-only attribute-filter views appended after -// caller-supplied MetricViews by beholder.Config.metricViews. The denylist is -// empty by default so no attributes are stripped until keys are configured. -func DefaultViews() []sdkmetric.View { - return nil +// caller-supplied MetricViews by beholder.Config.metricViews. When denyKeys is +// empty, no views are returned and no attributes are stripped. +func DefaultViews(denyKeys []string) []sdkmetric.View { + if len(denyKeys) == 0 { + return nil + } + keys := make([]attribute.Key, len(denyKeys)) + for i, k := range denyKeys { + keys[i] = attribute.Key(k) + } + return []sdkmetric.View{ + sdkmetric.NewView( + sdkmetric.Instrument{Name: "*"}, + sdkmetric.Stream{AttributeFilter: attribute.NewDenyKeysFilter(keys...)}, + ), + } } diff --git a/pkg/beholder/metricviews/views_test.go b/pkg/beholder/metricviews/views_test.go index dd38891222..d88cf146da 100644 --- a/pkg/beholder/metricviews/views_test.go +++ b/pkg/beholder/metricviews/views_test.go @@ -15,14 +15,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder/metricviews" ) -func TestDefaultViews_emptyByDefault(t *testing.T) { +func TestDefaultViews_emptyBlacklist(t *testing.T) { t.Parallel() - assert.Empty(t, metricviews.DefaultViews()) + assert.Nil(t, metricviews.DefaultViews(nil)) reader := sdkmetric.NewManualReader() mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(reader), - sdkmetric.WithView(metricviews.DefaultViews()...), + sdkmetric.WithView(metricviews.DefaultViews(nil)...), ) t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) @@ -47,6 +47,37 @@ func TestDefaultViews_emptyByDefault(t *testing.T) { assert.Contains(t, keys, attribute.Key("workflow_execution_id")) } +func TestDefaultViews_dropsBlacklistedAttributes(t *testing.T) { + t.Parallel() + + reader := sdkmetric.NewManualReader() + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(reader), + sdkmetric.WithView(metricviews.DefaultViews([]string{"event_id"})...), + ) + t.Cleanup(func() { _ = mp.Shutdown(context.Background()) }) + + meter := mp.Meter("test") + counter, err := meter.Int64Counter("some_metric_total") + require.NoError(t, err) + + counter.Add(context.Background(), 1, + metric.WithAttributes( + attribute.String("service", "foo"), + attribute.String("event_id", "ev-1"), + attribute.String("workflow_execution_id", "exec-1"), + ), + ) + + var rm metricdata.ResourceMetrics + require.NoError(t, reader.Collect(context.Background(), &rm)) + + keys := attributeKeysFromSum(t, rm) + assert.Contains(t, keys, attribute.Key("service")) + assert.NotContains(t, keys, attribute.Key("event_id")) + assert.Contains(t, keys, attribute.Key("workflow_execution_id")) +} + func attributeKeysFromSum(t *testing.T, rm metricdata.ResourceMetrics) []attribute.Key { t.Helper() require.Len(t, rm.ScopeMetrics, 1) diff --git a/pkg/beholder/testdata/config-example.json b/pkg/beholder/testdata/config-example.json index db2a37a1d7..4f5ac8434d 100644 --- a/pkg/beholder/testdata/config-example.json +++ b/pkg/beholder/testdata/config-example.json @@ -33,6 +33,7 @@ "MetricRetryConfig": null, "MetricViews": null, "MetricViewsDisabled": false, + "MetricViewsAttributeBlacklist": null, "MetricCardinalityLimit": 0, "MetricCompressor": "gzip", "MetricProducers": null, diff --git a/pkg/loop/config.go b/pkg/loop/config.go index e0fcd7118e..8311d1959b 100644 --- a/pkg/loop/config.go +++ b/pkg/loop/config.go @@ -83,8 +83,9 @@ const ( envTelemetryTraceCompressor = "CL_TELEMETRY_TRACE_COMPRESSOR" envTelemetryMetricCompressor = "CL_TELEMETRY_METRIC_COMPRESSOR" envTelemetryMetricCardinalityLimit = "CL_TELEMETRY_METRIC_CARDINALITY_LIMIT" - envTelemetryMetricViewsDisabled = "CL_TELEMETRY_METRIC_VIEWS_DISABLED" - envTelemetryPrometheusBridgeEnabled = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" + envTelemetryMetricViewsDisabled = "CL_TELEMETRY_METRIC_VIEWS_DISABLED" + envTelemetryMetricViewsAttributeBlacklist = "CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_BLACKLIST" + envTelemetryPrometheusBridgeEnabled = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" envTelemetryPrometheusBridgePrefixes = "CL_TELEMETRY_PROMETHEUS_BRIDGE_PREFIXES" envTelemetryLogCompressor = "CL_TELEMETRY_LOG_COMPRESSOR" envMeterRecordsEnabled = "CL_METER_RECORDS_ENABLED" @@ -183,8 +184,11 @@ type EnvConfig struct { // explicit 0, which disables the limit. TelemetryMetricCardinalityLimit *int // TelemetryMetricViewsDisabled skips metricviews.DefaultViews() in the beholder client. - TelemetryMetricViewsDisabled bool - TelemetryPrometheusBridgeEnabled bool + TelemetryMetricViewsDisabled bool + // TelemetryMetricViewsAttributeBlacklist lists attribute keys dropped by the + // default global deny view (e.g. event_id). Empty means no keys are stripped. + TelemetryMetricViewsAttributeBlacklist []string + TelemetryPrometheusBridgeEnabled bool TelemetryPrometheusBridgePrefixes []string TelemetryLogCompressor string MeterRecordsEnabled bool @@ -302,6 +306,9 @@ func (e *EnvConfig) AsCmdEnv() (env []string) { add(envTelemetryMetricCardinalityLimit, strconv.Itoa(*e.TelemetryMetricCardinalityLimit)) } add(envTelemetryMetricViewsDisabled, strconv.FormatBool(e.TelemetryMetricViewsDisabled)) + if len(e.TelemetryMetricViewsAttributeBlacklist) > 0 { + add(envTelemetryMetricViewsAttributeBlacklist, strings.Join(e.TelemetryMetricViewsAttributeBlacklist, ",")) + } add(envTelemetryPrometheusBridgeEnabled, strconv.FormatBool(e.TelemetryPrometheusBridgeEnabled)) add(envTelemetryPrometheusBridgePrefixes, strings.Join(e.TelemetryPrometheusBridgePrefixes, ",")) add(envTelemetryLogCompressor, e.TelemetryLogCompressor) @@ -559,6 +566,9 @@ func (e *EnvConfig) parse() error { if err != nil { return fmt.Errorf("failed to parse %s: %w", envTelemetryMetricViewsDisabled, err) } + if v, ok := os.LookupEnv(envTelemetryMetricViewsAttributeBlacklist); ok && v != "" { + e.TelemetryMetricViewsAttributeBlacklist = splitCommaTrimmed(v) + } e.TelemetryPrometheusBridgeEnabled, err = getBool(envTelemetryPrometheusBridgeEnabled) if err != nil { return fmt.Errorf("failed to parse %s: %w", envTelemetryPrometheusBridgeEnabled, err) @@ -634,6 +644,17 @@ func getValidCollectorTarget() (string, error) { return tracingCollectorTarget, nil } +func splitCommaTrimmed(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + out = append(out, trimmed) + } + } + return out +} + func getMap(envKeyPrefix string) map[string]string { m := make(map[string]string) for _, env := range os.Environ() { diff --git a/pkg/loop/config_test.go b/pkg/loop/config_test.go index 830ca416c6..b05bf355e2 100644 --- a/pkg/loop/config_test.go +++ b/pkg/loop/config_test.go @@ -83,6 +83,7 @@ func TestEnvConfig_parse(t *testing.T) { envTelemetryEmitterExportMaxBatchSize: "100", envTelemetryEmitterMaxQueueSize: "1000", envTelemetryLogStreamingEnabled: "false", + envTelemetryMetricViewsAttributeBlacklist: "event_id", envTelemetryPrometheusBridgeEnabled: "true", envTelemetryPrometheusBridgePrefixes: "foo,bar", envMeterRecordsEnabled: "true", @@ -214,9 +215,10 @@ var envCfgFull = EnvConfig{ TelemetryEmitterExportMaxBatchSize: 100, TelemetryEmitterMaxQueueSize: 1000, TelemetryLogStreamingEnabled: false, - TelemetryMetricCardinalityLimit: new(100000), - TelemetryMetricViewsDisabled: false, - TelemetryPrometheusBridgeEnabled: true, + TelemetryMetricCardinalityLimit: new(100000), + TelemetryMetricViewsDisabled: false, + TelemetryMetricViewsAttributeBlacklist: nil, + TelemetryPrometheusBridgeEnabled: true, TelemetryPrometheusBridgePrefixes: []string{"foo", "bar"}, MeterRecordsEnabled: true, MeterSnapshotsEnabled: false, @@ -291,6 +293,8 @@ func TestEnvConfig_AsCmdEnv(t *testing.T) { assert.Equal(t, "false", got[envTelemetryLogStreamingEnabled]) assert.Equal(t, "100000", got[envTelemetryMetricCardinalityLimit]) assert.Equal(t, "false", got[envTelemetryMetricViewsDisabled]) + _, hasBlacklist := got[envTelemetryMetricViewsAttributeBlacklist] + assert.False(t, hasBlacklist) assert.Equal(t, "true", got[envTelemetryPrometheusBridgeEnabled]) assert.Equal(t, "foo,bar", got[envTelemetryPrometheusBridgePrefixes]) assert.Equal(t, "true", got[envMeterRecordsEnabled]) @@ -363,6 +367,43 @@ func TestEnvConfig_MetricCardinalityLimit_RoundTrip(t *testing.T) { }) } +// TestEnvConfig_MetricViewsAttributeBlacklist_RoundTrip verifies that a +// comma-separated blacklist survives an AsCmdEnv -> parse round trip. +func TestEnvConfig_MetricViewsAttributeBlacklist_RoundTrip(t *testing.T) { + setEnvFromCmdEnv := func(t *testing.T, env []string) { + t.Helper() + for _, kv := range env { + pair := strings.SplitN(kv, "=", 2) + require.Len(t, pair, 2) + t.Setenv(pair[0], pair[1]) + } + } + + t.Run("blacklist propagates", func(t *testing.T) { + cfg := envCfgFull + cfg.TelemetryMetricViewsAttributeBlacklist = []string{"event_id", "workflow_execution_id"} + setEnvFromCmdEnv(t, cfg.AsCmdEnv()) + + var parsed EnvConfig + require.NoError(t, parsed.parse()) + assert.Equal(t, []string{"event_id", "workflow_execution_id"}, parsed.TelemetryMetricViewsAttributeBlacklist) + }) + + t.Run("unset leaves blacklist nil", func(t *testing.T) { + cfg := envCfgFull + cfg.TelemetryMetricViewsAttributeBlacklist = nil + env := cfg.AsCmdEnv() + for _, kv := range env { + require.NotContains(t, kv, envTelemetryMetricViewsAttributeBlacklist+"=", "unset blacklist must not be emitted") + } + setEnvFromCmdEnv(t, env) + + var parsed EnvConfig + require.NoError(t, parsed.parse()) + assert.Nil(t, parsed.TelemetryMetricViewsAttributeBlacklist) + }) +} + // TestEnvConfig_MetricViewsDisabled_RoundTrip verifies that an explicit // disable survives an AsCmdEnv -> parse round trip and that leaving the field // unset keeps default views enabled. diff --git a/pkg/loop/server.go b/pkg/loop/server.go index 4da7a6bde8..8e8b9b553a 100644 --- a/pkg/loop/server.go +++ b/pkg/loop/server.go @@ -193,6 +193,7 @@ func (s *Server) start(opts ...ServerOpt) error { MetricCompressor: s.EnvConfig.TelemetryMetricCompressor, MetricCardinalityLimit: *s.EnvConfig.TelemetryMetricCardinalityLimit, MetricViewsDisabled: s.EnvConfig.TelemetryMetricViewsDisabled, + MetricViewsAttributeBlacklist: s.EnvConfig.TelemetryMetricViewsAttributeBlacklist, } if s.EnvConfig.TelemetryPrometheusBridgeEnabled { From ad88da1b1dccb87d767f30b99451f567b78b4b6e Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:05:21 -0400 Subject: [PATCH 3/4] Add configurable metric attribute blacklist for default views. Wire MetricViewsAttributeBlacklist through beholder.Config and CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_BLACKLIST via loop env/server. DefaultViews(blacklist) registers a global deny view when non-empty; empty by default so no attributes are stripped until configured. # Conflicts: # pkg/loop/config_test.go --- pkg/loop/config_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/loop/config_test.go b/pkg/loop/config_test.go index b05bf355e2..df96a4590b 100644 --- a/pkg/loop/config_test.go +++ b/pkg/loop/config_test.go @@ -217,7 +217,7 @@ var envCfgFull = EnvConfig{ TelemetryLogStreamingEnabled: false, TelemetryMetricCardinalityLimit: new(100000), TelemetryMetricViewsDisabled: false, - TelemetryMetricViewsAttributeBlacklist: nil, + TelemetryMetricViewsAttributeBlacklist: []string{"event_id"}, TelemetryPrometheusBridgeEnabled: true, TelemetryPrometheusBridgePrefixes: []string{"foo", "bar"}, MeterRecordsEnabled: true, @@ -293,8 +293,7 @@ func TestEnvConfig_AsCmdEnv(t *testing.T) { assert.Equal(t, "false", got[envTelemetryLogStreamingEnabled]) assert.Equal(t, "100000", got[envTelemetryMetricCardinalityLimit]) assert.Equal(t, "false", got[envTelemetryMetricViewsDisabled]) - _, hasBlacklist := got[envTelemetryMetricViewsAttributeBlacklist] - assert.False(t, hasBlacklist) + assert.Equal(t, "event_id", got[envTelemetryMetricViewsAttributeBlacklist]) assert.Equal(t, "true", got[envTelemetryPrometheusBridgeEnabled]) assert.Equal(t, "foo,bar", got[envTelemetryPrometheusBridgePrefixes]) assert.Equal(t, "true", got[envMeterRecordsEnabled]) From c0e49eaf5407d0221053075a0ea52ae29f84a492 Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:05:44 -0400 Subject: [PATCH 4/4] Drop MetricViewsDisabled; rename attribute blacklist to denylist. Empty MetricViewsAttributeDenylist skips DefaultViews() instead of a separate disabled flag. Env var is CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_DENYLIST. Co-authored-by: Cursor # Conflicts: # pkg/loop/config_test.go --- pkg/beholder/config.go | 9 +-- pkg/beholder/config_test.go | 3 +- pkg/beholder/meter_provider.go | 4 +- pkg/beholder/meter_provider_test.go | 10 ++- pkg/beholder/metricviews/views_test.go | 4 +- pkg/beholder/testdata/config-example.json | 3 +- pkg/loop/config.go | 24 +++----- pkg/loop/config_test.go | 74 +++++------------------ pkg/loop/server.go | 3 +- 9 files changed, 37 insertions(+), 97 deletions(-) diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index 86efa2f2f7..79ef5b0d01 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -40,12 +40,9 @@ type Config struct { MetricReaderInterval time.Duration MetricRetryConfig *RetryConfig MetricViews []metric.View - // MetricViewsDisabled skips metricviews.DefaultViews() attribute filtering. - // Caller-supplied MetricViews are still applied. Set via CL_TELEMETRY_METRIC_VIEWS_DISABLED. - MetricViewsDisabled bool - // MetricViewsAttributeBlacklist lists attribute keys dropped by the default - // global deny view (e.g. event_id). Empty means no keys are stripped. - MetricViewsAttributeBlacklist []string + // MetricViewsAttributeDenylist lists attribute keys dropped by the default + // global deny view (e.g. event_id). Empty skips metricviews.DefaultViews(). + MetricViewsAttributeDenylist []string // MetricCardinalityLimit sets the SDK per-instrument attribute-set limit (0 = disabled). // DefaultConfig uses 100000 as a production safety valve for high-cardinality workloads. MetricCardinalityLimit int diff --git a/pkg/beholder/config_test.go b/pkg/beholder/config_test.go index e6e5d29fc7..34c41740bd 100644 --- a/pkg/beholder/config_test.go +++ b/pkg/beholder/config_test.go @@ -55,8 +55,7 @@ func TestConfig(t *testing.T) { // Metric MetricReaderInterval: 1 * time.Second, MetricCompressor: "gzip", - MetricViewsDisabled: false, - MetricViewsAttributeBlacklist: nil, + MetricViewsAttributeDenylist: nil, MetricCardinalityLimit: 0, // OTel metric exporter retry config MetricRetryConfig: nil, diff --git a/pkg/beholder/meter_provider.go b/pkg/beholder/meter_provider.go index e47be568ba..dcb19de410 100644 --- a/pkg/beholder/meter_provider.go +++ b/pkg/beholder/meter_provider.go @@ -16,10 +16,10 @@ import ( // resolve to the same output stream (same name/description/unit/kind), the SDK // keeps only the first in registration order and drops the rest. func (cfg Config) metricViews() []sdkmetric.View { - if cfg.MetricViewsDisabled { + if len(cfg.MetricViewsAttributeDenylist) == 0 { return cfg.MetricViews } - return append(cfg.MetricViews, metricviews.DefaultViews(cfg.MetricViewsAttributeBlacklist)...) + return append(cfg.MetricViews, metricviews.DefaultViews(cfg.MetricViewsAttributeDenylist)...) } // metricOptions returns cfg-derived sdkmetric.Option values (metric views and the diff --git a/pkg/beholder/meter_provider_test.go b/pkg/beholder/meter_provider_test.go index e4d4ae019f..62d82ea1d8 100644 --- a/pkg/beholder/meter_provider_test.go +++ b/pkg/beholder/meter_provider_test.go @@ -25,7 +25,6 @@ func TestConfig_metricOptions_cardinalityLimit(t *testing.T) { reader := sdkmetric.NewManualReader() cfg := DefaultConfig() cfg.MetricCardinalityLimit = limit - cfg.MetricViewsDisabled = true mpOpts := append(cfg.metricOptions(), sdkmetric.WithReader(reader)) mp := sdkmetric.NewMeterProvider(mpOpts...) @@ -66,18 +65,18 @@ func TestConfig_metricViews_appendsDefaultsAfterCallerViews(t *testing.T) { require.GreaterOrEqual(t, len(views), len(metricviews.DefaultViews(nil))+1) } -func TestConfig_metricViews_includesBlacklistDefaultView(t *testing.T) { +func TestConfig_metricViews_includesDenylistDefaultView(t *testing.T) { t.Parallel() cfg := DefaultConfig() - cfg.MetricViewsAttributeBlacklist = []string{"event_id"} + cfg.MetricViewsAttributeDenylist = []string{"event_id"} views := cfg.metricViews() - require.Len(t, views, len(metricviews.DefaultViews(cfg.MetricViewsAttributeBlacklist))) + require.Len(t, views, len(metricviews.DefaultViews(cfg.MetricViewsAttributeDenylist))) require.NotEmpty(t, views) } -func TestConfig_metricViews_disabledSkipsDefaults(t *testing.T) { +func TestConfig_metricViews_emptyDenylistSkipsDefaults(t *testing.T) { t.Parallel() callerView := sdkmetric.NewView( @@ -86,7 +85,6 @@ func TestConfig_metricViews_disabledSkipsDefaults(t *testing.T) { ) cfg := DefaultConfig() cfg.MetricViews = []sdkmetric.View{callerView} - cfg.MetricViewsDisabled = true views := cfg.metricViews() require.Len(t, views, 1) diff --git a/pkg/beholder/metricviews/views_test.go b/pkg/beholder/metricviews/views_test.go index d88cf146da..733c12d31c 100644 --- a/pkg/beholder/metricviews/views_test.go +++ b/pkg/beholder/metricviews/views_test.go @@ -15,7 +15,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/beholder/metricviews" ) -func TestDefaultViews_emptyBlacklist(t *testing.T) { +func TestDefaultViews_emptyDenylist(t *testing.T) { t.Parallel() assert.Nil(t, metricviews.DefaultViews(nil)) @@ -47,7 +47,7 @@ func TestDefaultViews_emptyBlacklist(t *testing.T) { assert.Contains(t, keys, attribute.Key("workflow_execution_id")) } -func TestDefaultViews_dropsBlacklistedAttributes(t *testing.T) { +func TestDefaultViews_dropsDeniedAttributes(t *testing.T) { t.Parallel() reader := sdkmetric.NewManualReader() diff --git a/pkg/beholder/testdata/config-example.json b/pkg/beholder/testdata/config-example.json index 4f5ac8434d..b8e3d33d2f 100644 --- a/pkg/beholder/testdata/config-example.json +++ b/pkg/beholder/testdata/config-example.json @@ -32,8 +32,7 @@ "MetricReaderInterval": 1000000000, "MetricRetryConfig": null, "MetricViews": null, - "MetricViewsDisabled": false, - "MetricViewsAttributeBlacklist": null, + "MetricViewsAttributeDenylist": null, "MetricCardinalityLimit": 0, "MetricCompressor": "gzip", "MetricProducers": null, diff --git a/pkg/loop/config.go b/pkg/loop/config.go index 8311d1959b..9b146acb20 100644 --- a/pkg/loop/config.go +++ b/pkg/loop/config.go @@ -83,8 +83,7 @@ const ( envTelemetryTraceCompressor = "CL_TELEMETRY_TRACE_COMPRESSOR" envTelemetryMetricCompressor = "CL_TELEMETRY_METRIC_COMPRESSOR" envTelemetryMetricCardinalityLimit = "CL_TELEMETRY_METRIC_CARDINALITY_LIMIT" - envTelemetryMetricViewsDisabled = "CL_TELEMETRY_METRIC_VIEWS_DISABLED" - envTelemetryMetricViewsAttributeBlacklist = "CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_BLACKLIST" + envTelemetryMetricViewsAttributeDenylist = "CL_TELEMETRY_METRIC_VIEWS_ATTRIBUTE_DENYLIST" envTelemetryPrometheusBridgeEnabled = "CL_TELEMETRY_PROMETHEUS_BRIDGE_ENABLED" envTelemetryPrometheusBridgePrefixes = "CL_TELEMETRY_PROMETHEUS_BRIDGE_PREFIXES" envTelemetryLogCompressor = "CL_TELEMETRY_LOG_COMPRESSOR" @@ -183,11 +182,9 @@ type EnvConfig struct { // distinguish "no opinion" (child applies its own default) from an // explicit 0, which disables the limit. TelemetryMetricCardinalityLimit *int - // TelemetryMetricViewsDisabled skips metricviews.DefaultViews() in the beholder client. - TelemetryMetricViewsDisabled bool - // TelemetryMetricViewsAttributeBlacklist lists attribute keys dropped by the - // default global deny view (e.g. event_id). Empty means no keys are stripped. - TelemetryMetricViewsAttributeBlacklist []string + // TelemetryMetricViewsAttributeDenylist lists attribute keys dropped by the + // default global deny view (e.g. event_id). Empty skips DefaultViews(). + TelemetryMetricViewsAttributeDenylist []string TelemetryPrometheusBridgeEnabled bool TelemetryPrometheusBridgePrefixes []string TelemetryLogCompressor string @@ -305,9 +302,8 @@ func (e *EnvConfig) AsCmdEnv() (env []string) { if e.TelemetryMetricCardinalityLimit != nil { add(envTelemetryMetricCardinalityLimit, strconv.Itoa(*e.TelemetryMetricCardinalityLimit)) } - add(envTelemetryMetricViewsDisabled, strconv.FormatBool(e.TelemetryMetricViewsDisabled)) - if len(e.TelemetryMetricViewsAttributeBlacklist) > 0 { - add(envTelemetryMetricViewsAttributeBlacklist, strings.Join(e.TelemetryMetricViewsAttributeBlacklist, ",")) + if len(e.TelemetryMetricViewsAttributeDenylist) > 0 { + add(envTelemetryMetricViewsAttributeDenylist, strings.Join(e.TelemetryMetricViewsAttributeDenylist, ",")) } add(envTelemetryPrometheusBridgeEnabled, strconv.FormatBool(e.TelemetryPrometheusBridgeEnabled)) add(envTelemetryPrometheusBridgePrefixes, strings.Join(e.TelemetryPrometheusBridgePrefixes, ",")) @@ -562,12 +558,8 @@ func (e *EnvConfig) parse() error { defaultLimit := 100000 e.TelemetryMetricCardinalityLimit = &defaultLimit } - e.TelemetryMetricViewsDisabled, err = getBool(envTelemetryMetricViewsDisabled) - if err != nil { - return fmt.Errorf("failed to parse %s: %w", envTelemetryMetricViewsDisabled, err) - } - if v, ok := os.LookupEnv(envTelemetryMetricViewsAttributeBlacklist); ok && v != "" { - e.TelemetryMetricViewsAttributeBlacklist = splitCommaTrimmed(v) + if v, ok := os.LookupEnv(envTelemetryMetricViewsAttributeDenylist); ok && v != "" { + e.TelemetryMetricViewsAttributeDenylist = splitCommaTrimmed(v) } e.TelemetryPrometheusBridgeEnabled, err = getBool(envTelemetryPrometheusBridgeEnabled) if err != nil { diff --git a/pkg/loop/config_test.go b/pkg/loop/config_test.go index df96a4590b..f2ad95fb60 100644 --- a/pkg/loop/config_test.go +++ b/pkg/loop/config_test.go @@ -83,7 +83,7 @@ func TestEnvConfig_parse(t *testing.T) { envTelemetryEmitterExportMaxBatchSize: "100", envTelemetryEmitterMaxQueueSize: "1000", envTelemetryLogStreamingEnabled: "false", - envTelemetryMetricViewsAttributeBlacklist: "event_id", + envTelemetryMetricViewsAttributeDenylist: "event_id", envTelemetryPrometheusBridgeEnabled: "true", envTelemetryPrometheusBridgePrefixes: "foo,bar", envMeterRecordsEnabled: "true", @@ -215,10 +215,9 @@ var envCfgFull = EnvConfig{ TelemetryEmitterExportMaxBatchSize: 100, TelemetryEmitterMaxQueueSize: 1000, TelemetryLogStreamingEnabled: false, - TelemetryMetricCardinalityLimit: new(100000), - TelemetryMetricViewsDisabled: false, - TelemetryMetricViewsAttributeBlacklist: []string{"event_id"}, - TelemetryPrometheusBridgeEnabled: true, + TelemetryMetricCardinalityLimit: new(100000), + TelemetryMetricViewsAttributeDenylist: []string{"event_id"}, + TelemetryPrometheusBridgeEnabled: true, TelemetryPrometheusBridgePrefixes: []string{"foo", "bar"}, MeterRecordsEnabled: true, MeterSnapshotsEnabled: false, @@ -292,8 +291,7 @@ func TestEnvConfig_AsCmdEnv(t *testing.T) { assert.Equal(t, "1000", got[envTelemetryEmitterMaxQueueSize]) assert.Equal(t, "false", got[envTelemetryLogStreamingEnabled]) assert.Equal(t, "100000", got[envTelemetryMetricCardinalityLimit]) - assert.Equal(t, "false", got[envTelemetryMetricViewsDisabled]) - assert.Equal(t, "event_id", got[envTelemetryMetricViewsAttributeBlacklist]) + assert.Equal(t, "event_id", got[envTelemetryMetricViewsAttributeDenylist]) assert.Equal(t, "true", got[envTelemetryPrometheusBridgeEnabled]) assert.Equal(t, "foo,bar", got[envTelemetryPrometheusBridgePrefixes]) assert.Equal(t, "true", got[envMeterRecordsEnabled]) @@ -366,9 +364,9 @@ func TestEnvConfig_MetricCardinalityLimit_RoundTrip(t *testing.T) { }) } -// TestEnvConfig_MetricViewsAttributeBlacklist_RoundTrip verifies that a -// comma-separated blacklist survives an AsCmdEnv -> parse round trip. -func TestEnvConfig_MetricViewsAttributeBlacklist_RoundTrip(t *testing.T) { +// TestEnvConfig_MetricViewsAttributeDenylist_RoundTrip verifies that a +// comma-separated denylist survives an AsCmdEnv -> parse round trip. +func TestEnvConfig_MetricViewsAttributeDenylist_RoundTrip(t *testing.T) { setEnvFromCmdEnv := func(t *testing.T, env []string) { t.Helper() for _, kv := range env { @@ -378,70 +376,28 @@ func TestEnvConfig_MetricViewsAttributeBlacklist_RoundTrip(t *testing.T) { } } - t.Run("blacklist propagates", func(t *testing.T) { + t.Run("denylist propagates", func(t *testing.T) { cfg := envCfgFull - cfg.TelemetryMetricViewsAttributeBlacklist = []string{"event_id", "workflow_execution_id"} + cfg.TelemetryMetricViewsAttributeDenylist = []string{"event_id", "workflow_execution_id"} setEnvFromCmdEnv(t, cfg.AsCmdEnv()) var parsed EnvConfig require.NoError(t, parsed.parse()) - assert.Equal(t, []string{"event_id", "workflow_execution_id"}, parsed.TelemetryMetricViewsAttributeBlacklist) + assert.Equal(t, []string{"event_id", "workflow_execution_id"}, parsed.TelemetryMetricViewsAttributeDenylist) }) - t.Run("unset leaves blacklist nil", func(t *testing.T) { + t.Run("unset leaves denylist nil", func(t *testing.T) { cfg := envCfgFull - cfg.TelemetryMetricViewsAttributeBlacklist = nil + cfg.TelemetryMetricViewsAttributeDenylist = nil env := cfg.AsCmdEnv() for _, kv := range env { - require.NotContains(t, kv, envTelemetryMetricViewsAttributeBlacklist+"=", "unset blacklist must not be emitted") + require.NotContains(t, kv, envTelemetryMetricViewsAttributeDenylist+"=", "unset denylist must not be emitted") } setEnvFromCmdEnv(t, env) var parsed EnvConfig require.NoError(t, parsed.parse()) - assert.Nil(t, parsed.TelemetryMetricViewsAttributeBlacklist) - }) -} - -// TestEnvConfig_MetricViewsDisabled_RoundTrip verifies that an explicit -// disable survives an AsCmdEnv -> parse round trip and that leaving the field -// unset keeps default views enabled. -func TestEnvConfig_MetricViewsDisabled_RoundTrip(t *testing.T) { - setEnvFromCmdEnv := func(t *testing.T, env []string) { - t.Helper() - for _, kv := range env { - pair := strings.SplitN(kv, "=", 2) - require.Len(t, pair, 2) - t.Setenv(pair[0], pair[1]) - } - } - - t.Run("explicit disable propagates as true", func(t *testing.T) { - cfg := envCfgFull - cfg.TelemetryMetricViewsDisabled = true - setEnvFromCmdEnv(t, cfg.AsCmdEnv()) - - var parsed EnvConfig - require.NoError(t, parsed.parse()) - assert.True(t, parsed.TelemetryMetricViewsDisabled) - }) - - t.Run("unset keeps default views enabled", func(t *testing.T) { - cfg := envCfgFull - cfg.TelemetryMetricViewsDisabled = false - env := cfg.AsCmdEnv() - for _, kv := range env { - pair := strings.SplitN(kv, "=", 2) - require.Len(t, pair, 2) - if pair[0] == envTelemetryMetricViewsDisabled { - continue - } - t.Setenv(pair[0], pair[1]) - } - - var parsed EnvConfig - require.NoError(t, parsed.parse()) - assert.False(t, parsed.TelemetryMetricViewsDisabled) + assert.Nil(t, parsed.TelemetryMetricViewsAttributeDenylist) }) } diff --git a/pkg/loop/server.go b/pkg/loop/server.go index 8e8b9b553a..8ddfd3f1d1 100644 --- a/pkg/loop/server.go +++ b/pkg/loop/server.go @@ -192,8 +192,7 @@ func (s *Server) start(opts ...ServerOpt) error { ChipIngressLogger: s.Logger, MetricCompressor: s.EnvConfig.TelemetryMetricCompressor, MetricCardinalityLimit: *s.EnvConfig.TelemetryMetricCardinalityLimit, - MetricViewsDisabled: s.EnvConfig.TelemetryMetricViewsDisabled, - MetricViewsAttributeBlacklist: s.EnvConfig.TelemetryMetricViewsAttributeBlacklist, + MetricViewsAttributeDenylist: s.EnvConfig.TelemetryMetricViewsAttributeDenylist, } if s.EnvConfig.TelemetryPrometheusBridgeEnabled {