diff --git a/api/metric/api.go b/api/metric/api.go index 4ea202227..bb06efe47 100644 --- a/api/metric/api.go +++ b/api/metric/api.go @@ -66,6 +66,23 @@ type ObservationCallback func(LabelSet, MeasurementValue) // the registered observers. type ObserverCallback func(Meter, Observer, ObservationCallback) +// WithDescriptor is an interface that all metric implement. +type WithDescriptor interface { + // Descriptor returns a descriptor of this metric. + Descriptor() *Descriptor +} + +type hiddenType struct{} + +// ExplicitReportingMetric is an interface that is implemented only by +// metrics that support getting a Handle. +type ExplicitReportingMetric interface { + WithDescriptor + // SupportHandle is a dummy function that can be only + // implemented in this package. + SupportHandle() hiddenType +} + // Meter is an interface to the metrics portion of the OpenTelemetry SDK. type Meter interface { // DefineLabels returns a reference to a set of labels that @@ -78,7 +95,7 @@ type Meter interface { // NewHandle creates a Handle that contains the passed // key-value pairs. This should not be used directly - prefer // using GetHandle function of a metric. - NewHandle(*Descriptor, LabelSet) Handle + NewHandle(ExplicitReportingMetric, LabelSet) Handle // DeleteHandle destroys the Handle and does a cleanup of the // underlying resources. DeleteHandle(Handle) diff --git a/api/metric/api_test.go b/api/metric/api_test.go new file mode 100644 index 000000000..f735f0b3a --- /dev/null +++ b/api/metric/api_test.go @@ -0,0 +1,718 @@ +// Copyright 2019, 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 + +import ( + "context" + "fmt" + "testing" + + "go.opentelemetry.io/api/core" + "go.opentelemetry.io/api/unit" +) + +func TestCounterOptions(t *testing.T) { + type testcase struct { + name string + opts []CounterOptionApplier + keys []core.Key + desc string + unit unit.Unit + alt bool + } + testcases := []testcase{ + { + name: "no opts", + opts: nil, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + { + name: "keys keys keys", + opts: []CounterOptionApplier{ + WithKeys(key("foo"), key("foo2")), + WithKeys(key("bar"), key("bar2")), + WithKeys(key("baz"), key("baz2")), + }, + keys: []core.Key{ + key("foo"), key("foo2"), + key("bar"), key("bar2"), + key("baz"), key("baz2"), + }, + desc: "", + unit: "", + alt: false, + }, + { + name: "description", + opts: []CounterOptionApplier{ + WithDescription("stuff"), + }, + keys: nil, + desc: "stuff", + unit: "", + alt: false, + }, + { + name: "description override", + opts: []CounterOptionApplier{ + WithDescription("stuff"), + WithDescription("things"), + }, + keys: nil, + desc: "things", + unit: "", + alt: false, + }, + { + name: "unit", + opts: []CounterOptionApplier{ + WithUnit("s"), + }, + keys: nil, + desc: "", + unit: "s", + alt: false, + }, + { + name: "unit override", + opts: []CounterOptionApplier{ + WithUnit("s"), + WithUnit("h"), + }, + keys: nil, + desc: "", + unit: "h", + alt: false, + }, + { + name: "nonmonotonic", + opts: []CounterOptionApplier{ + WithNonMonotonic(true), + }, + keys: nil, + desc: "", + unit: "", + alt: true, + }, + { + name: "nonmonotonic, but not really", + opts: []CounterOptionApplier{ + WithNonMonotonic(true), + WithNonMonotonic(false), + }, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + } + checkCounterDescriptor := func(tt testcase, vk ValueKind, d *Descriptor) { + e := descriptor{ + name: tt.name, + keys: tt.keys, + desc: tt.desc, + unit: tt.unit, + alt: tt.alt, + kind: CounterKind, + vk: vk, + } + checkDescriptor(t, e, d) + } + for idx, tt := range testcases { + t.Logf("Testing counter case %s (%d)", tt.name, idx) + f := NewFloat64Counter(tt.name, tt.opts...) + checkCounterDescriptor(tt, Float64ValueKind, f.Descriptor()) + i := NewInt64Counter(tt.name, tt.opts...) + checkCounterDescriptor(tt, Int64ValueKind, i.Descriptor()) + } +} + +func TestGaugeOptions(t *testing.T) { + type testcase struct { + name string + opts []GaugeOptionApplier + keys []core.Key + desc string + unit unit.Unit + alt bool + } + testcases := []testcase{ + { + name: "no opts", + opts: nil, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + { + name: "keys keys keys", + opts: []GaugeOptionApplier{ + WithKeys(key("foo"), key("foo2")), + WithKeys(key("bar"), key("bar2")), + WithKeys(key("baz"), key("baz2")), + }, + keys: []core.Key{ + key("foo"), key("foo2"), + key("bar"), key("bar2"), + key("baz"), key("baz2"), + }, + desc: "", + unit: "", + alt: false, + }, + { + name: "description", + opts: []GaugeOptionApplier{ + WithDescription("stuff"), + }, + keys: nil, + desc: "stuff", + unit: "", + alt: false, + }, + { + name: "description override", + opts: []GaugeOptionApplier{ + WithDescription("stuff"), + WithDescription("things"), + }, + keys: nil, + desc: "things", + unit: "", + alt: false, + }, + { + name: "unit", + opts: []GaugeOptionApplier{ + WithUnit("s"), + }, + keys: nil, + desc: "", + unit: "s", + alt: false, + }, + { + name: "unit override", + opts: []GaugeOptionApplier{ + WithUnit("s"), + WithUnit("h"), + }, + keys: nil, + desc: "", + unit: "h", + alt: false, + }, + { + name: "monotonic", + opts: []GaugeOptionApplier{ + WithMonotonic(true), + }, + keys: nil, + desc: "", + unit: "", + alt: true, + }, + { + name: "monotonic, but not really", + opts: []GaugeOptionApplier{ + WithMonotonic(true), + WithMonotonic(false), + }, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + } + checkGaugeDescriptor := func(tt testcase, vk ValueKind, d *Descriptor) { + e := descriptor{ + name: tt.name, + keys: tt.keys, + desc: tt.desc, + unit: tt.unit, + alt: tt.alt, + kind: GaugeKind, + vk: vk, + } + checkDescriptor(t, e, d) + } + for idx, tt := range testcases { + t.Logf("Testing gauge case %s (%d)", tt.name, idx) + f := NewFloat64Gauge(tt.name, tt.opts...) + checkGaugeDescriptor(tt, Float64ValueKind, f.Descriptor()) + i := NewInt64Gauge(tt.name, tt.opts...) + checkGaugeDescriptor(tt, Int64ValueKind, i.Descriptor()) + } +} + +func TestMeasureOptions(t *testing.T) { + type testcase struct { + name string + opts []MeasureOptionApplier + keys []core.Key + desc string + unit unit.Unit + alt bool + } + testcases := []testcase{ + { + name: "no opts", + opts: nil, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + { + name: "keys keys keys", + opts: []MeasureOptionApplier{ + WithKeys(key("foo"), key("foo2")), + WithKeys(key("bar"), key("bar2")), + WithKeys(key("baz"), key("baz2")), + }, + keys: []core.Key{ + key("foo"), key("foo2"), + key("bar"), key("bar2"), + key("baz"), key("baz2"), + }, + desc: "", + unit: "", + alt: false, + }, + { + name: "description", + opts: []MeasureOptionApplier{ + WithDescription("stuff"), + }, + keys: nil, + desc: "stuff", + unit: "", + alt: false, + }, + { + name: "description override", + opts: []MeasureOptionApplier{ + WithDescription("stuff"), + WithDescription("things"), + }, + keys: nil, + desc: "things", + unit: "", + alt: false, + }, + { + name: "unit", + opts: []MeasureOptionApplier{ + WithUnit("s"), + }, + keys: nil, + desc: "", + unit: "s", + alt: false, + }, + { + name: "unit override", + opts: []MeasureOptionApplier{ + WithUnit("s"), + WithUnit("h"), + }, + keys: nil, + desc: "", + unit: "h", + alt: false, + }, + { + name: "signed", + opts: []MeasureOptionApplier{ + WithSigned(true), + }, + keys: nil, + desc: "", + unit: "", + alt: true, + }, + { + name: "signed, but not really", + opts: []MeasureOptionApplier{ + WithSigned(true), + WithSigned(false), + }, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + } + checkMeasureDescriptor := func(tt testcase, vk ValueKind, d *Descriptor) { + e := descriptor{ + name: tt.name, + keys: tt.keys, + desc: tt.desc, + unit: tt.unit, + alt: tt.alt, + kind: MeasureKind, + vk: vk, + } + checkDescriptor(t, e, d) + } + for idx, tt := range testcases { + t.Logf("Testing measure case %s (%d)", tt.name, idx) + f := NewFloat64Measure(tt.name, tt.opts...) + checkMeasureDescriptor(tt, Float64ValueKind, f.Descriptor()) + i := NewInt64Measure(tt.name, tt.opts...) + checkMeasureDescriptor(tt, Int64ValueKind, i.Descriptor()) + } +} + +func TestObserverOptions(t *testing.T) { + type testcase struct { + name string + opts []GaugeOptionApplier + keys []core.Key + desc string + unit unit.Unit + alt bool + } + testcases := []testcase{ + { + name: "no opts", + opts: nil, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + { + name: "keys keys keys", + opts: []GaugeOptionApplier{ + WithKeys(key("foo"), key("foo2")), + WithKeys(key("bar"), key("bar2")), + WithKeys(key("baz"), key("baz2")), + }, + keys: []core.Key{ + key("foo"), key("foo2"), + key("bar"), key("bar2"), + key("baz"), key("baz2"), + }, + desc: "", + unit: "", + alt: false, + }, + { + name: "description", + opts: []GaugeOptionApplier{ + WithDescription("stuff"), + }, + keys: nil, + desc: "stuff", + unit: "", + alt: false, + }, + { + name: "description override", + opts: []GaugeOptionApplier{ + WithDescription("stuff"), + WithDescription("things"), + }, + keys: nil, + desc: "things", + unit: "", + alt: false, + }, + { + name: "unit", + opts: []GaugeOptionApplier{ + WithUnit("s"), + }, + keys: nil, + desc: "", + unit: "s", + alt: false, + }, + { + name: "unit override", + opts: []GaugeOptionApplier{ + WithUnit("s"), + WithUnit("h"), + }, + keys: nil, + desc: "", + unit: "h", + alt: false, + }, + { + name: "monotonic", + opts: []GaugeOptionApplier{ + WithMonotonic(true), + }, + keys: nil, + desc: "", + unit: "", + alt: true, + }, + { + name: "monotonic, but not really", + opts: []GaugeOptionApplier{ + WithMonotonic(true), + WithMonotonic(false), + }, + keys: nil, + desc: "", + unit: "", + alt: false, + }, + } + checkObserverDescriptor := func(tt testcase, vk ValueKind, d *Descriptor) { + e := descriptor{ + name: tt.name, + keys: tt.keys, + desc: tt.desc, + unit: tt.unit, + alt: tt.alt, + kind: ObserverKind, + vk: vk, + } + checkDescriptor(t, e, d) + } + for idx, tt := range testcases { + t.Logf("Testing observer case %s (%d)", tt.name, idx) + f := NewFloat64Observer(tt.name, tt.opts...) + checkObserverDescriptor(tt, Float64ValueKind, f.Descriptor()) + i := NewInt64Observer(tt.name, tt.opts...) + checkObserverDescriptor(tt, Int64ValueKind, i.Descriptor()) + } +} + +func key(name string) core.Key { + return core.Key{ + Name: name, + } +} + +type descriptor struct { + name string + keys []core.Key + desc string + unit unit.Unit + alt bool + kind Kind + vk ValueKind +} + +func checkDescriptor(t *testing.T, e descriptor, d *Descriptor) { + if e.name != d.Name() { + t.Errorf("Expected name %q, got %q", e.name, d.Name()) + } + if len(e.keys) != len(d.Keys()) { + t.Errorf("Expected %d key(s), got %d", len(e.keys), len(d.Keys())) + } + minLen := len(e.keys) + if minLen > len(d.Keys()) { + minLen = len(d.Keys()) + } + for i := 0; i < minLen; i++ { + if e.keys[i].Name != d.Keys()[i].Name { + t.Errorf("Expected key %q, got %q", e.keys[i].Name, d.Keys()[i].Name) + } + } + if e.desc != d.Description() { + t.Errorf("Expected description %q, got %q", e.desc, d.Description()) + } + if e.unit != d.Unit() { + t.Errorf("Expected unit %q, got %q", e.unit, d.Unit()) + } + if e.alt != d.Alternate() { + t.Errorf("Expected alternate %v, got %v", e.alt, d.Alternate()) + } + if e.vk != d.ValueKind() { + t.Errorf("Expected value kind %q, got %q", e.vk, d.ValueKind()) + } + if e.kind != d.Kind() { + t.Errorf("Expected kind %q, got %q", e.kind, d.Kind()) + } +} + +func TestCounter(t *testing.T) { + { + c := NewFloat64Counter("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + c.Add(ctx, 42, labels) + handle := c.GetHandle(labels) + handle.Add(ctx, 42) + meter.RecordBatch(ctx, labels, c.Measurement(42)) + t.Log("Testing float counter") + checkBatches(t, ctx, labels, meter, c.Descriptor()) + } + { + c := NewInt64Counter("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + c.Add(ctx, 42, labels) + handle := c.GetHandle(labels) + handle.Add(ctx, 42) + meter.RecordBatch(ctx, labels, c.Measurement(42)) + t.Log("Testing int counter") + checkBatches(t, ctx, labels, meter, c.Descriptor()) + } +} + +func TestGauge(t *testing.T) { + { + g := NewFloat64Gauge("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + g.Set(ctx, 42, labels) + handle := g.GetHandle(labels) + handle.Set(ctx, 42) + meter.RecordBatch(ctx, labels, g.Measurement(42)) + t.Log("Testing float gauge") + checkBatches(t, ctx, labels, meter, g.Descriptor()) + } + { + g := NewInt64Gauge("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + g.Set(ctx, 42, labels) + handle := g.GetHandle(labels) + handle.Set(ctx, 42) + meter.RecordBatch(ctx, labels, g.Measurement(42)) + t.Log("Testing int gauge") + checkBatches(t, ctx, labels, meter, g.Descriptor()) + } +} + +func TestMeasure(t *testing.T) { + { + m := NewFloat64Measure("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + m.Record(ctx, 42, labels) + handle := m.GetHandle(labels) + handle.Record(ctx, 42) + meter.RecordBatch(ctx, labels, m.Measurement(42)) + t.Log("Testing float measure") + checkBatches(t, ctx, labels, meter, m.Descriptor()) + } + { + m := NewInt64Measure("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + m.Record(ctx, 42, labels) + handle := m.GetHandle(labels) + handle.Record(ctx, 42) + meter.RecordBatch(ctx, labels, m.Measurement(42)) + t.Log("Testing int measure") + checkBatches(t, ctx, labels, meter, m.Descriptor()) + } +} + +func TestObserver(t *testing.T) { + { + o := NewFloat64Observer("ajwaj") + meter := newMockMeter() + ctx := context.Background() + labels := meter.DefineLabels(ctx) + RegisterFloat64Observer(meter, o, func(meter Meter, o Float64Observer, cb Float64ObservationCallback) { + cb(labels, 42) + cb(labels, 42) + cb(labels, 42) + }) + meter.PerformObservations() + t.Log("Testing float observer") + checkBatches(t, ctx, labels, meter, o.Descriptor()) + } + { + o := NewInt64Observer("ajwaj") + ctx := context.Background() + meter := newMockMeter() + labels := meter.DefineLabels(ctx) + RegisterInt64Observer(meter, o, func(meter Meter, o Int64Observer, cb Int64ObservationCallback) { + cb(labels, 42) + cb(labels, 42) + cb(labels, 42) + }) + meter.PerformObservations() + t.Log("Testing int observer") + checkBatches(t, ctx, labels, meter, o.Descriptor()) + } +} + +func checkBatches(t *testing.T, ctx context.Context, labels LabelSet, meter *mockMeter, descriptor *Descriptor) { + if len(meter.measurementBatches) != 3 { + t.Errorf("Expected 3 recorded measurement batches, got %d", len(meter.measurementBatches)) + } + ourLabelSet := labels.(*mockLabelSet) + minLen := 3 + if minLen > len(meter.measurementBatches) { + minLen = len(meter.measurementBatches) + } + for i := 0; i < minLen; i++ { + got := meter.measurementBatches[i] + if got.ctx != ctx { + d := func(c context.Context) string { + return fmt.Sprintf("(ptr: %p, ctx %#v)", c, c) + } + t.Errorf("Wrong recorded context in batch %d, expected %s, got %s", i, d(ctx), d(got.ctx)) + } + if got.labelSet != ourLabelSet { + d := func(l *mockLabelSet) string { + return fmt.Sprintf("(ptr: %p, labels %#v)", l, l.labels) + } + t.Errorf("Wrong recorded label set in batch %d, expected %s, got %s", i, d(ourLabelSet), d(got.labelSet)) + } + if len(got.measurements) != 1 { + t.Errorf("Expected 1 measurement in batch %d, got %d", i, len(got.measurements)) + } + minMLen := 1 + if minMLen > len(got.measurements) { + minMLen = len(got.measurements) + } + for j := 0; j < minMLen; j++ { + measurement := got.measurements[j] + if measurement.Descriptor != descriptor { + d := func(d *Descriptor) string { + return fmt.Sprintf("(ptr: %p, descriptor %#v)", d, d) + } + t.Errorf("Wrong recorded descriptor in measurement %d in batch %d, expected %s, got %s", j, i, d(descriptor), d(measurement.Descriptor)) + } + ft := fortyTwo(t, descriptor.ValueKind()) + if measurement.Value.RawCompare(ft.AsRaw(), descriptor.ValueKind()) != 0 { + t.Errorf("Wrong recorded value in measurement %d in batch %d, expected %s, got %s", j, i, ft.Emit(descriptor.ValueKind()), measurement.Value.Emit(descriptor.ValueKind())) + } + } + } +} + +func fortyTwo(t *testing.T, kind ValueKind) MeasurementValue { + switch kind { + case Int64ValueKind: + return NewInt64MeasurementValue(42) + case Float64ValueKind: + return NewFloat64MeasurementValue(42) + } + t.Errorf("Invalid value kind %q", kind) + return NewInt64MeasurementValue(0) +} diff --git a/api/metric/common.go b/api/metric/common.go index 52faaaf0a..1858a6e7b 100644 --- a/api/metric/common.go +++ b/api/metric/common.go @@ -23,43 +23,48 @@ var ( descriptorID uint64 ) -// TODO: Maybe unexport that and document very _very_ clearly, that -// you can still get a descriptor with NewInt64Counter(…).Descriptor - -// CommonMetric holds a descriptor. It is used mostly to implement the -// common parts for every metric kind. -type CommonMetric struct { - *Descriptor +type commonMetric struct { + d *Descriptor } -func (m CommonMetric) getHandle(labels LabelSet) Handle { - return labels.Meter().NewHandle(m.Descriptor, labels) +var _ ExplicitReportingMetric = commonMetric{} + +func (m commonMetric) Descriptor() *Descriptor { + return m.d } -func (m CommonMetric) float64Measurement(value float64) Measurement { +func (m commonMetric) SupportHandle() hiddenType { + return hiddenType{} +} + +func (m commonMetric) getHandle(labels LabelSet) Handle { + return labels.Meter().NewHandle(m, labels) +} + +func (m commonMetric) float64Measurement(value float64) Measurement { return Measurement{ - Descriptor: m.Descriptor, + Descriptor: m.d, Value: NewFloat64MeasurementValue(value), } } -func (m CommonMetric) int64Measurement(value int64) Measurement { +func (m commonMetric) int64Measurement(value int64) Measurement { return Measurement{ - Descriptor: m.Descriptor, + Descriptor: m.d, Value: NewInt64MeasurementValue(value), } } -func (m CommonMetric) recordOne(ctx context.Context, value MeasurementValue, labels LabelSet) { +func (m commonMetric) recordOne(ctx context.Context, value MeasurementValue, labels LabelSet) { labels.Meter().RecordBatch(ctx, labels, Measurement{ - Descriptor: m.Descriptor, + Descriptor: m.d, Value: value, }) } -func registerCommonMetric(name string, kind Kind, valueKind ValueKind) CommonMetric { - return CommonMetric{ - Descriptor: registerDescriptor(name, kind, valueKind), +func registerCommonMetric(name string, kind Kind, valueKind ValueKind) commonMetric { + return commonMetric{ + d: registerDescriptor(name, kind, valueKind), } } diff --git a/api/metric/counter.go b/api/metric/counter.go index 796d69166..d8b1b2a63 100644 --- a/api/metric/counter.go +++ b/api/metric/counter.go @@ -20,12 +20,12 @@ import ( // Float64Counter is a metric that accumulates float64 values. type Float64Counter struct { - CommonMetric + commonMetric } // Int64Counter is a metric that accumulates int64 values. type Int64Counter struct { - CommonMetric + commonMetric } // Float64CounterHandle is a handle for Float64Counter. @@ -50,43 +50,55 @@ type counterOptionWrapper struct { F Option } -var _ CounterOptionApplier = counterOptionWrapper{} +var ( + _ CounterOptionApplier = counterOptionWrapper{} + _ ExplicitReportingMetric = Float64Counter{} + _ ExplicitReportingMetric = Int64Counter{} +) func (o counterOptionWrapper) ApplyCounterOption(d *Descriptor) { o.F(d) } -func newCounter(name string, valueKind ValueKind, mos ...CounterOptionApplier) CommonMetric { +func newCounter(name string, valueKind ValueKind, mos ...CounterOptionApplier) commonMetric { m := registerCommonMetric(name, CounterKind, valueKind) for _, opt := range mos { - opt.ApplyCounterOption(m.Descriptor) + opt.ApplyCounterOption(m.Descriptor()) } return m } // NewFloat64Counter creates a new counter for float64. func NewFloat64Counter(name string, mos ...CounterOptionApplier) (c Float64Counter) { - c.CommonMetric = newCounter(name, Float64ValueKind, mos...) + c.commonMetric = newCounter(name, Float64ValueKind, mos...) return } // NewInt64Counter creates a new counter for int64. func NewInt64Counter(name string, mos ...CounterOptionApplier) (c Int64Counter) { - c.CommonMetric = newCounter(name, Int64ValueKind, mos...) + c.commonMetric = newCounter(name, Int64ValueKind, mos...) return } // GetHandle creates a handle for this counter. The labels should -// contain the keys and values specified in the counter with the -// WithKeys option. +// contain the keys and values for each key specified in the counter +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// counter with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Float64Counter) GetHandle(labels LabelSet) (h Float64CounterHandle) { h.Handle = c.getHandle(labels) return } // GetHandle creates a handle for this counter. The labels should -// contain the keys and values specified in the counter with the -// WithKeys option. +// contain the keys and values for each key specified in the counter +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// counter with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Int64Counter) GetHandle(labels LabelSet) (h Int64CounterHandle) { h.Handle = c.getHandle(labels) return @@ -105,15 +117,23 @@ func (c *Int64Counter) Measurement(value int64) Measurement { } // Add adds the value to the counter's sum. The labels should contain -// the keys and values specified in the counter with the WithKeys -// option. +// the keys and values for each key specified in the counter with the +// WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// counter with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Float64Counter) Add(ctx context.Context, value float64, labels LabelSet) { c.recordOne(ctx, NewFloat64MeasurementValue(value), labels) } // Add adds the value to the counter's sum. The labels should contain -// the keys and values specified in the counter with the WithKeys -// option. +// the keys and values for each key specified in the counter with the +// WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// counter with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Int64Counter) Add(ctx context.Context, value int64, labels LabelSet) { c.recordOne(ctx, NewInt64MeasurementValue(value), labels) } diff --git a/api/metric/gauge.go b/api/metric/gauge.go index 88af1f8d6..933e51f27 100644 --- a/api/metric/gauge.go +++ b/api/metric/gauge.go @@ -20,12 +20,12 @@ import ( // Float64Gauge is a metric that stores the last float64 value. type Float64Gauge struct { - CommonMetric + commonMetric } // Int64Gauge is a metric that stores the last int64 value. type Int64Gauge struct { - CommonMetric + commonMetric } // Float64GaugeHandle is a handle for Float64Gauge. @@ -50,43 +50,55 @@ type gaugeOptionWrapper struct { F Option } -var _ GaugeOptionApplier = gaugeOptionWrapper{} +var ( + _ GaugeOptionApplier = gaugeOptionWrapper{} + _ ExplicitReportingMetric = Float64Gauge{} + _ ExplicitReportingMetric = Int64Gauge{} +) func (o gaugeOptionWrapper) ApplyGaugeOption(d *Descriptor) { o.F(d) } -func newGauge(name string, valueKind ValueKind, mos ...GaugeOptionApplier) CommonMetric { +func newGauge(name string, valueKind ValueKind, mos ...GaugeOptionApplier) commonMetric { m := registerCommonMetric(name, GaugeKind, valueKind) for _, opt := range mos { - opt.ApplyGaugeOption(m.Descriptor) + opt.ApplyGaugeOption(m.Descriptor()) } return m } // NewFloat64Gauge creates a new gauge for float64. func NewFloat64Gauge(name string, mos ...GaugeOptionApplier) (g Float64Gauge) { - g.CommonMetric = newGauge(name, Float64ValueKind, mos...) + g.commonMetric = newGauge(name, Float64ValueKind, mos...) return } // NewInt64Gauge creates a new gauge for int64. func NewInt64Gauge(name string, mos ...GaugeOptionApplier) (g Int64Gauge) { - g.CommonMetric = newGauge(name, Int64ValueKind, mos...) + g.commonMetric = newGauge(name, Int64ValueKind, mos...) return } // GetHandle creates a handle for this gauge. The labels should -// contain the keys and values specified in the gauge with the -// WithKeys option. +// contain the keys and values for each key specified in the gauge +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// gauge with the WithKeys option, then the missing value will be +// treated as unspecified. func (g *Float64Gauge) GetHandle(labels LabelSet) (h Float64GaugeHandle) { h.Handle = g.getHandle(labels) return } // GetHandle creates a handle for this gauge. The labels should -// contain the keys and values specified in the gauge with the -// WithKeys option. +// contain the keys and values for each key specified in the gauge +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// gauge with the WithKeys option, then the missing value will be +// treated as unspecified. func (g *Int64Gauge) GetHandle(labels LabelSet) (h Int64GaugeHandle) { h.Handle = g.getHandle(labels) return @@ -104,26 +116,34 @@ func (g *Int64Gauge) Measurement(value int64) Measurement { return g.int64Measurement(value) } -// Set sets the value of the gauge to the passed value. The labels -// should contain the keys and values specified in the gauge with the -// WithKeys option. +// Set assigns the passed value to the value of the gauge. The labels +// should contain the keys and values for each key specified in the +// gauge with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// gauge with the WithKeys option, then the missing value will be +// treated as unspecified. func (g *Float64Gauge) Set(ctx context.Context, value float64, labels LabelSet) { g.recordOne(ctx, NewFloat64MeasurementValue(value), labels) } -// Set sets the value of the gauge to the passed value. The labels -// should contain the keys and values specified in the gauge with the -// WithKeys option. +// Set assigns the passed value to the value of the gauge. The labels +// should contain the keys and values for each key specified in the +// gauge with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// gauge with the WithKeys option, then the missing value will be +// treated as unspecified. func (g *Int64Gauge) Set(ctx context.Context, value int64, labels LabelSet) { g.recordOne(ctx, NewInt64MeasurementValue(value), labels) } -// Set sets the value of the gauge to the passed value. +// Set assigns the passed value to the value of the gauge. func (h *Float64GaugeHandle) Set(ctx context.Context, value float64) { h.RecordOne(ctx, NewFloat64MeasurementValue(value)) } -// Set sets the value of the gauge to the passed value. +// Set assigns the passed value to the value of the gauge. func (h *Int64GaugeHandle) Set(ctx context.Context, value int64) { h.RecordOne(ctx, NewInt64MeasurementValue(value)) } diff --git a/api/metric/global_test.go b/api/metric/global_test.go new file mode 100644 index 000000000..a41ec22d4 --- /dev/null +++ b/api/metric/global_test.go @@ -0,0 +1,33 @@ +// Copyright 2019, 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 + +import ( + "testing" +) + +func TestGlobalMeter(t *testing.T) { + m := GlobalMeter() + if _, ok := m.(noopMeter); !ok { + t.Errorf("Expected global meter to be a noopMeter instance, got an instance of %T", m) + } + + SetGlobalMeter(newMockMeter()) + + m = GlobalMeter() + if _, ok := m.(*mockMeter); !ok { + t.Errorf("Expected global meter to be a *mockMetric.MockMeter instance, got an instance of %T", m) + } +} diff --git a/api/metric/measure.go b/api/metric/measure.go index 0bc892d3d..b0b7166c6 100644 --- a/api/metric/measure.go +++ b/api/metric/measure.go @@ -20,12 +20,12 @@ import ( // Float64Measure is a metric that records float64 values. type Float64Measure struct { - CommonMetric + commonMetric } // Int64Measure is a metric that records int64 values. type Int64Measure struct { - CommonMetric + commonMetric } // Float64MeasureHandle is a handle for Float64Measure. @@ -50,43 +50,55 @@ type measureOptionWrapper struct { F Option } -var _ MeasureOptionApplier = measureOptionWrapper{} +var ( + _ MeasureOptionApplier = measureOptionWrapper{} + _ ExplicitReportingMetric = Float64Measure{} + _ ExplicitReportingMetric = Int64Measure{} +) func (o measureOptionWrapper) ApplyMeasureOption(d *Descriptor) { o.F(d) } -func newMeasure(name string, valueKind ValueKind, mos ...MeasureOptionApplier) CommonMetric { +func newMeasure(name string, valueKind ValueKind, mos ...MeasureOptionApplier) commonMetric { m := registerCommonMetric(name, MeasureKind, valueKind) for _, opt := range mos { - opt.ApplyMeasureOption(m.Descriptor) + opt.ApplyMeasureOption(m.Descriptor()) } return m } // NewFloat64Measure creates a new measure for float64. func NewFloat64Measure(name string, mos ...MeasureOptionApplier) (c Float64Measure) { - c.CommonMetric = newMeasure(name, Float64ValueKind, mos...) + c.commonMetric = newMeasure(name, Float64ValueKind, mos...) return } // NewInt64Measure creates a new measure for int64. func NewInt64Measure(name string, mos ...MeasureOptionApplier) (c Int64Measure) { - c.CommonMetric = newMeasure(name, Int64ValueKind, mos...) + c.commonMetric = newMeasure(name, Int64ValueKind, mos...) return } // GetHandle creates a handle for this measure. The labels should -// contain the keys and values specified in the measure with the -// WithKeys option. +// contain the keys and values for each key specified in the measure +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// measure with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Float64Measure) GetHandle(labels LabelSet) (h Float64MeasureHandle) { h.Handle = c.getHandle(labels) return } // GetHandle creates a handle for this measure. The labels should -// contain the keys and values specified in the measure with the -// WithKeys option. +// contain the keys and values for each key specified in the measure +// with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// measure with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Int64Measure) GetHandle(labels LabelSet) (h Int64MeasureHandle) { h.Handle = c.getHandle(labels) return @@ -105,15 +117,23 @@ func (c *Int64Measure) Measurement(value int64) Measurement { } // Record adds a new value to the list of measure's records. The -// labels should contain the keys and values specified in the measure -// with the WithKeys option. +// labels should contain the keys and values for each key specified in +// the measure with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// measure with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Float64Measure) Record(ctx context.Context, value float64, labels LabelSet) { c.recordOne(ctx, NewFloat64MeasurementValue(value), labels) } // Record adds a new value to the list of measure's records. The -// labels should contain the keys and values specified in the measure -// with the WithKeys option. +// labels should contain the keys and values for each key specified in +// the measure with the WithKeys option. +// +// If the labels do not contain a value for the key specified in the +// measure with the WithKeys option, then the missing value will be +// treated as unspecified. func (c *Int64Measure) Record(ctx context.Context, value int64, labels LabelSet) { c.recordOne(ctx, NewInt64MeasurementValue(value), labels) } diff --git a/api/metric/mock_test.go b/api/metric/mock_test.go new file mode 100644 index 000000000..3dba98d70 --- /dev/null +++ b/api/metric/mock_test.go @@ -0,0 +1,141 @@ +// Copyright 2019, 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 + +import ( + "context" + + "go.opentelemetry.io/api/core" +) + +type ( + mockHandle struct { + descriptor *Descriptor + labelSet *mockLabelSet + } + + mockLabelSet struct { + meter *mockMeter + labels map[core.Key]core.Value + } + + batch struct { + ctx context.Context + labelSet *mockLabelSet + measurements []Measurement + } + + observerData struct { + observer Observer + callback ObserverCallback + } + + observerMap map[DescriptorID]observerData + + mockMeter struct { + measurementBatches []batch + observers observerMap + } +) + +var ( + _ Handle = &mockHandle{} + _ LabelSet = &mockLabelSet{} + _ Meter = &mockMeter{} +) + +func (h *mockHandle) RecordOne(ctx context.Context, value MeasurementValue) { + h.labelSet.meter.RecordBatch(ctx, h.labelSet, Measurement{ + Descriptor: h.descriptor, + Value: value, + }) +} + +func (s *mockLabelSet) Meter() Meter { + return s.meter +} + +func newMockMeter() *mockMeter { + return &mockMeter{} +} + +func (m *mockMeter) DefineLabels(ctx context.Context, labels ...core.KeyValue) LabelSet { + ul := make(map[core.Key]core.Value) + for _, kv := range labels { + ul[kv.Key] = kv.Value + } + return &mockLabelSet{ + meter: m, + labels: ul, + } +} + +func (m *mockMeter) RecordBatch(ctx context.Context, labels LabelSet, measurements ...Measurement) { + ourLabelSet := labels.(*mockLabelSet) + m.measurementBatches = append(m.measurementBatches, batch{ + ctx: ctx, + labelSet: ourLabelSet, + measurements: measurements, + }) +} + +func (m *mockMeter) NewHandle(erm ExplicitReportingMetric, labels LabelSet) Handle { + descriptor := erm.Descriptor() + ourLabels := labels.(*mockLabelSet) + + return &mockHandle{ + descriptor: descriptor, + labelSet: ourLabels, + } +} + +func (m *mockMeter) DeleteHandle(Handle) { +} + +func (m *mockMeter) RegisterObserver(o Observer, cb ObserverCallback) { + id := o.Descriptor().ID() + if _, ok := m.observers[id]; ok { + return + } + data := observerData{ + observer: o, + callback: cb, + } + if m.observers == nil { + m.observers = observerMap{ + id: data, + } + } else { + m.observers[id] = data + } +} + +func (m *mockMeter) UnregisterObserver(o Observer) { + delete(m.observers, o.Descriptor().ID()) +} + +func (m *mockMeter) PerformObservations() { + for _, data := range m.observers { + o := data.observer + descriptor := o.Descriptor() + ocb := func(l LabelSet, v MeasurementValue) { + m.RecordBatch(context.Background(), l, Measurement{ + Descriptor: descriptor, + Value: v, + }) + } + data.callback(m, o, ocb) + } +} diff --git a/api/metric/noop.go b/api/metric/noop.go index 2dda66266..f6297fc7c 100644 --- a/api/metric/noop.go +++ b/api/metric/noop.go @@ -25,7 +25,7 @@ func (noopMeter) DefineLabels(context.Context, ...core.KeyValue) LabelSet { return noopLabelSet{} } -func (noopMeter) NewHandle(*Descriptor, LabelSet) Handle { +func (noopMeter) NewHandle(ExplicitReportingMetric, LabelSet) Handle { return noopHandle{} } diff --git a/api/metric/observer.go b/api/metric/observer.go index 0d93997b8..44acd27dc 100644 --- a/api/metric/observer.go +++ b/api/metric/observer.go @@ -16,7 +16,7 @@ package metric // Observer is a base of typed-observers. Shouldn't be used directly. type Observer struct { - *Descriptor + d *Descriptor } // Float64Observer is an observer that reports float64 values. @@ -30,9 +30,9 @@ type Int64Observer struct { } func newObserver(name string, valueKind ValueKind, mos ...GaugeOptionApplier) (o Observer) { - o.Descriptor = registerDescriptor(name, ObserverKind, valueKind) + o.d = registerDescriptor(name, ObserverKind, valueKind) for _, opt := range mos { - opt.ApplyGaugeOption(o.Descriptor) + opt.ApplyGaugeOption(o.d) } return } @@ -48,3 +48,7 @@ func NewInt64Observer(name string, mos ...GaugeOptionApplier) (o Int64Observer) o.Observer = newObserver(name, Int64ValueKind, mos...) return } + +func (o Observer) Descriptor() *Descriptor { + return o.d +} diff --git a/experimental/streaming/sdk/metric.go b/experimental/streaming/sdk/metric.go index 6974b335b..46483c0ad 100644 --- a/experimental/streaming/sdk/metric.go +++ b/experimental/streaming/sdk/metric.go @@ -60,11 +60,11 @@ func (s *sdk) DefineLabels(ctx context.Context, labels ...core.KeyValue) metric. } } -func (s *sdk) NewHandle(descriptor *metric.Descriptor, labels metric.LabelSet) metric.Handle { +func (s *sdk) NewHandle(erm metric.ExplicitReportingMetric, labels metric.LabelSet) metric.Handle { mlabels, _ := labels.(metricLabels) return &metricHandle{ - descriptor: descriptor, + descriptor: erm.Descriptor(), labels: mlabels, } } @@ -100,7 +100,7 @@ func (s *sdk) insertNewObserver(observer metric.Observer, callback metric.Observ s.observersLock.Lock() defer s.observersLock.Unlock() old := s.loadObserversMap() - id := observer.Descriptor.ID() + id := observer.Descriptor().ID() if _, ok := old[id]; ok { return false } @@ -120,7 +120,7 @@ func (s *sdk) UnregisterObserver(observer metric.Observer) { s.observersLock.Lock() defer s.observersLock.Unlock() old := s.loadObserversMap() - id := observer.Descriptor.ID() + id := observer.Descriptor().ID() if _, ok := old[id]; !ok { return } @@ -146,7 +146,7 @@ func (s *sdk) observersRoutine() { return } for _, data := range m { - ocb := s.getObservationCallback(data.observer.Descriptor) + ocb := s.getObservationCallback(data.observer.Descriptor()) data.callback(s, data.observer, ocb) } }