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

Define the reader interface, and create a manual reader #2885

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion sdk/metric/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,26 @@
package metric // import "go.opentelemetry.io/otel/sdk/metric"

// config contains configuration options for a MeterProvider.
type config struct{}
type config struct {
readers []Reader
}

// Option applies a configuration option value to a MeterProvider.
type Option interface {
apply(config) config
}

type optionFunc func(cfg config) config

func (o optionFunc) apply(cfg config) config {
return o(cfg)
}

func WithReader(rdr Reader) Option {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
return optionFunc(func(cfg config) config {
cfg.readers = append(cfg.readers, rdr)
return cfg
})
}

// TODO (#2819): implement provider options.
90 changes: 90 additions & 0 deletions sdk/metric/export/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.

// TODO: NOTE this is a temporary space, it may be moved following the
// discussion of #2813, or #2841

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

import (
"time"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
)

type (
// Metrics is the result of a single collection.
//
// This struct supports re-use of the nested memory structure
// in its Scopes slice such that repeated calls Produce will
// not reallocate the same quantity of memory again and again.
//
// To re-use the memory from a previous Metrics value, pass a
// pointer to the former result to Produce(). This is safe for
// push-based exporters that perform sequential collection.
Metrics struct {
// Resource is the MeterProvider's configured Resource.
Resource *resource.Resource

// Scopes is a slice of metric data, one per Meter.
Scopes []Scope
}

// Scope is the result of a single collection for a single Meter.
//
// See the comments on Metrics about re-use of slices in this struct.
Scope struct {
// Library describes the instrumentation scope.
Library instrumentation.Library

// Instruments is a slice of metric data, one per Instrument
// in the scope.
Instruments []Instrument
}

// Instrument is the result of a single collection for a single Instrument.
//
// See the comments on Metrics about re-use of slices in this struct.
Instrument struct {
// Descriptor describes an instrument created through a View,
// including name, unit, description, instrument and number kinds.
// TODO Define sdkinsturment package #2813
// Descriptor sdkinstrument.Descriptor

// Points is a slice of metric data, one per attribute.Set value.
Points []Point
}

// Point is a timeseries data point resulting from a single collection.
Point struct {
// Attributes are the coordinates of this series.
Attributes attribute.Set

// Aggregation determines the kind of data point
// recorded in this series.
// TODO: Define aggregation types #2827, #2828
// Aggregation aggregation.Aggregation

// Start indicates the start of the collection
// interval reflected in this series, which is set
// according to the configured temporality.
Start time.Time

// End indicates the moment at which the collection
// was performed.
End time.Time
}
)
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 6 additions & 1 deletion sdk/metric/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ module go.opentelemetry.io/otel/sdk/metric

go 1.16

require go.opentelemetry.io/otel/metric v0.0.0-00010101000000-000000000000
require (
github.com/stretchr/testify v1.7.1
go.opentelemetry.io/otel v1.6.3
go.opentelemetry.io/otel/metric v0.0.0-00010101000000-000000000000
go.opentelemetry.io/otel/sdk v1.7.0
)

replace go.opentelemetry.io/otel => ../..

Expand Down
5 changes: 5 additions & 0 deletions sdk/metric/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
Expand All @@ -10,7 +12,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
62 changes: 62 additions & 0 deletions sdk/metric/manual_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 // import "go.opentelemetry.io/otel/sdk/metric/reader"

import (
"context"
"fmt"

"go.opentelemetry.io/otel/sdk/metric/export"
)

// ManualReader is a a simple Reader that allows an application to
// read metrics on demand. It simply stores the Producer interface
// provided through registration. Flush and Shutdown are no-ops.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
type ManualReader struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is odd to me that we are including the "manual reader" and not the periodic reader in this PR. Can this be added in a subsequent PR after we agree on the Reader design?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have gotten to an impasse on "Just Design" and I think either an implementation to demonstrate how this would work, or some logic in the MetricProducer of how this is used is warranted.

I for one have already seen the use case of the Shutdown method and will have to work to add that code into this.

But I still don't see how ForceFlush works in these implementations. This leads me to believe that I don't think we should be designing these in the absence of some use case, either the minimal implementation or where it is used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to understand this concern about Shutdown and ForceFlush. They way I understand both of them, both method calls pass-through to each of the readers (i.e., exporters). If a reader (i.e., exporter) has nothing applicable to do for flush or shutdown, they'll do nothing. The reason we want Reader, Producer, and the Produce() method to be public, is so that users and/or exporters can choose to perform manual reading simply by implementing the 1-line Register function plus custom Shutdown and ForceFlush logic.

producer
}

var _ Reader = &ManualReader{}
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

// NewManualReader returns an Reader that stores the Producer for
// manual use and returns a configurable `name` as its String(),
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
func NewManualReader() *ManualReader {
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
return &ManualReader{}
}

// Register stores the Producer which enables the caller to read
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
// metrics on demand.
func (mr *ManualReader) register(p producer) {
mr.producer = p
}

// ForceFlush is a no-op, always returns nil.
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
func (mr *ManualReader) ForceFlush(context.Context) error {
return nil
}

// Shutdown is a no-op, always returns nil.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the call to Shutdown, subsequent invocations to Collect are not allowed. SDKs SHOULD return some failure for these calls, if possible.

This should be an operation this method call will perform.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is perfect, as long as we expose the Register() and Produce() methods and the Producer interface. The ManualReader is just a convenience for exporters that read on-demand and have no need of a Shutdown or ForceFlush routine. If an exporter reads manually and needs a ForceFlush or Shutdown, they'll implement the Reader interface directly.

func (mr *ManualReader) Shutdown(context.Context) error {
return nil
}

func (mr *ManualReader) Collect(ctx context.Context) (export.Metrics, error) {
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
if mr.producer == nil {
return export.Metrics{}, ErrReaderNotRegistered
}
return mr.produce(ctx), nil
}

var ErrReaderNotRegistered = fmt.Errorf("reader is not registered")
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
59 changes: 59 additions & 0 deletions sdk/metric/manual_reader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 // import "go.opentelemetry.io/otel/sdk/metric/reader"

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/export"
)

func TestManualReaderNotRegistered(t *testing.T) {
rdr := &ManualReader{}

_, err := rdr.Collect(context.Background())
require.ErrorIs(t, err, ErrReaderNotRegistered)
}

type testProducer struct{}

var testMetrics = export.Metrics{
Scopes: []export.Scope{
{
Library: instrumentation.Library{
Name: "TestLibrary",
Version: "0.0.1-beta1",
},
},
},
}

func (p testProducer) produce(context.Context) export.Metrics {
return testMetrics
}

func TestManualReaderProducer(t *testing.T) {
rdr := &ManualReader{}
rdr.register(testProducer{})

m, err := rdr.Collect(context.Background())
assert.NoError(t, err)
assert.Equal(t, testMetrics, m)
}
92 changes: 92 additions & 0 deletions sdk/metric/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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

import (
"context"

"go.opentelemetry.io/otel/sdk/metric/export"
"go.opentelemetry.io/otel/sdk/metric/view"
)

// Reader is the interface used between the SDK and an
// exporter. Control flow is bi-directional through the
// Reader, since the SDK initiates ForceFlush and Shutdown
// while the initiates collection. The Register() method here
// informs the Reader that it can begin reading, signaling the
// start of bi-directional control flow.
//
// Typically, push-based exporters that are periodic will
// implement PeroidicExporter themselves and construct a
// PeriodicReader to satisfy this interface.
//
// Pull-based exporters will typically implement Register
// themselves, since they read on demand.
jmacd marked this conversation as resolved.
Show resolved Hide resolved
type Reader interface {
// Register is called when the SDK is fully
// configured. The Producer passed allows the
// Reader to begin collecting metrics using its
// Produce() method.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
register(producer)
jmacd marked this conversation as resolved.
Show resolved Hide resolved

Collect(context.Context) (export.Metrics, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be removed, it's not needed if the Register method, Producer type, and Produce method are exposed. When these are exposed, the ManualReader is only needed for simple applications; anything that wants manual reading behavior and also to control flush/shutdown has the option to manually implement the reader themselves, it's very simple.

In my branch, Collect() is an interface method that the (async/sync)x(cumulative/delta) instrument objects implement, used by the producer during Produce()

// Collector is an interface for things that produce Instrument data.
// One instrument may output more than one Instrument data by
// appending to `output`.
type Collector interface {
	Collect(sequence Sequence, output *[]Instrument)
}

In my branch I named what is export here as the package sdk/metric/data, so ^^^ is the data.Collector interface and Collect() collects is how one API-level instrument outputs multiple View-level metric data objects. The user would not use this, it's between the *meter and the internal packages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// ForceFlush is called when MeterProvider.ForceFlush() is called.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
ForceFlush(context.Context) error

// Shutdown is called when MeterProvider.Shutdown() is called.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
Shutdown(context.Context) error
}

// Producer is the interface used to perform collection by the reader.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
type producer interface {
// Produce returns metrics from a single collection.
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
//
// Produce may be called concurrently,
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
//
// The `in` parameter supports re-use of memory from
// one collection to the next. Callers that pass `in`
// will write metrics into the same slices and structs.
//
// When `in` is nil, a new Metrics object is returned.
jmacd marked this conversation as resolved.
Show resolved Hide resolved
produce(context.Context) export.Metrics
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
}
MrAlias marked this conversation as resolved.
Show resolved Hide resolved

type readerConfig struct {
views []view.Config
// TODO: create defaults for instruments after instrument and aggregations
// are defined. #2813, #2827
// defAggr [sdkinstrument.NumKinds]aggregation.Kind
// defTempo [sdkinstrument.NumKinds]aggregation.Temporality
// defI64Cfg [sdkinstrument.NumKinds]aggregator.Config
// defF64Cfg [sdkinstrument.NumKinds]aggregator.Config
}

type ReaderOption interface {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
apply(readerConfig) readerConfig
}

type readerOptionFunc func(readerConfig) readerConfig

func (f readerOptionFunc) apply(cfg readerConfig) readerConfig {
return f(cfg)
}

func WithViews(views ...view.Config) ReaderOption {
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved
return readerOptionFunc(func(cfg readerConfig) readerConfig {
cfg.views = append(cfg.views, views...)
return cfg
})
}
MadVikingGod marked this conversation as resolved.
Show resolved Hide resolved