-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create an Inmemory Exporter for test (#2776)
* in memory exporter * Add comments * Split tests and address PR comments * address PR comments. * Changelog * fix Changelog. * Apply suggestions from code review Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Joshua MacDonald <jmacd@users.noreply.github.com> * Add numberkind * Made the tests require.NoError * fix the Label changing * Update CHANGELOG.md Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Joshua MacDonald <jmacd@users.noreply.github.com>
- Loading branch information
1 parent
c05c3e2
commit 8982a14
Showing
7 changed files
with
684 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
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 | ||
// 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 | ||
} |
Oops, something went wrong.