diff --git a/api/metric/api.go b/api/metric/api.go index 949401694..63dd3f286 100644 --- a/api/metric/api.go +++ b/api/metric/api.go @@ -21,6 +21,7 @@ import ( "go.opentelemetry.io/otel/api/core" "go.opentelemetry.io/otel/api/unit" + "go.opentelemetry.io/otel/sdk/resource" ) // Provider supports named Meter instances. @@ -45,6 +46,8 @@ type Config struct { // Keys are recommended keys determined in the handles // obtained for the metric. Keys []core.Key + // Resource describes the entity for which measurements are made. + Resource resource.Resource } // Option is an interface for applying metric options. @@ -141,6 +144,12 @@ func (d Descriptor) NumberKind() core.NumberKind { return d.numberKind } +// Resource returns the Resource describing the entity for which the metric +// instrument measures. +func (d Descriptor) Resource() resource.Resource { + return d.config.Resource +} + // Meter is an interface to the metrics portion of the OpenTelemetry SDK. type Meter interface { // Labels returns a reference to a set of labels that cannot @@ -212,3 +221,16 @@ type keysOption []core.Key func (k keysOption) Apply(config *Config) { config.Keys = append(config.Keys, k...) } + +// WithResource applies provided Resource. +// +// This will override any existing Resource. +func WithResource(r resource.Resource) Option { + return resourceOption(r) +} + +type resourceOption resource.Resource + +func (r resourceOption) Apply(config *Config) { + config.Resource = resource.Resource(r) +} diff --git a/api/metric/api_test.go b/api/metric/api_test.go index 4f0e44451..aab513e1d 100644 --- a/api/metric/api_test.go +++ b/api/metric/api_test.go @@ -25,6 +25,7 @@ import ( "go.opentelemetry.io/otel/api/metric" "go.opentelemetry.io/otel/api/unit" mockTest "go.opentelemetry.io/otel/internal/metric" + "go.opentelemetry.io/otel/sdk/resource" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" @@ -35,19 +36,21 @@ var Must = metric.Must func TestOptions(t *testing.T) { type testcase struct { - name string - opts []metric.Option - keys []core.Key - desc string - unit unit.Unit + name string + opts []metric.Option + keys []core.Key + desc string + unit unit.Unit + resource resource.Resource } testcases := []testcase{ { - name: "no opts", - opts: nil, - keys: nil, - desc: "", - unit: "", + name: "no opts", + opts: nil, + keys: nil, + desc: "", + unit: "", + resource: resource.Resource{}, }, { name: "keys keys keys", @@ -61,17 +64,19 @@ func TestOptions(t *testing.T) { key.New("bar"), key.New("bar2"), key.New("baz"), key.New("baz2"), }, - desc: "", - unit: "", + desc: "", + unit: "", + resource: resource.Resource{}, }, { name: "description", opts: []metric.Option{ metric.WithDescription("stuff"), }, - keys: nil, - desc: "stuff", - unit: "", + keys: nil, + desc: "stuff", + unit: "", + resource: resource.Resource{}, }, { name: "description override", @@ -79,18 +84,20 @@ func TestOptions(t *testing.T) { metric.WithDescription("stuff"), metric.WithDescription("things"), }, - keys: nil, - desc: "things", - unit: "", + keys: nil, + desc: "things", + unit: "", + resource: resource.Resource{}, }, { name: "unit", opts: []metric.Option{ metric.WithUnit("s"), }, - keys: nil, - desc: "", - unit: "s", + keys: nil, + desc: "", + unit: "s", + resource: resource.Resource{}, }, { name: "unit override", @@ -98,9 +105,20 @@ func TestOptions(t *testing.T) { metric.WithUnit("s"), metric.WithUnit("h"), }, - keys: nil, - desc: "", - unit: "h", + keys: nil, + desc: "", + unit: "h", + resource: resource.Resource{}, + }, + { + name: "resource override", + opts: []metric.Option{ + metric.WithResource(*resource.New(key.New("name").String("test-name"))), + }, + keys: nil, + desc: "", + unit: "", + resource: *resource.New(key.New("name").String("test-name")), }, } for idx, tt := range testcases { @@ -109,6 +127,7 @@ func TestOptions(t *testing.T) { Description: tt.desc, Unit: tt.unit, Keys: tt.keys, + Resource: tt.resource, }); diff != "" { t.Errorf("Compare options: -got +want %s", diff) } diff --git a/api/metric/sdkhelpers.go b/api/metric/sdkhelpers.go index 72167830d..201860f2f 100644 --- a/api/metric/sdkhelpers.go +++ b/api/metric/sdkhelpers.go @@ -18,6 +18,7 @@ import ( "context" "go.opentelemetry.io/otel/api/core" + "go.opentelemetry.io/otel/sdk/resource" ) // MeterImpl is a convenient interface for SDK and test @@ -133,6 +134,29 @@ func Configure(opts []Option) Config { return config } +// Resourcer is implemented by any value that has a Resource method, +// which returns the Resource associated with the value. +// The Resource method is used to set the Resource for Descriptors of new +// metric instruments. +type Resourcer interface { + Resource() resource.Resource +} + +// insertResource inserts a WithResource option at the beginning of opts +// using the resource defined by impl if impl implements Resourcer. +// +// If opts contains a WithResource option already, that Option will take +// precedence and overwrite the Resource set from impl. +// +// The returned []Option may uses the same underlying array as opts. +func insertResource(impl MeterImpl, opts []Option) []Option { + if r, ok := impl.(Resourcer); ok { + // default to the impl resource and override if passed in opts. + return append([]Option{WithResource(r.Resource())}, opts...) + } + return opts +} + // WrapMeterImpl constructs a `Meter` implementation from a // `MeterImpl` implementation. func WrapMeterImpl(impl MeterImpl) Meter { @@ -159,6 +183,7 @@ func (m *wrappedMeterImpl) RecordBatch(ctx context.Context, ls LabelSet, ms ...M } func (m *wrappedMeterImpl) newSync(name string, metricKind Kind, numberKind core.NumberKind, opts []Option) (SyncImpl, error) { + opts = insertResource(m.impl, opts) return m.impl.NewSyncInstrument(NewDescriptor(name, metricKind, numberKind, opts...)) } @@ -219,6 +244,7 @@ func WrapFloat64MeasureInstrument(syncInst SyncImpl, err error) (Float64Measure, } func (m *wrappedMeterImpl) newAsync(name string, mkind Kind, nkind core.NumberKind, opts []Option, callback func(func(core.Number, LabelSet))) (AsyncImpl, error) { + opts = insertResource(m.impl, opts) return m.impl.NewAsyncInstrument( NewDescriptor(name, mkind, nkind, opts...), callback) diff --git a/sdk/export/metric/metric.go b/sdk/export/metric/metric.go index 895fb95bb..90e73e69d 100644 --- a/sdk/export/metric/metric.go +++ b/sdk/export/metric/metric.go @@ -1,4 +1,4 @@ -// Copyright 2019, OpenTelemetry Authors +// Copyright 2020, OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/sdk/metric/config.go b/sdk/metric/config.go new file mode 100644 index 000000000..d91a5ab45 --- /dev/null +++ b/sdk/metric/config.go @@ -0,0 +1,44 @@ +package metric + +import "go.opentelemetry.io/otel/sdk/resource" + +// Config contains configuration for an SDK. +type Config struct { + // ErrorHandler is the function called when the SDK encounters an error. + // + // This option can be overridden after instantiation of the SDK + // with the `SetErrorHandler` method. + ErrorHandler ErrorHandler + + // Resource is the OpenTelemetry resource associated with all Meters + // created by the SDK. + Resource resource.Resource +} + +// 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) +} + +// WithErrorHandler sets the ErrorHandler configuration option of a Config. +func WithErrorHandler(fn ErrorHandler) Option { + return errorHandlerOption(fn) +} + +type errorHandlerOption ErrorHandler + +func (o errorHandlerOption) Apply(config *Config) { + config.ErrorHandler = ErrorHandler(o) +} + +// WithResource sets the Resource configuration option of a Config. +func WithResource(r resource.Resource) Option { + return resourceOption(r) +} + +type resourceOption resource.Resource + +func (o resourceOption) Apply(config *Config) { + config.Resource = resource.Resource(o) +} diff --git a/sdk/metric/config_test.go b/sdk/metric/config_test.go new file mode 100644 index 000000000..2ab9324cc --- /dev/null +++ b/sdk/metric/config_test.go @@ -0,0 +1,47 @@ +package metric + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/api/core" + "go.opentelemetry.io/otel/sdk/resource" +) + +func TestWithErrorHandler(t *testing.T) { + errH, reg := func() (ErrorHandler, *error) { + e := fmt.Errorf("default invalid") + reg := &e + return func(err error) { + *reg = err + }, reg + }() + + c := &Config{} + WithErrorHandler(errH).Apply(c) + err1 := fmt.Errorf("error 1") + c.ErrorHandler(err1) + assert.EqualError(t, *reg, err1.Error()) + + // Ensure overwriting works. + c = &Config{ErrorHandler: DefaultErrorHandler} + WithErrorHandler(errH).Apply(c) + err2 := fmt.Errorf("error 2") + c.ErrorHandler(err2) + assert.EqualError(t, *reg, err2.Error()) +} + +func TestWithResource(t *testing.T) { + r := resource.New(core.Key("A").String("a")) + + c := &Config{} + WithResource(*r).Apply(c) + assert.Equal(t, *r, c.Resource) + + // Ensure overwriting works. + c = &Config{Resource: resource.Resource{}} + WithResource(*r).Apply(c) + assert.Equal(t, *r, c.Resource) +} diff --git a/sdk/metric/controller/push/config.go b/sdk/metric/controller/push/config.go new file mode 100644 index 000000000..e3a254fca --- /dev/null +++ b/sdk/metric/controller/push/config.go @@ -0,0 +1,47 @@ +package push + +import ( + sdk "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" +) + +// Config contains configuration for a push Controller. +type Config struct { + // ErrorHandler is the function called when the Controller encounters an error. + // + // This option can be overridden after instantiation of the Controller + // with the `SetErrorHandler` method. + ErrorHandler sdk.ErrorHandler + + // Resource is the OpenTelemetry resource associated with all Meters + // created by the Controller. + Resource resource.Resource +} + +// 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) +} + +// WithErrorHandler sets the ErrorHandler configuration option of a Config. +func WithErrorHandler(fn sdk.ErrorHandler) Option { + return errorHandlerOption(fn) +} + +type errorHandlerOption sdk.ErrorHandler + +func (o errorHandlerOption) Apply(config *Config) { + config.ErrorHandler = sdk.ErrorHandler(o) +} + +// WithResource sets the Resource configuration option of a Config. +func WithResource(r resource.Resource) Option { + return resourceOption(r) +} + +type resourceOption resource.Resource + +func (o resourceOption) Apply(config *Config) { + config.Resource = resource.Resource(o) +} diff --git a/sdk/metric/controller/push/config_test.go b/sdk/metric/controller/push/config_test.go new file mode 100644 index 000000000..90e38211c --- /dev/null +++ b/sdk/metric/controller/push/config_test.go @@ -0,0 +1,48 @@ +package push + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/api/core" + sdk "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" +) + +func TestWithErrorHandler(t *testing.T) { + errH, reg := func() (sdk.ErrorHandler, *error) { + e := fmt.Errorf("default invalid") + reg := &e + return func(err error) { + *reg = err + }, reg + }() + + c := &Config{} + WithErrorHandler(errH).Apply(c) + err1 := fmt.Errorf("error 1") + c.ErrorHandler(err1) + assert.EqualError(t, *reg, err1.Error()) + + // Ensure overwriting works. + c = &Config{ErrorHandler: sdk.DefaultErrorHandler} + WithErrorHandler(errH).Apply(c) + err2 := fmt.Errorf("error 2") + c.ErrorHandler(err2) + assert.EqualError(t, *reg, err2.Error()) +} + +func TestWithResource(t *testing.T) { + r := resource.New(core.Key("A").String("a")) + + c := &Config{} + WithResource(*r).Apply(c) + assert.Equal(t, *r, c.Resource) + + // Ensure overwriting works. + c = &Config{Resource: resource.Resource{}} + WithResource(*r).Apply(c) + assert.Equal(t, *r, c.Resource) +} diff --git a/sdk/metric/controller/push/push.go b/sdk/metric/controller/push/push.go index 7cc0ce255..641a16914 100644 --- a/sdk/metric/controller/push/push.go +++ b/sdk/metric/controller/push/push.go @@ -66,26 +66,31 @@ var _ Clock = realClock{} var _ Ticker = realTicker{} // New constructs a Controller, an implementation of metric.Provider, -// using the provided batcher, exporter, and collection period to -// configure an SDK with periodic collection. The batcher itself is -// configured with the aggregation selector policy. +// using the provided batcher, exporter, collection period, and SDK +// configuration options to configure an SDK with periodic collection. +// The batcher itself is configured with the aggregation selector policy. // // If the Exporter implements the export.LabelEncoder interface, the // exporter will be used as the label encoder for the SDK itself, // otherwise the SDK will be configured with the default label // encoder. -func New(batcher export.Batcher, exporter export.Exporter, period time.Duration) *Controller { +func New(batcher export.Batcher, exporter export.Exporter, period time.Duration, opts ...Option) *Controller { lencoder, _ := exporter.(export.LabelEncoder) if lencoder == nil { lencoder = sdk.NewDefaultLabelEncoder() } - impl := sdk.New(batcher, lencoder) + c := &Config{ErrorHandler: sdk.DefaultErrorHandler} + for _, opt := range opts { + opt.Apply(c) + } + + impl := sdk.New(batcher, lencoder, sdk.WithResource(c.Resource), sdk.WithErrorHandler(c.ErrorHandler)) return &Controller{ sdk: impl, meter: metric.WrapMeterImpl(impl), - errorHandler: sdk.DefaultErrorHandler, + errorHandler: c.ErrorHandler, batcher: batcher, exporter: exporter, ch: make(chan struct{}), diff --git a/sdk/metric/sdk.go b/sdk/metric/sdk.go index aa0824c2b..bfab72472 100644 --- a/sdk/metric/sdk.go +++ b/sdk/metric/sdk.go @@ -29,6 +29,7 @@ import ( api "go.opentelemetry.io/otel/api/metric" export "go.opentelemetry.io/otel/sdk/export/metric" "go.opentelemetry.io/otel/sdk/export/metric/aggregator" + "go.opentelemetry.io/otel/sdk/resource" ) type ( @@ -67,6 +68,9 @@ type ( // errorHandler supports delivering errors to the user. errorHandler ErrorHandler + + // resource represents the entity producing telemetry. + resource resource.Resource } syncInstrument struct { @@ -159,6 +163,7 @@ var ( _ api.AsyncImpl = &asyncInstrument{} _ api.SyncImpl = &syncInstrument{} _ api.BoundSyncImpl = &record{} + _ api.Resourcer = &SDK{} _ export.LabelStorage = &labels{} kvType = reflect.TypeOf(core.KeyValue{}) @@ -298,14 +303,20 @@ func (s *syncInstrument) RecordOne(ctx context.Context, number core.Number, ls a // batcher will call Collect() when it receives a request to scrape // current metric values. A push-based batcher should configure its // own periodic collection. -func New(batcher export.Batcher, labelEncoder export.LabelEncoder) *SDK { +func New(batcher export.Batcher, labelEncoder export.LabelEncoder, opts ...Option) *SDK { + c := &Config{ErrorHandler: DefaultErrorHandler} + for _, opt := range opts { + opt.Apply(c) + } + m := &SDK{ empty: labels{ ordered: [0]core.KeyValue{}, }, batcher: batcher, labelEncoder: labelEncoder, - errorHandler: DefaultErrorHandler, + errorHandler: c.ErrorHandler, + resource: c.Resource, } m.empty.meter = m m.empty.cachedValue = reflect.ValueOf(m.empty.ordered) @@ -559,6 +570,16 @@ func (m *SDK) checkpoint(ctx context.Context, descriptor *metric.Descriptor, rec return 1 } +// Resource returns the Resource this SDK was created with describing the +// entity for which it creates instruments for. +// +// Resource means that the SDK implements the Resourcer interface and +// therefore all metric instruments it creates will inherit its +// Resource by default unless explicitly overwritten. +func (m *SDK) Resource() resource.Resource { + return m.resource +} + // RecordBatch enters a batch of metric events. func (m *SDK) RecordBatch(ctx context.Context, ls api.LabelSet, measurements ...api.Measurement) { for _, meas := range measurements { diff --git a/sdk/resource/resource.go b/sdk/resource/resource.go index 8e78ed803..cc45075c4 100644 --- a/sdk/resource/resource.go +++ b/sdk/resource/resource.go @@ -17,6 +17,8 @@ package resource import ( + "reflect" + "go.opentelemetry.io/otel/api/core" ) @@ -70,3 +72,8 @@ func (r Resource) Attributes() []core.KeyValue { } return attrs } + +// Equal returns true if other Resource is the equal to r. +func (r Resource) Equal(other Resource) bool { + return reflect.DeepEqual(r.labels, other.labels) +}