Skip to content

Commit 0ad38c3

Browse files
committed
Add metric level registry allowing components to declare minimum telemetry levels for their metrics
Signed-off-by: Israel Blancas <[email protected]>
1 parent a9aaa99 commit 0ad38c3

File tree

14 files changed

+619
-117
lines changed

14 files changed

+619
-117
lines changed

.chloggen/11754.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
7+
component: all
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add a registry so components can declare the telemetry metrics that require higher service metric levels, letting the service auto-drop those meters/instruments via default views.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [11754]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: Also adds `component.MetricLevelConfig` so component packages can register their own minimum metric levels.
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: [api]

component/metric_level.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package component // import "go.opentelemetry.io/collector/component"
5+
6+
import "sync"
7+
8+
// MetricLevelConfig declares the minimum service telemetry level required for
9+
// a given meter or instrument. Components can register these declarations so
10+
// the service can derive default views that drop metrics when the level is
11+
// below the configured threshold.
12+
type MetricLevelConfig struct {
13+
// MeterName is the fully-qualified meter name emitting the metric.
14+
MeterName string
15+
16+
// InstrumentName optionally scopes the config to a specific instrument. When
17+
// empty, the config applies to the complete meter.
18+
InstrumentName string
19+
20+
// Level is the minimum service telemetry level required for the meter or
21+
// instrument to be enabled.
22+
Level MetricLevel
23+
}
24+
25+
// MetricLevel mirrors service::telemetry::metrics::level values.
26+
type MetricLevel int32
27+
28+
// metricLevelRegistry is a global registry for metric level configurations.
29+
var metricLevelRegistry = struct {
30+
sync.RWMutex
31+
configs []MetricLevelConfig
32+
byMeter map[string][]int
33+
}{byMeter: make(map[string][]int)}
34+
35+
// RegisterMetricLevelConfigs registers one or more MetricLevelConfig entries.
36+
func RegisterMetricLevelConfigs(configs ...MetricLevelConfig) {
37+
if len(configs) == 0 {
38+
return
39+
}
40+
41+
metricLevelRegistry.Lock()
42+
defer metricLevelRegistry.Unlock()
43+
44+
for _, cfg := range configs {
45+
if cfg.MeterName == "" {
46+
panic("component: MetricLevelConfig requires MeterName")
47+
}
48+
idx := len(metricLevelRegistry.configs)
49+
metricLevelRegistry.configs = append(metricLevelRegistry.configs, cfg)
50+
metricLevelRegistry.byMeter[cfg.MeterName] = append(metricLevelRegistry.byMeter[cfg.MeterName], idx)
51+
}
52+
}
53+
54+
// RegisteredMetricLevelConfigs returns all registered metric level declarations.
55+
// The returned slice is a copy to prevent external modification of the registry.
56+
// For better performance when iterating, consider using RegisteredMetricLevelConfigsByMeter
57+
// if you only need configs for specific meters.
58+
func RegisteredMetricLevelConfigs() []MetricLevelConfig {
59+
metricLevelRegistry.RLock()
60+
defer metricLevelRegistry.RUnlock()
61+
62+
out := make([]MetricLevelConfig, len(metricLevelRegistry.configs))
63+
copy(out, metricLevelRegistry.configs)
64+
return out
65+
}
66+
67+
// RegisteredMetricLevelConfigsByMeter returns all registered metric level declarations
68+
// for the given meter name.
69+
func RegisteredMetricLevelConfigsByMeter(meterName string) []MetricLevelConfig {
70+
metricLevelRegistry.RLock()
71+
defer metricLevelRegistry.RUnlock()
72+
73+
indices, exists := metricLevelRegistry.byMeter[meterName]
74+
if !exists {
75+
return nil
76+
}
77+
78+
configs := make([]MetricLevelConfig, 0, len(indices))
79+
for _, idx := range indices {
80+
configs = append(configs, metricLevelRegistry.configs[idx])
81+
}
82+
return configs
83+
}
84+
85+
// ResetMetricLevelRegistryForTesting resets the global registry for testing.
86+
// This function is exported for use in test files (including tests in other packages)
87+
// and should never be called from production code.
88+
func ResetMetricLevelRegistryForTesting() {
89+
metricLevelRegistry.Lock()
90+
defer metricLevelRegistry.Unlock()
91+
metricLevelRegistry.configs = nil
92+
metricLevelRegistry.byMeter = make(map[string][]int)
93+
}

component/metric_level_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package component
5+
6+
import (
7+
"sync"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestRegisterMetricLevelConfigs(t *testing.T) {
15+
t.Cleanup(ResetMetricLevelRegistryForTesting)
16+
17+
RegisterMetricLevelConfigs(
18+
MetricLevelConfig{
19+
MeterName: "test/meter",
20+
Level: MetricLevel(2),
21+
},
22+
MetricLevelConfig{
23+
MeterName: "test/meter",
24+
InstrumentName: "metric",
25+
Level: MetricLevel(1),
26+
},
27+
)
28+
29+
configs := RegisteredMetricLevelConfigs()
30+
require.Len(t, configs, 2)
31+
assert.Equal(t, "test/meter", configs[0].MeterName)
32+
assert.Equal(t, MetricLevel(2), configs[0].Level)
33+
assert.Equal(t, "metric", configs[1].InstrumentName)
34+
assert.Equal(t, MetricLevel(1), configs[1].Level)
35+
}
36+
37+
func TestRegisterMetricLevelConfigsPanicsWithoutMeter(t *testing.T) {
38+
t.Cleanup(ResetMetricLevelRegistryForTesting)
39+
40+
assert.Panics(t, func() {
41+
RegisterMetricLevelConfigs(MetricLevelConfig{Level: MetricLevel(2)})
42+
})
43+
}
44+
45+
func TestRegisteredMetricLevelConfigsByMeter(t *testing.T) {
46+
t.Cleanup(ResetMetricLevelRegistryForTesting)
47+
48+
RegisterMetricLevelConfigs(
49+
MetricLevelConfig{
50+
MeterName: "test/meter1",
51+
Level: MetricLevel(1),
52+
},
53+
MetricLevelConfig{
54+
MeterName: "test/meter1",
55+
InstrumentName: "instrument1",
56+
Level: MetricLevel(2),
57+
},
58+
MetricLevelConfig{
59+
MeterName: "test/meter2",
60+
Level: MetricLevel(1),
61+
},
62+
)
63+
64+
tests := []struct {
65+
name string
66+
meterName string
67+
wantCount int
68+
wantLevels []MetricLevel
69+
}{
70+
{
71+
name: "existing meter with multiple configs",
72+
meterName: "test/meter1",
73+
wantCount: 2,
74+
wantLevels: []MetricLevel{1, 2},
75+
},
76+
{
77+
name: "existing meter with single config",
78+
meterName: "test/meter2",
79+
wantCount: 1,
80+
wantLevels: []MetricLevel{1},
81+
},
82+
{
83+
name: "non-existent meter",
84+
meterName: "test/nonexistent",
85+
wantCount: 0,
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
configs := RegisteredMetricLevelConfigsByMeter(tt.meterName)
92+
require.Len(t, configs, tt.wantCount)
93+
for i, cfg := range configs {
94+
assert.Equal(t, tt.meterName, cfg.MeterName)
95+
if len(tt.wantLevels) > i {
96+
assert.Equal(t, tt.wantLevels[i], cfg.Level)
97+
}
98+
}
99+
})
100+
}
101+
}
102+
103+
func TestRegisterMetricLevelConfigsEmptySlice(t *testing.T) {
104+
t.Cleanup(ResetMetricLevelRegistryForTesting)
105+
106+
initialCount := len(RegisteredMetricLevelConfigs())
107+
RegisterMetricLevelConfigs()
108+
assert.Len(t, RegisteredMetricLevelConfigs(), initialCount)
109+
}
110+
111+
func TestRegisterMetricLevelConfigsConcurrent(t *testing.T) {
112+
t.Cleanup(ResetMetricLevelRegistryForTesting)
113+
114+
const numGoroutines = 10
115+
const configsPerGoroutine = 5
116+
117+
var wg sync.WaitGroup
118+
wg.Add(numGoroutines)
119+
120+
for i := range numGoroutines {
121+
go func(id int) {
122+
defer wg.Done()
123+
configs := make([]MetricLevelConfig, configsPerGoroutine)
124+
for j := range configsPerGoroutine {
125+
configs[j] = MetricLevelConfig{
126+
MeterName: "test/concurrent",
127+
InstrumentName: "instrument",
128+
Level: MetricLevel(id*configsPerGoroutine + j),
129+
}
130+
}
131+
RegisterMetricLevelConfigs(configs...)
132+
}(i)
133+
}
134+
135+
wg.Wait()
136+
137+
allConfigs := RegisteredMetricLevelConfigs()
138+
require.GreaterOrEqual(t, len(allConfigs), numGoroutines*configsPerGoroutine)
139+
140+
var readWg sync.WaitGroup
141+
readWg.Add(numGoroutines)
142+
for range numGoroutines {
143+
go func() {
144+
defer readWg.Done()
145+
configs := RegisteredMetricLevelConfigs()
146+
assert.GreaterOrEqual(t, len(configs), numGoroutines*configsPerGoroutine)
147+
}()
148+
}
149+
readWg.Wait()
150+
}
151+
152+
func TestRegisteredMetricLevelConfigsReturnsCopy(t *testing.T) {
153+
t.Cleanup(ResetMetricLevelRegistryForTesting)
154+
155+
RegisterMetricLevelConfigs(
156+
MetricLevelConfig{
157+
MeterName: "test/copy",
158+
Level: MetricLevel(1),
159+
},
160+
)
161+
162+
configs1 := RegisteredMetricLevelConfigs()
163+
configs2 := RegisteredMetricLevelConfigs()
164+
165+
configs1[0].MeterName = "modified"
166+
167+
assert.Equal(t, "test/copy", configs2[0].MeterName)
168+
assert.Equal(t, "modified", configs1[0].MeterName)
169+
}
170+
171+
func TestRegisteredMetricLevelConfigsByMeterReturnsCopy(t *testing.T) {
172+
t.Cleanup(ResetMetricLevelRegistryForTesting)
173+
174+
RegisterMetricLevelConfigs(
175+
MetricLevelConfig{
176+
MeterName: "test/copy",
177+
Level: MetricLevel(1),
178+
},
179+
)
180+
181+
configs1 := RegisteredMetricLevelConfigsByMeter("test/copy")
182+
configs2 := RegisteredMetricLevelConfigsByMeter("test/copy")
183+
184+
require.Len(t, configs1, 1)
185+
require.Len(t, configs2, 1)
186+
187+
configs1[0].MeterName = "modified"
188+
189+
assert.Equal(t, "test/copy", configs2[0].MeterName)
190+
assert.Equal(t, "modified", configs1[0].MeterName)
191+
}

exporter/exporterhelper/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
go.opentelemetry.io/collector/component/componenttest v0.142.0
1111
go.opentelemetry.io/collector/config/configoptional v1.48.0
1212
go.opentelemetry.io/collector/config/configretry v1.48.0
13+
go.opentelemetry.io/collector/config/configtelemetry v1.48.0
1314
go.opentelemetry.io/collector/confmap v1.48.0
1415
go.opentelemetry.io/collector/confmap/xconfmap v0.142.0
1516
go.opentelemetry.io/collector/consumer v1.48.0
@@ -89,6 +90,8 @@ replace go.opentelemetry.io/collector/pipeline => ../../pipeline
8990

9091
replace go.opentelemetry.io/collector/receiver => ../../receiver
9192

93+
replace go.opentelemetry.io/collector/config/configtelemetry => ../../config/configtelemetry
94+
9295
replace go.opentelemetry.io/collector/config/configretry => ../../config/configretry
9396

9497
replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package exporterhelper // import "go.opentelemetry.io/collector/exporter/exporterhelper"
5+
6+
import (
7+
"go.opentelemetry.io/collector/component"
8+
"go.opentelemetry.io/collector/config/configtelemetry"
9+
)
10+
11+
func init() {
12+
component.RegisterMetricLevelConfigs(component.MetricLevelConfig{
13+
MeterName: "go.opentelemetry.io/collector/exporter/exporterhelper",
14+
InstrumentName: "otelcol_exporter_queue_batch_send_size_bytes",
15+
Level: component.MetricLevel(configtelemetry.LevelDetailed),
16+
})
17+
}

exporter/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
3434
github.com/pmezard/go-difflib v1.0.0 // indirect
3535
go.opentelemetry.io/collector/client v1.48.0 // indirect
36+
go.opentelemetry.io/collector/config/configtelemetry v1.48.0 // indirect
3637
go.opentelemetry.io/collector/confmap v1.48.0 // indirect
3738
go.opentelemetry.io/collector/confmap/xconfmap v0.142.0 // indirect
3839
go.opentelemetry.io/collector/consumer/consumererror v0.142.0 // indirect
@@ -76,6 +77,8 @@ retract v0.76.0 // Depends on retracted pdata v1.0.0-rc10 module
7677

7778
replace go.opentelemetry.io/collector/config/configretry => ../config/configretry
7879

80+
replace go.opentelemetry.io/collector/config/configtelemetry => ../config/configtelemetry
81+
7982
replace go.opentelemetry.io/collector/consumer/xconsumer => ../consumer/xconsumer
8083

8184
replace go.opentelemetry.io/collector/consumer/consumertest => ../consumer/consumertest

processor/batchprocessor/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
go.opentelemetry.io/collector/client v1.48.0
88
go.opentelemetry.io/collector/component v1.48.0
99
go.opentelemetry.io/collector/component/componenttest v0.142.0
10+
go.opentelemetry.io/collector/config/configtelemetry v1.48.0
1011
go.opentelemetry.io/collector/confmap v1.48.0
1112
go.opentelemetry.io/collector/consumer v1.48.0
1213
go.opentelemetry.io/collector/consumer/consumererror v0.142.0
@@ -77,6 +78,8 @@ replace go.opentelemetry.io/collector/pdata/xpdata => ../../pdata/xpdata
7778

7879
replace go.opentelemetry.io/collector/consumer => ../../consumer
7980

81+
replace go.opentelemetry.io/collector/config/configtelemetry => ../../config/configtelemetry
82+
8083
retract (
8184
v0.76.0 // Depends on retracted pdata v1.0.0-rc10 module, use v0.76.1
8285
v0.69.0 // Release failed, use v0.69.1

0 commit comments

Comments
 (0)