Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create an Inmemory Exporter for test #2776

Merged
merged 14 commits into from Apr 24, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
The package contains semantic conventions from the `v1.9.0` version of the OpenTelemetry specification. (#2792)
- Add the `go.opentelemetry.io/otel/semconv/v1.10.0` package.
The package contains semantic conventions from the `v1.10.0` version of the OpenTelemetry specification. (#2842)
- Added an in-memory exporter to metrictest to aid testing with a full SDK. (#2776)
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

Expand Down Expand Up @@ -43,6 +44,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [0.29.0] - 2022-04-11

### Added

- The metrics global package was added back into several test files. (#2764)
- The `Meter` function is added back to the `go.opentelemetry.io/otel/metric/global` package.
This function is a convenience function equivalent to calling `global.MeterProvider().Meter(...)`. (#2750)
Expand Down
3 changes: 1 addition & 2 deletions sdk/metric/aggregator/aggregatortest/test.go
Expand Up @@ -28,7 +28,6 @@ import (
ottest "go.opentelemetry.io/otel/internal/internaltest"
"go.opentelemetry.io/otel/sdk/metric/aggregator"
"go.opentelemetry.io/otel/sdk/metric/export/aggregation"
"go.opentelemetry.io/otel/sdk/metric/metrictest"
"go.opentelemetry.io/otel/sdk/metric/number"
"go.opentelemetry.io/otel/sdk/metric/sdkapi"
)
Expand Down Expand Up @@ -65,7 +64,7 @@ func newProfiles() []Profile {
}

func NewAggregatorTest(mkind sdkapi.InstrumentKind, nkind number.Kind) *sdkapi.Descriptor {
desc := metrictest.NewDescriptor("test.name", mkind, nkind)
desc := sdkapi.NewDescriptor("test.name", mkind, nkind, "", "")
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
return &desc
}

Expand Down
3 changes: 1 addition & 2 deletions sdk/metric/export/aggregation/temporality_test.go
Expand Up @@ -19,7 +19,6 @@ import (

"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/sdk/metric/metrictest"
"go.opentelemetry.io/otel/sdk/metric/number"
"go.opentelemetry.io/otel/sdk/metric/sdkapi"
)
Expand Down Expand Up @@ -59,7 +58,7 @@ func TestTemporalitySelectors(t *testing.T) {
sAggTemp := StatelessTemporalitySelector()

for _, ikind := range append(deltaMemoryTemporalties, cumulativeMemoryTemporalties...) {
desc := metrictest.NewDescriptor("instrument", ikind, number.Int64Kind)
desc := sdkapi.NewDescriptor("instrument", ikind, number.Int64Kind, "", "")

var akind Kind
if ikind.Adding() {
Expand Down
57 changes: 57 additions & 0 deletions sdk/metric/metrictest/config.go
@@ -0,0 +1,57 @@
// 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 metrictest // import "go.opentelemetry.io/otel/sdk/metric/metrictest"

import "go.opentelemetry.io/otel/sdk/metric/export/aggregation"

type config struct {
temporalitySelector aggregation.TemporalitySelector
}

func newConfig(opts ...Option) config {
cfg := config{
temporalitySelector: aggregation.CumulativeTemporalitySelector(),
}
for _, opt := range opts {
cfg = opt.apply(cfg)
}
return cfg
}

// Option allow for control of details of the TestMeterProvider created.
type Option interface {
apply(config) config
}

type functionOption func(config) config

func (f functionOption) apply(cfg config) config {
return f(cfg)
}

// WithTemporalitySelector allows for the use of either cumulative (default) or
// delta metrics.
//
// Warning: the current SDK does not convert async instruments into delta
// temporality.
func WithTemporalitySelector(ts aggregation.TemporalitySelector) Option {
return functionOption(func(cfg config) config {
if ts == nil {
return cfg
}
cfg.temporalitySelector = ts
return cfg
})
}
18 changes: 18 additions & 0 deletions sdk/metric/metrictest/doc.go
@@ -0,0 +1,18 @@
// 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.

// The metrictest package is a collection of tools used to make testing parts of
// the SDK easier.

package metrictest // import "go.opentelemetry.io/otel/sdk/metric/metrictest"
180 changes: 180 additions & 0 deletions sdk/metric/metrictest/exporter.go
@@ -0,0 +1,180 @@
// 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 metrictest // import "go.opentelemetry.io/otel/sdk/metric/metrictest"

import (
"context"
"fmt"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk/instrumentation"
controller "go.opentelemetry.io/otel/sdk/metric/controller/basic"
"go.opentelemetry.io/otel/sdk/metric/export"
"go.opentelemetry.io/otel/sdk/metric/export/aggregation"
"go.opentelemetry.io/otel/sdk/metric/number"
processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
selector "go.opentelemetry.io/otel/sdk/metric/selector/simple"
)

// Exporter is a manually collected exporter for testing the SDK. It does not
// satisfy the `export.Exporter` interface because it is not intended to be
// used with the periodic collection of the SDK, instead the test should
// manually call `Collect()`
//
// Exporters are not thread safe, and should only be used for testing.
type Exporter struct {
// Records contains the last metrics collected.
Records []ExportRecord

controller *controller.Controller
temporalitySelector aggregation.TemporalitySelector
}

// NewTestMeterProvider creates a MeterProvider and Exporter to be used in tests.
func NewTestMeterProvider(opts ...Option) (metric.MeterProvider, *Exporter) {
cfg := newConfig(opts...)

c := controller.New(
processor.NewFactory(
selector.NewWithHistogramDistribution(),
cfg.temporalitySelector,
),
controller.WithCollectPeriod(0),
)
exp := &Exporter{
controller: c,
temporalitySelector: cfg.temporalitySelector,
}

return c, exp
}

// ExportRecord represents one collected datapoint from the Exporter.
type ExportRecord struct {
InstrumentName string
InstrumentationLibrary Library
Attributes []attribute.KeyValue
AggregationKind aggregation.Kind
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
NumberKind number.Kind
Sum number.Number
Count uint64
Histogram aggregation.Buckets
LastValue number.Number
}

// Collect triggers the SDK's collect methods and then aggregates the data into
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
// ExportRecords. This will overwrite any previous collected metrics.
func (e *Exporter) Collect(ctx context.Context) error {
e.Records = []ExportRecord{}

err := e.controller.Collect(ctx)
if err != nil {
return err
}

return e.controller.ForEach(func(l instrumentation.Library, r export.Reader) error {
lib := Library{
InstrumentationName: l.Name,
InstrumentationVersion: l.Version,
SchemaURL: l.SchemaURL,
}

return r.ForEach(e.temporalitySelector, func(rec export.Record) error {
record := ExportRecord{
InstrumentName: rec.Descriptor().Name(),
InstrumentationLibrary: lib,
Attributes: rec.Attributes().ToSlice(),
AggregationKind: rec.Aggregation().Kind(),
NumberKind: rec.Descriptor().NumberKind(),
}

var err error
switch agg := rec.Aggregation().(type) {
case aggregation.Histogram:
record.AggregationKind = aggregation.HistogramKind
record.Histogram, err = agg.Histogram()
if err != nil {
return err
}
record.Sum, err = agg.Sum()
if err != nil {
return err
}
record.Count, err = agg.Count()
if err != nil {
return err
}
case aggregation.Count:
record.Count, err = agg.Count()
if err != nil {
return err
}
case aggregation.LastValue:
record.LastValue, _, err = agg.LastValue()
if err != nil {
return err
}
case aggregation.Sum:
record.Sum, err = agg.Sum()
if err != nil {
return err
}
}

e.Records = append(e.Records, record)
return nil
})
})
}

// GetRecords returns all Records found by the SDK.
func (e *Exporter) GetRecords() []ExportRecord {
return e.Records
}

var errNotFound = fmt.Errorf("record not found")

// GetByName returns the first Record with a matching instrument name.
func (e *Exporter) GetByName(name string) (ExportRecord, error) {
for _, rec := range e.Records {
if rec.InstrumentName == name {
return rec, nil
}
}
return ExportRecord{}, errNotFound
}

// GetByNameAndAttributes returns the first Record with a matching name and the sub-set of attributes.
func (e *Exporter) GetByNameAndAttributes(name string, attributes []attribute.KeyValue) (ExportRecord, error) {
for _, rec := range e.Records {
if rec.InstrumentName == name && subSet(attributes, rec.Attributes) {
return rec, nil
}
}
return ExportRecord{}, errNotFound
}

// subSet returns true if attributesA is a subset of attributesB.
func subSet(attributesA, attributesB []attribute.KeyValue) bool {
b := attribute.NewSet(attributesB...)

for _, kv := range attributesA {
if v, found := b.Value(kv.Key); !found || v != kv.Value {
return false
}
}
return true
}