You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	This is the last PR adding missing linters, adding [errorlint](https://github.com/polyfloyd/go-errorlint). Co-authored-by: Sam Xie <sam@samxie.me>
		
			
				
	
	
		
			371 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			371 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright The OpenTelemetry Authors
 | |
| // SPDX-License-Identifier: Apache-2.0
 | |
| 
 | |
| 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)
 | |
| 		if err == nil || errors.Is(err, ErrReaderShutdown) {
 | |
| 			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,
 | |
| 	}
 | |
| }
 |