diff --git a/CHANGELOG.md b/CHANGELOG.md index 237b663b65e..f004ec1d788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Add the `ReadOnlySpan` and `ReadWriteSpan` interfaces to provide better control for accessing span data. (#1360) +- `MetricsLabelsEnricher` type is added to `go.opentelemetry.io/otel/sdk/metric` package. (#1271) +- `WithMetricsLabelsEnricher` config option is added to `go.opentelemetry.io/otel/sdk/push` and `go.opentelemetry.io/otel/sdk/pull` packages to allow providing a function to enrich metrics labels based on context. (#1271) ### Changed @@ -49,6 +51,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - An `EventOption` and the related `NewEventConfig` function are added to the `go.opentelemetry.io/otel` package to configure Span events. (#1254) - A `TextMapPropagator` and associated `TextMapCarrier` are added to the `go.opentelemetry.io/otel/oteltest` package to test `TextMap` type propagators and their use. (#1259) - `SpanContextFromContext` returns `SpanContext` from context. (#1255) +- Add an opencensus to opentelemetry tracing bridge. (#1305) - `DeploymentEnvironmentKey` added to `go.opentelemetry.io/otel/semconv` package. (#1323) - Add an OpenCensus to OpenTelemetry tracing bridge. (#1305) - Add a parent context argument to `SpanProcessor.OnStart` to follow the specification. (#1333) diff --git a/internal/tools/tools.go b/internal/tools/tools.go index 306a95e0daf..c86ce2fdc90 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -18,8 +18,8 @@ package tools import ( _ "github.com/client9/misspell/cmd/misspell" + _ "github.com/gogo/protobuf/protoc-gen-gogofast" _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/itchyny/gojq" _ "golang.org/x/tools/cmd/stringer" - _ "github.com/gogo/protobuf/protoc-gen-gogofast" ) diff --git a/sdk/metric/config.go b/sdk/metric/config.go new file mode 100644 index 00000000000..e3526cbe5cb --- /dev/null +++ b/sdk/metric/config.go @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metric + +// Config contains configuration for an SDK. +type Config struct { + // If provided, MetricsLabelsEnricher is executed each time a metric is recorded + // by the Accumulator's sync instrument implementation + MetricsLabelsEnricher MetricsLabelsEnricher +} + +// Option is the interface that applies the value to a configuration option. +type Option interface { + // Apply sets the Option value of a Config. + Apply(*Config) +} + +func WithMetricsLabelsEnricher(e MetricsLabelsEnricher) Option { + return metricsLabelsEnricherOption(e) +} + +type metricsLabelsEnricherOption MetricsLabelsEnricher + +func (e metricsLabelsEnricherOption) Apply(config *Config) { + config.MetricsLabelsEnricher = MetricsLabelsEnricher(e) +} diff --git a/sdk/metric/controller/pull/config.go b/sdk/metric/controller/pull/config.go index 363285bf8c7..826c42b9858 100644 --- a/sdk/metric/controller/pull/config.go +++ b/sdk/metric/controller/pull/config.go @@ -17,6 +17,7 @@ package pull // import "go.opentelemetry.io/otel/sdk/metric/controller/pull" import ( "time" + "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" ) @@ -33,6 +34,10 @@ type Config struct { // If the period is zero, caching of the result is disabled. // The default value is 10 seconds. CachePeriod time.Duration + + // MetricsLabelsEnricher is a function that enriches metrics labels based + // on kvs stored in context when metrics are recorded. + MetricsLabelsEnricher metric.MetricsLabelsEnricher } // Option is the interface that applies the value to a configuration option. @@ -62,3 +67,13 @@ type cachePeriodOption time.Duration func (o cachePeriodOption) Apply(config *Config) { config.CachePeriod = time.Duration(o) } + +func WithMetricsLabelsEnricher(e metric.MetricsLabelsEnricher) Option { + return metricsLabelsEnricherOption(e) +} + +type metricsLabelsEnricherOption metric.MetricsLabelsEnricher + +func (e metricsLabelsEnricherOption) Apply(config *Config) { + config.MetricsLabelsEnricher = metric.MetricsLabelsEnricher(e) +} diff --git a/sdk/metric/controller/pull/pull.go b/sdk/metric/controller/pull/pull.go index 68ea34b351c..315d6a8ecee 100644 --- a/sdk/metric/controller/pull/pull.go +++ b/sdk/metric/controller/pull/pull.go @@ -61,6 +61,7 @@ func New(checkpointer export.Checkpointer, options ...Option) *Controller { accum := sdk.NewAccumulator( checkpointer, config.Resource, + sdk.WithMetricsLabelsEnricher(config.MetricsLabelsEnricher), ) return &Controller{ accumulator: accum, diff --git a/sdk/metric/controller/pull/pull_test.go b/sdk/metric/controller/pull/pull_test.go index 06988105deb..2450dae5f1d 100644 --- a/sdk/metric/controller/pull/pull_test.go +++ b/sdk/metric/controller/pull/pull_test.go @@ -20,6 +20,8 @@ import ( "testing" "time" + "go.opentelemetry.io/otel/baggage" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/label" @@ -117,3 +119,35 @@ func TestPullWithCache(t *testing.T) { }, records.Map()) } + +func TestPullWithMetricsLabelEnricher(t *testing.T) { + metricsLabelsEnricher := func(ctx context.Context, kvs []label.KeyValue) ([]label.KeyValue, error) { + baggage := baggage.Set(ctx) + kvs = append(baggage.ToSlice(), kvs...) + return kvs, nil + } + + puller := pull.New( + basic.New( + selector.NewWithExactDistribution(), + export.CumulativeExportKindSelector(), + basic.WithMemory(true), + ), + pull.WithCachePeriod(0), + pull.WithMetricsLabelsEnricher(metricsLabelsEnricher), + ) + + ctx := baggage.ContextWithValues(context.Background(), label.String("A", "B")) + meter := puller.MeterProvider().Meter("withLabelEnricher") + counter := metric.Must(meter).NewInt64Counter("counter.sum") + + counter.Add(ctx, 10) + + require.NoError(t, puller.Collect(context.Background())) + records := processortest.NewOutput(label.DefaultEncoder()) + require.NoError(t, puller.ForEach(export.CumulativeExportKindSelector(), records.AddRecord)) + + require.EqualValues(t, map[string]float64{ + "counter.sum/A=B/": 10, + }, records.Map()) +} diff --git a/sdk/metric/controller/push/config.go b/sdk/metric/controller/push/config.go index d708780d685..5abb694b366 100644 --- a/sdk/metric/controller/push/config.go +++ b/sdk/metric/controller/push/config.go @@ -17,6 +17,7 @@ package push // import "go.opentelemetry.io/otel/sdk/metric/controller/push" import ( "time" + "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" ) @@ -33,6 +34,10 @@ type Config struct { // integrate, and export) can last before it is canceled. Defaults to // the controller push period. Timeout time.Duration + + // MetricsLabelsEnricher is a function that enriches metrics labels based + // on kvs stored in context when metrics are recorded. + MetricsLabelsEnricher metric.MetricsLabelsEnricher } // Option is the interface that applies the value to a configuration option. @@ -73,3 +78,13 @@ type timeoutOption time.Duration func (o timeoutOption) Apply(config *Config) { config.Timeout = time.Duration(o) } + +func WithMetricsLabelsEnricher(e metric.MetricsLabelsEnricher) Option { + return metricsLabelsEnricherOption(e) +} + +type metricsLabelsEnricherOption metric.MetricsLabelsEnricher + +func (e metricsLabelsEnricherOption) Apply(config *Config) { + config.MetricsLabelsEnricher = metric.MetricsLabelsEnricher(e) +} diff --git a/sdk/metric/controller/push/push.go b/sdk/metric/controller/push/push.go index 4350de32de5..0b7b15267b2 100644 --- a/sdk/metric/controller/push/push.go +++ b/sdk/metric/controller/push/push.go @@ -62,6 +62,7 @@ func New(checkpointer export.Checkpointer, exporter export.Exporter, opts ...Opt impl := sdk.NewAccumulator( checkpointer, c.Resource, + sdk.WithMetricsLabelsEnricher(c.MetricsLabelsEnricher), ) return &Controller{ provider: registry.NewMeterProvider(impl), diff --git a/sdk/metric/controller/push/push_test.go b/sdk/metric/controller/push/push_test.go index 17c21f29703..961d2376487 100644 --- a/sdk/metric/controller/push/push_test.go +++ b/sdk/metric/controller/push/push_test.go @@ -22,6 +22,8 @@ import ( "testing" "time" + "go.opentelemetry.io/otel/baggage" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" @@ -223,3 +225,39 @@ func TestPushExportError(t *testing.T) { }) } } + +func TestWithMetricsLabelsEnricher(t *testing.T) { + exporter := newExporter() + checkpointer := newCheckpointer() + metricsLabelsEnricher := func(ctx context.Context, kvs []label.KeyValue) ([]label.KeyValue, error) { + baggage := baggage.Set(ctx) + kvs = append(baggage.ToSlice(), kvs...) + return kvs, nil + } + p := push.New( + checkpointer, + exporter, + push.WithPeriod(time.Second), + push.WithMetricsLabelsEnricher(metricsLabelsEnricher), + ) + meter := p.MeterProvider().Meter("name") + + mock := controllertest.NewMockClock() + p.SetClock(mock) + + counter := metric.Must(meter).NewInt64Counter("counter.sum") + + p.Start() + + ctx := baggage.ContextWithValues(context.Background(), label.String("A", "B")) + counter.Add(ctx, 1) + + require.EqualValues(t, map[string]float64{}, exporter.Values()) + + mock.Add(time.Second) + runtime.Gosched() + + require.EqualValues(t, map[string]float64{ + "counter.sum/A=B/": 1, + }, exporter.Values()) +} diff --git a/sdk/metric/sdk.go b/sdk/metric/sdk.go index c1f68b24fe7..efab61fcbe9 100644 --- a/sdk/metric/sdk.go +++ b/sdk/metric/sdk.go @@ -67,6 +67,9 @@ type ( // resource is applied to all records in this Accumulator. resource *resource.Resource + + // metricsLabelsEnricher is applied to all records in this Accumulator + metricsLabelsEnricher MetricsLabelsEnricher } syncInstrument struct { @@ -139,6 +142,10 @@ type ( labels *label.Set observed export.Aggregator } + + // MetricsLabelsEnricher can be provided as a config option to enrich metrics labels based on + // the context when the metrics are recorded + MetricsLabelsEnricher func(context.Context, []label.KeyValue) ([]label.KeyValue, error) ) var ( @@ -287,6 +294,16 @@ func (s *syncInstrument) Bind(kvs []label.KeyValue) metric.BoundSyncImpl { } func (s *syncInstrument) RecordOne(ctx context.Context, num number.Number, kvs []label.KeyValue) { + + if s.meter.metricsLabelsEnricher != nil { + var err error + kvs, err = s.meter.metricsLabelsEnricher(ctx, kvs) + if err != nil { + otel.Handle(err) + return + } + } + h := s.acquireHandle(kvs, nil) defer h.Unbind() h.RecordOne(ctx, num) @@ -301,11 +318,17 @@ func (s *syncInstrument) RecordOne(ctx context.Context, num number.Number, kvs [ // processor will call Collect() when it receives a request to scrape // current metric values. A push-based processor should configure its // own periodic collection. -func NewAccumulator(processor export.Processor, resource *resource.Resource) *Accumulator { +func NewAccumulator(processor export.Processor, resource *resource.Resource, opts ...Option) *Accumulator { + c := &Config{} + for _, opt := range opts { + opt.Apply(c) + } + return &Accumulator{ - processor: processor, - asyncInstruments: internal.NewAsyncInstrumentState(), - resource: resource, + processor: processor, + asyncInstruments: internal.NewAsyncInstrumentState(), + resource: resource, + metricsLabelsEnricher: c.MetricsLabelsEnricher, } }