From 0598dae2346d82de790fa0a5eb1ef94828cdac4d Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Fri, 6 Dec 2024 01:25:12 -0800 Subject: [PATCH] sdk/metric: Add experimental Enabled method to synchronous instruments (#6016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #6002 --------- Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> Co-authored-by: Robert PajÄ…k Co-authored-by: Damien Mathieu <42@dmathieu.com> --- CHANGELOG.md | 4 ++ sdk/metric/instrument.go | 11 +++++ sdk/metric/internal/x/README.md | 19 ++++++++ sdk/metric/internal/x/x.go | 12 +++++ sdk/metric/meter_test.go | 85 +++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 566829ae1e2..14ca27bd9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Add `Reset` method to `SpanRecorder` in `go.opentelemetry.io/otel/sdk/trace/tracetest`. (#5994) +- Add `EnabledInstrument` interface in `go.opentelemetry.io/otel/sdk/metric/internal/x`. + This is an experimental interface that is implemented by synchronous instruments provided by `go.opentelemetry.io/otel/sdk/metric`. + Users can use it to avoid performing computationally expensive operations when recording measurements. + It does not fall within the scope of the OpenTelemetry Go versioning and stability [policy](./VERSIONING.md) and it may be changed in backwards incompatible ways or removed in feature releases. (#6016) ### Changed diff --git a/sdk/metric/instrument.go b/sdk/metric/instrument.go index 48b723a7b3b..c33e1a28cb4 100644 --- a/sdk/metric/instrument.go +++ b/sdk/metric/instrument.go @@ -16,6 +16,7 @@ import ( "go.opentelemetry.io/otel/metric/embedded" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric/internal/aggregate" + "go.opentelemetry.io/otel/sdk/metric/internal/x" ) var zeroScope instrumentation.Scope @@ -190,6 +191,7 @@ var ( _ metric.Int64UpDownCounter = (*int64Inst)(nil) _ metric.Int64Histogram = (*int64Inst)(nil) _ metric.Int64Gauge = (*int64Inst)(nil) + _ x.EnabledInstrument = (*int64Inst)(nil) ) func (i *int64Inst) Add(ctx context.Context, val int64, opts ...metric.AddOption) { @@ -202,6 +204,10 @@ func (i *int64Inst) Record(ctx context.Context, val int64, opts ...metric.Record i.aggregate(ctx, val, c.Attributes()) } +func (i *int64Inst) Enabled(_ context.Context) bool { + return len(i.measures) != 0 +} + func (i *int64Inst) aggregate(ctx context.Context, val int64, s attribute.Set) { // nolint:revive // okay to shadow pkg with method. for _, in := range i.measures { in(ctx, val, s) @@ -222,6 +228,7 @@ var ( _ metric.Float64UpDownCounter = (*float64Inst)(nil) _ metric.Float64Histogram = (*float64Inst)(nil) _ metric.Float64Gauge = (*float64Inst)(nil) + _ x.EnabledInstrument = (*float64Inst)(nil) ) func (i *float64Inst) Add(ctx context.Context, val float64, opts ...metric.AddOption) { @@ -234,6 +241,10 @@ func (i *float64Inst) Record(ctx context.Context, val float64, opts ...metric.Re i.aggregate(ctx, val, c.Attributes()) } +func (i *float64Inst) Enabled(_ context.Context) bool { + return len(i.measures) != 0 +} + func (i *float64Inst) aggregate(ctx context.Context, val float64, s attribute.Set) { for _, in := range i.measures { in(ctx, val, s) diff --git a/sdk/metric/internal/x/README.md b/sdk/metric/internal/x/README.md index aba69d65471..59f736b733f 100644 --- a/sdk/metric/internal/x/README.md +++ b/sdk/metric/internal/x/README.md @@ -10,6 +10,7 @@ See the [Compatibility and Stability](#compatibility-and-stability) section for - [Cardinality Limit](#cardinality-limit) - [Exemplars](#exemplars) +- [Instrument Enabled](#instrument-enabled) ### Cardinality Limit @@ -102,6 +103,24 @@ Revert to the default exemplar filter (`"trace_based"`) unset OTEL_METRICS_EXEMPLAR_FILTER ``` +### Instrument Enabled + +To help users avoid performing computationally expensive operations when recording measurements, synchronous instruments provide an `Enabled` method. + +#### Examples + +The following code shows an example of how to check if an instrument implements the `EnabledInstrument` interface before using the `Enabled` function to avoid doing an expensive computation: + +```go +type enabledInstrument interface { Enabled(context.Context) bool } + +ctr, err := m.Int64Counter("expensive-counter") +c, ok := ctr.(enabledInstrument) +if !ok || c.Enabled(context.Background()) { + c.Add(expensiveComputation()) +} +``` + ## Compatibility and Stability Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md). diff --git a/sdk/metric/internal/x/x.go b/sdk/metric/internal/x/x.go index 08919937068..a98606238ad 100644 --- a/sdk/metric/internal/x/x.go +++ b/sdk/metric/internal/x/x.go @@ -8,6 +8,7 @@ package x // import "go.opentelemetry.io/otel/sdk/metric/internal/x" import ( + "context" "os" "strconv" ) @@ -67,3 +68,14 @@ func (f Feature[T]) Enabled() bool { _, ok := f.Lookup() return ok } + +// EnabledInstrument informs whether the instrument is enabled. +// +// EnabledInstrument interface is implemented by synchronous instruments. +type EnabledInstrument interface { + // Enabled returns whether the instrument will process measurements for the given context. + // + // This function can be used in places where measuring an instrument + // would result in computationally expensive operations. + Enabled(context.Context) bool +} diff --git a/sdk/metric/meter_test.go b/sdk/metric/meter_test.go index 3d87e58f6c3..10f473a4ca6 100644 --- a/sdk/metric/meter_test.go +++ b/sdk/metric/meter_test.go @@ -24,6 +24,7 @@ import ( "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/metric/exemplar" + "go.opentelemetry.io/otel/sdk/metric/internal/x" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" "go.opentelemetry.io/otel/sdk/resource" @@ -388,6 +389,9 @@ func TestMeterCreatesInstruments(t *testing.T) { ctr, err := m.Int64Counter("sint") assert.NoError(t, err) + c, ok := ctr.(x.EnabledInstrument) + require.True(t, ok) + assert.True(t, c.Enabled(context.Background())) ctr.Add(ctx, 3) }, want: metricdata.Metrics{ @@ -407,6 +411,9 @@ func TestMeterCreatesInstruments(t *testing.T) { ctr, err := m.Int64UpDownCounter("sint") assert.NoError(t, err) + c, ok := ctr.(x.EnabledInstrument) + require.True(t, ok) + assert.True(t, c.Enabled(context.Background())) ctr.Add(ctx, 11) }, want: metricdata.Metrics{ @@ -452,6 +459,9 @@ func TestMeterCreatesInstruments(t *testing.T) { ctr, err := m.Float64Counter("sfloat") assert.NoError(t, err) + c, ok := ctr.(x.EnabledInstrument) + require.True(t, ok) + assert.True(t, c.Enabled(context.Background())) ctr.Add(ctx, 3) }, want: metricdata.Metrics{ @@ -471,6 +481,9 @@ func TestMeterCreatesInstruments(t *testing.T) { ctr, err := m.Float64UpDownCounter("sfloat") assert.NoError(t, err) + c, ok := ctr.(x.EnabledInstrument) + require.True(t, ok) + assert.True(t, c.Enabled(context.Background())) ctr.Add(ctx, 11) }, want: metricdata.Metrics{ @@ -532,6 +545,78 @@ func TestMeterCreatesInstruments(t *testing.T) { } } +func TestMeterWithDropView(t *testing.T) { + dropView := NewView( + Instrument{Name: "*"}, + Stream{Aggregation: AggregationDrop{}}, + ) + m := NewMeterProvider(WithView(dropView)).Meter(t.Name()) + + testCases := []struct { + name string + fn func(*testing.T) (any, error) + }{ + { + name: "Int64Counter", + fn: func(*testing.T) (any, error) { + return m.Int64Counter("sint") + }, + }, + { + name: "Int64UpDownCounter", + fn: func(*testing.T) (any, error) { + return m.Int64UpDownCounter("sint") + }, + }, + { + name: "Int64Gauge", + fn: func(*testing.T) (any, error) { + return m.Int64Gauge("sint") + }, + }, + { + name: "Int64Histogram", + fn: func(*testing.T) (any, error) { + return m.Int64Histogram("histogram") + }, + }, + { + name: "Float64Counter", + fn: func(*testing.T) (any, error) { + return m.Float64Counter("sfloat") + }, + }, + { + name: "Float64UpDownCounter", + fn: func(*testing.T) (any, error) { + return m.Float64UpDownCounter("sfloat") + }, + }, + { + name: "Float64Gauge", + fn: func(*testing.T) (any, error) { + return m.Float64Gauge("sfloat") + }, + }, + { + name: "Float64Histogram", + fn: func(*testing.T) (any, error) { + return m.Float64Histogram("histogram") + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.fn(t) + require.NoError(t, err) + c, ok := got.(x.EnabledInstrument) + require.True(t, ok) + assert.False(t, c.Enabled(context.Background())) + }) + } +} + func TestMeterCreatesInstrumentsValidations(t *testing.T) { testCases := []struct { name string