From d8ebf61944bb1334e67e6b29f827783fe184f7c5 Mon Sep 17 00:00:00 2001 From: Bogdan Drutu Date: Thu, 23 May 2024 06:52:23 -0700 Subject: [PATCH] Add support to configure views with config.NewSdk --- CHANGELOG.md | 1 + config/metric.go | 162 ++++++++++++++- config/metric_test.go | 456 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 618 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36eff57b763..3ded41b68ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - The `go.opentelemetry.io/contrib/config` add support to configure periodic reader interval and timeout. (#5661) +- Add support to configure views when creating MeterProvider using the config package. (#5654) ### Fixed diff --git a/config/metric.go b/config/metric.go index 4763406b529..bbe13fe8e8c 100644 --- a/config/metric.go +++ b/config/metric.go @@ -18,16 +18,22 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" otelprom "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/sdk/instrumentation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" ) +var zeroScope instrumentation.Scope + +const instrumentKindUndefined = sdkmetric.InstrumentKind(0) + func meterProvider(cfg configOptions, res *resource.Resource) (metric.MeterProvider, shutdownFunc, error) { if cfg.opentelemetryConfig.MeterProvider == nil { return noop.NewMeterProvider(), noopShutdown, nil @@ -45,6 +51,15 @@ func meterProvider(cfg configOptions, res *resource.Resource) (metric.MeterProvi errs = append(errs, err) } } + for _, vw := range cfg.opentelemetryConfig.MeterProvider.Views { + v, err := view(vw) + if err == nil { + opts = append(opts, sdkmetric.WithView(v)) + } else { + errs = append(errs, err) + } + } + if len(errs) > 0 { return noop.NewMeterProvider(), noopShutdown, errors.Join(errs...) } @@ -156,7 +171,7 @@ func otlpHTTPMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmet } func otlpGRPCMetricExporter(ctx context.Context, otlpConfig *OTLPMetric) (sdkmetric.Exporter, error) { - opts := []otlpmetricgrpc.Option{} + var opts []otlpmetricgrpc.Option if len(otlpConfig.Endpoint) > 0 { u, err := url.ParseRequestURI(otlpConfig.Endpoint) @@ -266,3 +281,148 @@ func (rws readerWithServer) Shutdown(ctx context.Context) error { rws.server.Shutdown(ctx), ) } + +func view(v View) (sdkmetric.View, error) { + if v.Selector == nil { + return nil, errors.New("view: no selector provided") + } + + inst, err := instrument(*v.Selector) + if err != nil { + return nil, err + } + + return sdkmetric.NewView(inst, stream(v.Stream)), nil +} + +func instrument(vs ViewSelector) (sdkmetric.Instrument, error) { + kind, err := instrumentKind(vs.InstrumentType) + if err != nil { + return sdkmetric.Instrument{}, fmt.Errorf("view_selector: %w", err) + } + inst := sdkmetric.Instrument{ + Name: strOrEmpty(vs.InstrumentName), + Unit: strOrEmpty(vs.Unit), + Kind: kind, + Scope: instrumentation.Scope{ + Name: strOrEmpty(vs.MeterName), + Version: strOrEmpty(vs.MeterVersion), + SchemaURL: strOrEmpty(vs.MeterSchemaUrl), + }, + } + + if instrumentIsEmpty(inst) { + return sdkmetric.Instrument{}, errors.New("view_selector: empty selector not supporter") + } + return inst, nil +} + +func stream(vs *ViewStream) sdkmetric.Stream { + if vs == nil { + return sdkmetric.Stream{} + } + + return sdkmetric.Stream{ + Name: strOrEmpty(vs.Name), + Description: strOrEmpty(vs.Description), + Aggregation: aggregation(vs.Aggregation), + AttributeFilter: attributeFilter(vs.AttributeKeys), + } +} + +func attributeFilter(attributeKeys []string) attribute.Filter { + var attrKeys []attribute.Key + for _, attrStr := range attributeKeys { + attrKeys = append(attrKeys, attribute.Key(attrStr)) + } + return attribute.NewAllowKeysFilter(attrKeys...) +} + +func aggregation(aggr *ViewStreamAggregation) sdkmetric.Aggregation { + if aggr == nil { + return nil + } + + if aggr.Base2ExponentialBucketHistogram != nil { + return sdkmetric.AggregationBase2ExponentialHistogram{ + MaxSize: int32(intOrZero(aggr.Base2ExponentialBucketHistogram.MaxSize)), + MaxScale: int32(intOrZero(aggr.Base2ExponentialBucketHistogram.MaxScale)), + // Need to negate because config has the positive action RecordMinMax. + NoMinMax: !boolOrFalse(aggr.Base2ExponentialBucketHistogram.RecordMinMax), + } + } + if aggr.Default != nil { + // TODO: Understand what to set here. + return nil + } + if aggr.Drop != nil { + return sdkmetric.AggregationDrop{} + } + if aggr.ExplicitBucketHistogram != nil { + return sdkmetric.AggregationExplicitBucketHistogram{ + Boundaries: aggr.ExplicitBucketHistogram.Boundaries, + // Need to negate because config has the positive action RecordMinMax. + NoMinMax: !boolOrFalse(aggr.ExplicitBucketHistogram.RecordMinMax), + } + } + if aggr.LastValue != nil { + return sdkmetric.AggregationLastValue{} + } + if aggr.Sum != nil { + return sdkmetric.AggregationSum{} + } + return nil +} + +func instrumentKind(vsit *ViewSelectorInstrumentType) (sdkmetric.InstrumentKind, error) { + if vsit == nil { + // Equivalent to instrumentKindUndefined. + return instrumentKindUndefined, nil + } + + switch *vsit { + case ViewSelectorInstrumentTypeCounter: + return sdkmetric.InstrumentKindCounter, nil + case ViewSelectorInstrumentTypeUpDownCounter: + return sdkmetric.InstrumentKindUpDownCounter, nil + case ViewSelectorInstrumentTypeHistogram: + return sdkmetric.InstrumentKindHistogram, nil + case ViewSelectorInstrumentTypeObservableCounter: + return sdkmetric.InstrumentKindObservableCounter, nil + case ViewSelectorInstrumentTypeObservableUpDownCounter: + return sdkmetric.InstrumentKindObservableUpDownCounter, nil + case ViewSelectorInstrumentTypeObservableGauge: + return sdkmetric.InstrumentKindObservableGauge, nil + } + + return instrumentKindUndefined, errors.New("instrument_type: invalid value") +} + +func instrumentIsEmpty(i sdkmetric.Instrument) bool { + return i.Name == "" && + i.Description == "" && + i.Kind == instrumentKindUndefined && + i.Unit == "" && + i.Scope == zeroScope +} + +func boolOrFalse(pBool *bool) bool { + if pBool == nil { + return false + } + return *pBool +} + +func intOrZero(pInt *int) int { + if pInt == nil { + return 0 + } + return *pInt +} + +func strOrEmpty(pStr *string) string { + if pStr == nil { + return "" + } + return *pStr +} diff --git a/config/metric_test.go b/config/metric_test.go index 7fc0fabc6e5..83aa7a8beb0 100644 --- a/config/metric_test.go +++ b/config/metric_test.go @@ -14,12 +14,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" otelprom "go.opentelemetry.io/otel/exporters/prometheus" "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" + "go.opentelemetry.io/otel/sdk/instrumentation" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" ) @@ -495,6 +497,460 @@ func TestReader(t *testing.T) { } } +func TestView(t *testing.T) { + testCases := []struct { + name string + view View + args any + wantErr string + matchInstrument *sdkmetric.Instrument + wantStream sdkmetric.Stream + wantResult bool + }{ + { + name: "no selector", + wantErr: "view: no selector provided", + }, + { + name: "selector/invalid_type", + view: View{ + Selector: &ViewSelector{ + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("invalid_type")), + }, + }, + wantErr: "view_selector: instrument_type: invalid value", + }, + { + name: "selector/invalid_type", + view: View{ + Selector: &ViewSelector{}, + }, + wantErr: "view_selector: empty selector not supporter", + }, + { + name: "all selectors match", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "test_meter_version", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{Name: "test_name", Unit: "test_unit"}, + wantResult: true, + }, + { + name: "all selectors no match name", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "not_match", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "test_meter_version", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "all selectors no match unit", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "not_match", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "test_meter_version", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "all selectors no match kind", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("histogram")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "test_meter_version", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "all selectors no match meter name", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "not_match", + Version: "test_meter_version", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "all selectors no match meter version", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "not_match", + SchemaURL: "test_schema_url", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "all selectors no match meter schema url", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + InstrumentType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + Unit: newStringPtr("test_unit"), + MeterName: newStringPtr("test_meter_name"), + MeterVersion: newStringPtr("test_meter_version"), + MeterSchemaUrl: newStringPtr("test_schema_url"), + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Unit: "test_unit", + Kind: sdkmetric.InstrumentKindCounter, + Scope: instrumentation.Scope{ + Name: "test_meter_name", + Version: "test_meter_version", + SchemaURL: "not_match", + }, + }, + wantStream: sdkmetric.Stream{}, + wantResult: false, + }, + { + name: "with stream", + view: View{ + Selector: &ViewSelector{ + InstrumentName: newStringPtr("test_name"), + Unit: newStringPtr("test_unit"), + }, + Stream: &ViewStream{ + Name: newStringPtr("new_name"), + Description: newStringPtr("new_description"), + AttributeKeys: []string{"foo", "bar"}, + Aggregation: &ViewStreamAggregation{Sum: make(ViewStreamAggregationSum)}, + }, + }, + matchInstrument: &sdkmetric.Instrument{ + Name: "test_name", + Description: "test_description", + Unit: "test_unit", + }, + wantStream: sdkmetric.Stream{ + Name: "new_name", + Description: "new_description", + Unit: "test_unit", + Aggregation: sdkmetric.AggregationSum{}, + }, + wantResult: true, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, err := view(tt.view) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, got) + } else { + require.NoError(t, err) + gotStream, gotResult := got(*tt.matchInstrument) + // Remove filter, since it cannot be compared + gotStream.AttributeFilter = nil + require.Equal(t, tt.wantStream, gotStream) + require.Equal(t, tt.wantResult, gotResult) + } + }) + } +} + +func TestInstrumentType(t *testing.T) { + testCases := []struct { + name string + instType *ViewSelectorInstrumentType + wantErr error + wantKind sdkmetric.InstrumentKind + }{ + { + name: "nil", + wantKind: sdkmetric.InstrumentKind(0), + }, + { + name: "counter", + instType: (*ViewSelectorInstrumentType)(newStringPtr("counter")), + wantKind: sdkmetric.InstrumentKindCounter, + }, + { + name: "up_down_counter", + instType: (*ViewSelectorInstrumentType)(newStringPtr("up_down_counter")), + wantKind: sdkmetric.InstrumentKindUpDownCounter, + }, + { + name: "histogram", + instType: (*ViewSelectorInstrumentType)(newStringPtr("histogram")), + wantKind: sdkmetric.InstrumentKindHistogram, + }, + { + name: "observable_counter", + instType: (*ViewSelectorInstrumentType)(newStringPtr("observable_counter")), + wantKind: sdkmetric.InstrumentKindObservableCounter, + }, + { + name: "observable_up_down_counter", + instType: (*ViewSelectorInstrumentType)(newStringPtr("observable_up_down_counter")), + wantKind: sdkmetric.InstrumentKindObservableUpDownCounter, + }, + { + name: "observable_gauge", + instType: (*ViewSelectorInstrumentType)(newStringPtr("observable_gauge")), + wantKind: sdkmetric.InstrumentKindObservableGauge, + }, + { + name: "invalid", + instType: (*ViewSelectorInstrumentType)(newStringPtr("invalid")), + wantErr: errors.New("instrument_type: invalid value"), + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, err := instrumentKind(tt.instType) + if tt.wantErr != nil { + require.Equal(t, err, tt.wantErr) + require.Zero(t, got) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantKind, got) + } + }) + } +} + +func TestAggregation(t *testing.T) { + testCases := []struct { + name string + aggregation *ViewStreamAggregation + wantAggregation sdkmetric.Aggregation + }{ + { + name: "nil", + wantAggregation: nil, + }, + { + name: "empty", + aggregation: &ViewStreamAggregation{}, + wantAggregation: nil, + }, + { + name: "Base2ExponentialBucketHistogram empty", + aggregation: &ViewStreamAggregation{ + Base2ExponentialBucketHistogram: &ViewStreamAggregationBase2ExponentialBucketHistogram{}, + }, + wantAggregation: sdkmetric.AggregationBase2ExponentialHistogram{ + MaxSize: 0, + MaxScale: 0, + NoMinMax: true, + }, + }, + { + name: "Base2ExponentialBucketHistogram", + aggregation: &ViewStreamAggregation{ + Base2ExponentialBucketHistogram: &ViewStreamAggregationBase2ExponentialBucketHistogram{ + MaxSize: newIntPtr(2), + MaxScale: newIntPtr(3), + RecordMinMax: newBoolPtr(true), + }, + }, + wantAggregation: sdkmetric.AggregationBase2ExponentialHistogram{ + MaxSize: 2, + MaxScale: 3, + NoMinMax: false, + }, + }, + { + name: "Default", + aggregation: &ViewStreamAggregation{ + Default: make(ViewStreamAggregationDefault), + }, + wantAggregation: nil, + }, + { + name: "Drop", + aggregation: &ViewStreamAggregation{ + Drop: make(ViewStreamAggregationDrop), + }, + wantAggregation: sdkmetric.AggregationDrop{}, + }, + { + name: "ExplicitBucketHistogram empty", + aggregation: &ViewStreamAggregation{ + ExplicitBucketHistogram: &ViewStreamAggregationExplicitBucketHistogram{}, + }, + wantAggregation: sdkmetric.AggregationExplicitBucketHistogram{ + Boundaries: nil, + NoMinMax: true, + }, + }, + { + name: "ExplicitBucketHistogram", + aggregation: &ViewStreamAggregation{ + ExplicitBucketHistogram: &ViewStreamAggregationExplicitBucketHistogram{ + Boundaries: []float64{1, 2, 3}, + RecordMinMax: newBoolPtr(true), + }, + }, + wantAggregation: sdkmetric.AggregationExplicitBucketHistogram{ + Boundaries: []float64{1, 2, 3}, + NoMinMax: false, + }, + }, + { + name: "LastValue", + aggregation: &ViewStreamAggregation{ + LastValue: make(ViewStreamAggregationLastValue), + }, + wantAggregation: sdkmetric.AggregationLastValue{}, + }, + { + name: "Sum", + aggregation: &ViewStreamAggregation{ + Sum: make(ViewStreamAggregationSum), + }, + wantAggregation: sdkmetric.AggregationSum{}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := aggregation(tt.aggregation) + require.Equal(t, tt.wantAggregation, got) + }) + } +} + +func TestAttributeFilter(t *testing.T) { + testCases := []struct { + name string + attributeKeys []string + wantPass []string + wantFail []string + }{ + { + name: "empty", + attributeKeys: []string{}, + wantPass: nil, + wantFail: []string{"foo", "bar"}, + }, + { + name: "filter", + attributeKeys: []string{"foo"}, + wantPass: []string{"foo"}, + wantFail: []string{"bar"}, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := attributeFilter(tt.attributeKeys) + for _, pass := range tt.wantPass { + require.True(t, got(attribute.KeyValue{Key: attribute.Key(pass), Value: attribute.StringValue("")})) + } + for _, fail := range tt.wantFail { + require.False(t, got(attribute.KeyValue{Key: attribute.Key(fail), Value: attribute.StringValue("")})) + } + }) + } +} + +func newBoolPtr(b bool) *bool { + return &b +} + func newIntPtr(i int) *int { return &i } + +func newStringPtr(str string) *string { + return &str +}