2023-11-20 18:43:55 +03:00
|
|
|
// Copyright The OpenTelemetry Authors
|
2024-08-26 19:05:54 +03:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2023-11-20 18:43:55 +03:00
|
|
|
|
|
|
|
package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
"go.opentelemetry.io/otel/internal/global"
|
|
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Default periodic reader timing.
|
|
|
|
const (
|
|
|
|
defaultTimeout = time.Millisecond * 30000
|
|
|
|
defaultInterval = time.Millisecond * 60000
|
|
|
|
)
|
|
|
|
|
|
|
|
// periodicReaderConfig contains configuration options for a PeriodicReader.
|
|
|
|
type periodicReaderConfig struct {
|
|
|
|
interval time.Duration
|
|
|
|
timeout time.Duration
|
|
|
|
producers []Producer
|
|
|
|
}
|
|
|
|
|
|
|
|
// newPeriodicReaderConfig returns a periodicReaderConfig configured with
|
|
|
|
// options.
|
|
|
|
func newPeriodicReaderConfig(options []PeriodicReaderOption) periodicReaderConfig {
|
|
|
|
c := periodicReaderConfig{
|
|
|
|
interval: envDuration(envInterval, defaultInterval),
|
|
|
|
timeout: envDuration(envTimeout, defaultTimeout),
|
|
|
|
}
|
|
|
|
for _, o := range options {
|
|
|
|
c = o.applyPeriodic(c)
|
|
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// PeriodicReaderOption applies a configuration option value to a PeriodicReader.
|
|
|
|
type PeriodicReaderOption interface {
|
|
|
|
applyPeriodic(periodicReaderConfig) periodicReaderConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
// periodicReaderOptionFunc applies a set of options to a periodicReaderConfig.
|
|
|
|
type periodicReaderOptionFunc func(periodicReaderConfig) periodicReaderConfig
|
|
|
|
|
|
|
|
// applyPeriodic returns a periodicReaderConfig with option(s) applied.
|
|
|
|
func (o periodicReaderOptionFunc) applyPeriodic(conf periodicReaderConfig) periodicReaderConfig {
|
|
|
|
return o(conf)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithTimeout configures the time a PeriodicReader waits for an export to
|
|
|
|
// complete before canceling it. This includes an export which occurs as part
|
|
|
|
// of Shutdown or ForceFlush if the user passed context does not have a
|
|
|
|
// deadline. If the user passed context does have a deadline, it will be used
|
|
|
|
// instead.
|
|
|
|
//
|
|
|
|
// This option overrides any value set for the
|
|
|
|
// OTEL_METRIC_EXPORT_TIMEOUT environment variable.
|
|
|
|
//
|
|
|
|
// If this option is not used or d is less than or equal to zero, 30 seconds
|
|
|
|
// is used as the default.
|
|
|
|
func WithTimeout(d time.Duration) PeriodicReaderOption {
|
|
|
|
return periodicReaderOptionFunc(func(conf periodicReaderConfig) periodicReaderConfig {
|
|
|
|
if d <= 0 {
|
|
|
|
return conf
|
|
|
|
}
|
|
|
|
conf.timeout = d
|
|
|
|
return conf
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithInterval configures the intervening time between exports for a
|
|
|
|
// PeriodicReader.
|
|
|
|
//
|
|
|
|
// This option overrides any value set for the
|
|
|
|
// OTEL_METRIC_EXPORT_INTERVAL environment variable.
|
|
|
|
//
|
|
|
|
// If this option is not used or d is less than or equal to zero, 60 seconds
|
|
|
|
// is used as the default.
|
|
|
|
func WithInterval(d time.Duration) PeriodicReaderOption {
|
|
|
|
return periodicReaderOptionFunc(func(conf periodicReaderConfig) periodicReaderConfig {
|
|
|
|
if d <= 0 {
|
|
|
|
return conf
|
|
|
|
}
|
|
|
|
conf.interval = d
|
|
|
|
return conf
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPeriodicReader returns a Reader that collects and exports metric data to
|
|
|
|
// the exporter at a defined interval. By default, the returned Reader will
|
|
|
|
// collect and export data every 60 seconds, and will cancel any attempts that
|
|
|
|
// exceed 30 seconds, collect and export combined. The collect and export time
|
|
|
|
// are not counted towards the interval between attempts.
|
|
|
|
//
|
|
|
|
// The Collect method of the returned Reader continues to gather and return
|
|
|
|
// metric data to the user. It will not automatically send that data to the
|
|
|
|
// exporter. That is left to the user to accomplish.
|
|
|
|
func NewPeriodicReader(exporter Exporter, options ...PeriodicReaderOption) *PeriodicReader {
|
|
|
|
conf := newPeriodicReaderConfig(options)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
r := &PeriodicReader{
|
|
|
|
interval: conf.interval,
|
|
|
|
timeout: conf.timeout,
|
|
|
|
exporter: exporter,
|
|
|
|
flushCh: make(chan chan error),
|
|
|
|
cancel: cancel,
|
|
|
|
done: make(chan struct{}),
|
|
|
|
rmPool: sync.Pool{
|
|
|
|
New: func() interface{} {
|
|
|
|
return &metricdata.ResourceMetrics{}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
r.externalProducers.Store(conf.producers)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer func() { close(r.done) }()
|
|
|
|
r.run(ctx, conf.interval)
|
|
|
|
}()
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
// PeriodicReader is a Reader that continuously collects and exports metric
|
|
|
|
// data at a set interval.
|
|
|
|
type PeriodicReader struct {
|
|
|
|
sdkProducer atomic.Value
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
isShutdown bool
|
|
|
|
externalProducers atomic.Value
|
|
|
|
|
|
|
|
interval time.Duration
|
|
|
|
timeout time.Duration
|
|
|
|
exporter Exporter
|
|
|
|
flushCh chan chan error
|
|
|
|
|
|
|
|
done chan struct{}
|
|
|
|
cancel context.CancelFunc
|
|
|
|
shutdownOnce sync.Once
|
|
|
|
|
|
|
|
rmPool sync.Pool
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compile time check the periodicReader implements Reader and is comparable.
|
|
|
|
var _ = map[Reader]struct{}{&PeriodicReader{}: {}}
|
|
|
|
|
|
|
|
// newTicker allows testing override.
|
|
|
|
var newTicker = time.NewTicker
|
|
|
|
|
|
|
|
// run continuously collects and exports metric data at the specified
|
|
|
|
// interval. This will run until ctx is canceled or times out.
|
|
|
|
func (r *PeriodicReader) run(ctx context.Context, interval time.Duration) {
|
|
|
|
ticker := newTicker(interval)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
|
|
|
err := r.collectAndExport(ctx)
|
|
|
|
if err != nil {
|
|
|
|
otel.Handle(err)
|
|
|
|
}
|
|
|
|
case errCh := <-r.flushCh:
|
|
|
|
errCh <- r.collectAndExport(ctx)
|
|
|
|
ticker.Reset(interval)
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// register registers p as the producer of this reader.
|
|
|
|
func (r *PeriodicReader) register(p sdkProducer) {
|
|
|
|
// Only register once. If producer is already set, do nothing.
|
|
|
|
if !r.sdkProducer.CompareAndSwap(nil, produceHolder{produce: p.produce}) {
|
|
|
|
msg := "did not register periodic reader"
|
|
|
|
global.Error(errDuplicateRegister, msg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// temporality reports the Temporality for the instrument kind provided.
|
|
|
|
func (r *PeriodicReader) temporality(kind InstrumentKind) metricdata.Temporality {
|
|
|
|
return r.exporter.Temporality(kind)
|
|
|
|
}
|
|
|
|
|
|
|
|
// aggregation returns what Aggregation to use for kind.
|
|
|
|
func (r *PeriodicReader) aggregation(kind InstrumentKind) Aggregation { // nolint:revive // import-shadow for method scoped by type.
|
|
|
|
return r.exporter.Aggregation(kind)
|
|
|
|
}
|
|
|
|
|
|
|
|
// collectAndExport gather all metric data related to the periodicReader r from
|
|
|
|
// the SDK and exports it with r's exporter.
|
|
|
|
func (r *PeriodicReader) collectAndExport(ctx context.Context) error {
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, r.timeout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
// TODO (#3047): Use a sync.Pool or persistent pointer instead of allocating rm every Collect.
|
|
|
|
rm := r.rmPool.Get().(*metricdata.ResourceMetrics)
|
|
|
|
err := r.Collect(ctx, rm)
|
|
|
|
if err == nil {
|
|
|
|
err = r.export(ctx, rm)
|
|
|
|
}
|
|
|
|
r.rmPool.Put(rm)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Collect gathers all metric data related to the Reader from
|
|
|
|
// the SDK and other Producers and stores the result in rm. The metric
|
|
|
|
// data is not exported to the configured exporter, it is left to the caller to
|
|
|
|
// handle that if desired.
|
|
|
|
//
|
|
|
|
// Collect will return an error if called after shutdown.
|
|
|
|
// Collect will return an error if rm is a nil ResourceMetrics.
|
|
|
|
// Collect will return an error if the context's Done channel is closed.
|
|
|
|
//
|
|
|
|
// This method is safe to call concurrently.
|
|
|
|
func (r *PeriodicReader) Collect(ctx context.Context, rm *metricdata.ResourceMetrics) error {
|
|
|
|
if rm == nil {
|
|
|
|
return errors.New("periodic reader: *metricdata.ResourceMetrics is nil")
|
|
|
|
}
|
|
|
|
// TODO (#3047): When collect is updated to accept output as param, pass rm.
|
|
|
|
return r.collect(ctx, r.sdkProducer.Load(), rm)
|
|
|
|
}
|
|
|
|
|
|
|
|
// collect unwraps p as a produceHolder and returns its produce results.
|
|
|
|
func (r *PeriodicReader) collect(ctx context.Context, p interface{}, rm *metricdata.ResourceMetrics) error {
|
|
|
|
if p == nil {
|
|
|
|
return ErrReaderNotRegistered
|
|
|
|
}
|
|
|
|
|
|
|
|
ph, ok := p.(produceHolder)
|
|
|
|
if !ok {
|
|
|
|
// The atomic.Value is entirely in the periodicReader's control so
|
|
|
|
// this should never happen. In the unforeseen case that this does
|
|
|
|
// happen, return an error instead of panicking so a users code does
|
|
|
|
// not halt in the processes.
|
|
|
|
err := fmt.Errorf("periodic reader: invalid producer: %T", p)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err := ph.produce(ctx, rm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
var errs []error
|
|
|
|
for _, producer := range r.externalProducers.Load().([]Producer) {
|
|
|
|
externalMetrics, err := producer.Produce(ctx)
|
|
|
|
if err != nil {
|
|
|
|
errs = append(errs, err)
|
|
|
|
}
|
|
|
|
rm.ScopeMetrics = append(rm.ScopeMetrics, externalMetrics...)
|
|
|
|
}
|
|
|
|
|
|
|
|
global.Debug("PeriodicReader collection", "Data", rm)
|
|
|
|
|
|
|
|
return unifyErrors(errs)
|
|
|
|
}
|
|
|
|
|
|
|
|
// export exports metric data m using r's exporter.
|
|
|
|
func (r *PeriodicReader) export(ctx context.Context, m *metricdata.ResourceMetrics) error {
|
|
|
|
return r.exporter.Export(ctx, m)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ForceFlush flushes pending telemetry.
|
|
|
|
//
|
|
|
|
// This method is safe to call concurrently.
|
|
|
|
func (r *PeriodicReader) ForceFlush(ctx context.Context) error {
|
|
|
|
// Prioritize the ctx timeout if it is set.
|
|
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
|
|
var cancel context.CancelFunc
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, r.timeout)
|
|
|
|
defer cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
select {
|
|
|
|
case r.flushCh <- errCh:
|
|
|
|
select {
|
|
|
|
case err := <-errCh:
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
close(errCh)
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
case <-r.done:
|
|
|
|
return ErrReaderShutdown
|
|
|
|
case <-ctx.Done():
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
return r.exporter.ForceFlush(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shutdown flushes pending telemetry and then stops the export pipeline.
|
|
|
|
//
|
|
|
|
// This method is safe to call concurrently.
|
|
|
|
func (r *PeriodicReader) Shutdown(ctx context.Context) error {
|
|
|
|
err := ErrReaderShutdown
|
|
|
|
r.shutdownOnce.Do(func() {
|
|
|
|
// Prioritize the ctx timeout if it is set.
|
|
|
|
if _, ok := ctx.Deadline(); !ok {
|
|
|
|
var cancel context.CancelFunc
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, r.timeout)
|
|
|
|
defer cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop the run loop.
|
|
|
|
r.cancel()
|
|
|
|
<-r.done
|
|
|
|
|
|
|
|
// Any future call to Collect will now return ErrReaderShutdown.
|
|
|
|
ph := r.sdkProducer.Swap(produceHolder{
|
|
|
|
produce: shutdownProducer{}.produce,
|
|
|
|
})
|
|
|
|
|
|
|
|
if ph != nil { // Reader was registered.
|
|
|
|
// Flush pending telemetry.
|
|
|
|
m := r.rmPool.Get().(*metricdata.ResourceMetrics)
|
|
|
|
err = r.collect(ctx, ph, m)
|
|
|
|
if err == nil {
|
|
|
|
err = r.export(ctx, m)
|
|
|
|
}
|
|
|
|
r.rmPool.Put(m)
|
|
|
|
}
|
|
|
|
|
|
|
|
sErr := r.exporter.Shutdown(ctx)
|
2024-08-26 19:05:54 +03:00
|
|
|
if err == nil || errors.Is(err, ErrReaderShutdown) {
|
2023-11-20 18:43:55 +03:00
|
|
|
err = sErr
|
|
|
|
}
|
|
|
|
|
|
|
|
r.mu.Lock()
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
r.isShutdown = true
|
|
|
|
// release references to Producer(s)
|
|
|
|
r.externalProducers.Store([]Producer{})
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalLog returns logging data about the PeriodicReader.
|
|
|
|
func (r *PeriodicReader) MarshalLog() interface{} {
|
|
|
|
r.mu.Lock()
|
|
|
|
down := r.isShutdown
|
|
|
|
r.mu.Unlock()
|
|
|
|
return struct {
|
|
|
|
Type string
|
|
|
|
Exporter Exporter
|
|
|
|
Registered bool
|
|
|
|
Shutdown bool
|
|
|
|
Interval time.Duration
|
|
|
|
Timeout time.Duration
|
|
|
|
}{
|
|
|
|
Type: "PeriodicReader",
|
|
|
|
Exporter: r.exporter,
|
|
|
|
Registered: r.sdkProducer.Load() != nil,
|
|
|
|
Shutdown: down,
|
|
|
|
Interval: r.interval,
|
|
|
|
Timeout: r.timeout,
|
|
|
|
}
|
|
|
|
}
|