-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add metric level registry for components #14338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Use this changelog template to create an entry for release notes. | ||
|
|
||
| # One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' | ||
| change_type: enhancement | ||
|
|
||
| # The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp) | ||
| component: all | ||
|
|
||
| # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). | ||
| 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. | ||
|
|
||
| # One or more tracking issues or pull requests related to the change | ||
| issues: [11754] | ||
|
|
||
| # (Optional) One or more lines of additional information to render under the primary note. | ||
| # These lines will be padded with 2 spaces and then inserted directly into the document. | ||
| # Use pipe (|) for multiline entries. | ||
| subtext: Also adds `component.MetricLevelConfig` so component packages can register their own minimum metric levels. | ||
|
|
||
| # Optional: The change log or logs in which this entry should be included. | ||
| # e.g. '[user]' or '[user, api]' | ||
| # Include 'user' if the change is relevant to end users. | ||
| # Include 'api' if there is a change to a library API. | ||
| # Default: '[user]' | ||
| change_logs: [api] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package component // import "go.opentelemetry.io/collector/component" | ||
|
|
||
| import "sync" | ||
|
|
||
| // MetricLevelConfig declares the minimum service telemetry level required for | ||
| // a given meter or instrument. Components can register these declarations so | ||
| // the service can derive default views that drop metrics when the level is | ||
| // below the configured threshold. | ||
| type MetricLevelConfig struct { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we allow components to register views instead?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe. I'm not against that. I was thinking about keeping this because maybe want to do something based on the stability metric level? Something we can add later. But I'm ok with the change. Let me know what do you think and I will be more than happy to change this. |
||
| // MeterName is the fully-qualified meter name emitting the metric. | ||
| MeterName string | ||
|
|
||
| // InstrumentName optionally scopes the config to a specific instrument. When | ||
| // empty, the config applies to the complete meter. | ||
| InstrumentName string | ||
|
|
||
| // Level is the minimum service telemetry level required for the meter or | ||
| // instrument to be enabled. | ||
| Level MetricLevel | ||
| } | ||
|
|
||
| // MetricLevel mirrors service::telemetry::metrics::level values. | ||
| type MetricLevel int32 | ||
|
|
||
| // metricLevelRegistry is a global registry for metric level configurations. | ||
| var metricLevelRegistry = struct { | ||
| sync.RWMutex | ||
| configs []MetricLevelConfig | ||
| byMeter map[string][]int | ||
| }{byMeter: make(map[string][]int)} | ||
|
|
||
| // RegisterMetricLevelConfigs registers one or more MetricLevelConfig entries. | ||
| func RegisterMetricLevelConfigs(configs ...MetricLevelConfig) { | ||
| if len(configs) == 0 { | ||
| return | ||
| } | ||
|
|
||
| metricLevelRegistry.Lock() | ||
| defer metricLevelRegistry.Unlock() | ||
|
|
||
| for _, cfg := range configs { | ||
| if cfg.MeterName == "" { | ||
| panic("component: MetricLevelConfig requires MeterName") | ||
| } | ||
| idx := len(metricLevelRegistry.configs) | ||
| metricLevelRegistry.configs = append(metricLevelRegistry.configs, cfg) | ||
| metricLevelRegistry.byMeter[cfg.MeterName] = append(metricLevelRegistry.byMeter[cfg.MeterName], idx) | ||
| } | ||
| } | ||
|
|
||
| // RegisteredMetricLevelConfigs returns all registered metric level declarations. | ||
| // The returned slice is a copy to prevent external modification of the registry. | ||
| // For better performance when iterating, consider using RegisteredMetricLevelConfigsByMeter | ||
| // if you only need configs for specific meters. | ||
| func RegisteredMetricLevelConfigs() []MetricLevelConfig { | ||
| metricLevelRegistry.RLock() | ||
| defer metricLevelRegistry.RUnlock() | ||
|
|
||
| out := make([]MetricLevelConfig, len(metricLevelRegistry.configs)) | ||
| copy(out, metricLevelRegistry.configs) | ||
| return out | ||
| } | ||
|
|
||
| // RegisteredMetricLevelConfigsByMeter returns all registered metric level declarations | ||
| // for the given meter name. | ||
| func RegisteredMetricLevelConfigsByMeter(meterName string) []MetricLevelConfig { | ||
| metricLevelRegistry.RLock() | ||
| defer metricLevelRegistry.RUnlock() | ||
|
|
||
| indices, exists := metricLevelRegistry.byMeter[meterName] | ||
| if !exists { | ||
| return nil | ||
| } | ||
|
|
||
| configs := make([]MetricLevelConfig, 0, len(indices)) | ||
| for _, idx := range indices { | ||
| configs = append(configs, metricLevelRegistry.configs[idx]) | ||
| } | ||
| return configs | ||
| } | ||
|
|
||
| // ResetMetricLevelRegistryForTesting resets the global registry for testing. | ||
| // This function is exported for use in test files (including tests in other packages) | ||
| // and should never be called from production code. | ||
| func ResetMetricLevelRegistryForTesting() { | ||
| metricLevelRegistry.Lock() | ||
| defer metricLevelRegistry.Unlock() | ||
| metricLevelRegistry.configs = nil | ||
| metricLevelRegistry.byMeter = make(map[string][]int) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package component | ||
|
|
||
| import ( | ||
| "sync" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestRegisterMetricLevelConfigs(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| RegisterMetricLevelConfigs( | ||
| MetricLevelConfig{ | ||
| MeterName: "test/meter", | ||
| Level: MetricLevel(2), | ||
| }, | ||
| MetricLevelConfig{ | ||
| MeterName: "test/meter", | ||
| InstrumentName: "metric", | ||
| Level: MetricLevel(1), | ||
| }, | ||
| ) | ||
|
|
||
| configs := RegisteredMetricLevelConfigs() | ||
| require.Len(t, configs, 2) | ||
| assert.Equal(t, "test/meter", configs[0].MeterName) | ||
| assert.Equal(t, MetricLevel(2), configs[0].Level) | ||
| assert.Equal(t, "metric", configs[1].InstrumentName) | ||
| assert.Equal(t, MetricLevel(1), configs[1].Level) | ||
| } | ||
|
|
||
| func TestRegisterMetricLevelConfigsPanicsWithoutMeter(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| assert.Panics(t, func() { | ||
| RegisterMetricLevelConfigs(MetricLevelConfig{Level: MetricLevel(2)}) | ||
| }) | ||
| } | ||
|
|
||
| func TestRegisteredMetricLevelConfigsByMeter(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| RegisterMetricLevelConfigs( | ||
| MetricLevelConfig{ | ||
| MeterName: "test/meter1", | ||
| Level: MetricLevel(1), | ||
| }, | ||
| MetricLevelConfig{ | ||
| MeterName: "test/meter1", | ||
| InstrumentName: "instrument1", | ||
| Level: MetricLevel(2), | ||
| }, | ||
| MetricLevelConfig{ | ||
| MeterName: "test/meter2", | ||
| Level: MetricLevel(1), | ||
| }, | ||
| ) | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| meterName string | ||
| wantCount int | ||
| wantLevels []MetricLevel | ||
| }{ | ||
| { | ||
| name: "existing meter with multiple configs", | ||
| meterName: "test/meter1", | ||
| wantCount: 2, | ||
| wantLevels: []MetricLevel{1, 2}, | ||
| }, | ||
| { | ||
| name: "existing meter with single config", | ||
| meterName: "test/meter2", | ||
| wantCount: 1, | ||
| wantLevels: []MetricLevel{1}, | ||
| }, | ||
| { | ||
| name: "non-existent meter", | ||
| meterName: "test/nonexistent", | ||
| wantCount: 0, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| configs := RegisteredMetricLevelConfigsByMeter(tt.meterName) | ||
| require.Len(t, configs, tt.wantCount) | ||
| for i, cfg := range configs { | ||
| assert.Equal(t, tt.meterName, cfg.MeterName) | ||
| if len(tt.wantLevels) > i { | ||
| assert.Equal(t, tt.wantLevels[i], cfg.Level) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestRegisterMetricLevelConfigsEmptySlice(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| initialCount := len(RegisteredMetricLevelConfigs()) | ||
| RegisterMetricLevelConfigs() | ||
| assert.Len(t, RegisteredMetricLevelConfigs(), initialCount) | ||
| } | ||
|
|
||
| func TestRegisterMetricLevelConfigsConcurrent(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| const numGoroutines = 10 | ||
| const configsPerGoroutine = 5 | ||
|
|
||
| var wg sync.WaitGroup | ||
| wg.Add(numGoroutines) | ||
|
|
||
| for i := range numGoroutines { | ||
| go func(id int) { | ||
| defer wg.Done() | ||
| configs := make([]MetricLevelConfig, configsPerGoroutine) | ||
| for j := range configsPerGoroutine { | ||
| configs[j] = MetricLevelConfig{ | ||
| MeterName: "test/concurrent", | ||
| InstrumentName: "instrument", | ||
| Level: MetricLevel(id*configsPerGoroutine + j), | ||
| } | ||
| } | ||
| RegisterMetricLevelConfigs(configs...) | ||
| }(i) | ||
| } | ||
|
|
||
| wg.Wait() | ||
|
|
||
| allConfigs := RegisteredMetricLevelConfigs() | ||
| require.GreaterOrEqual(t, len(allConfigs), numGoroutines*configsPerGoroutine) | ||
|
|
||
| var readWg sync.WaitGroup | ||
| readWg.Add(numGoroutines) | ||
| for range numGoroutines { | ||
| go func() { | ||
| defer readWg.Done() | ||
| configs := RegisteredMetricLevelConfigs() | ||
| assert.GreaterOrEqual(t, len(configs), numGoroutines*configsPerGoroutine) | ||
| }() | ||
| } | ||
| readWg.Wait() | ||
| } | ||
|
|
||
| func TestRegisteredMetricLevelConfigsReturnsCopy(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| RegisterMetricLevelConfigs( | ||
| MetricLevelConfig{ | ||
| MeterName: "test/copy", | ||
| Level: MetricLevel(1), | ||
| }, | ||
| ) | ||
|
|
||
| configs1 := RegisteredMetricLevelConfigs() | ||
| configs2 := RegisteredMetricLevelConfigs() | ||
|
|
||
| configs1[0].MeterName = "modified" | ||
|
|
||
| assert.Equal(t, "test/copy", configs2[0].MeterName) | ||
| assert.Equal(t, "modified", configs1[0].MeterName) | ||
| } | ||
|
|
||
| func TestRegisteredMetricLevelConfigsByMeterReturnsCopy(t *testing.T) { | ||
| t.Cleanup(ResetMetricLevelRegistryForTesting) | ||
|
|
||
| RegisterMetricLevelConfigs( | ||
| MetricLevelConfig{ | ||
| MeterName: "test/copy", | ||
| Level: MetricLevel(1), | ||
| }, | ||
| ) | ||
|
|
||
| configs1 := RegisteredMetricLevelConfigsByMeter("test/copy") | ||
| configs2 := RegisteredMetricLevelConfigsByMeter("test/copy") | ||
|
|
||
| require.Len(t, configs1, 1) | ||
| require.Len(t, configs2, 1) | ||
|
|
||
| configs1[0].MeterName = "modified" | ||
|
|
||
| assert.Equal(t, "test/copy", configs2[0].MeterName) | ||
| assert.Equal(t, "modified", configs1[0].MeterName) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Copyright The OpenTelemetry Authors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package exporterhelper // import "go.opentelemetry.io/collector/exporter/exporterhelper" | ||
|
|
||
| import ( | ||
| "go.opentelemetry.io/collector/component" | ||
| "go.opentelemetry.io/collector/config/configtelemetry" | ||
| ) | ||
|
|
||
| func init() { | ||
| component.RegisterMetricLevelConfigs(component.MetricLevelConfig{ | ||
| MeterName: "go.opentelemetry.io/collector/exporter/exporterhelper", | ||
| InstrumentName: "otelcol_exporter_queue_batch_send_size_bytes", | ||
| Level: component.MetricLevel(configtelemetry.LevelDetailed), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the rest of this PR but I think this should be on an
xcomponentpackage instead of here since this would (at the beginning at least) an experimental package (there's also spec work about this, see open-telemetry/opentelemetry-specification/issues/4391)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm ok with that. Will implement the change.