Skip to content

MeterProvider: Excessive creation of metrics instruments #2820

Closed
@aslatter

Description

@aslatter

Acknowledgements

Describe the bug

When supplying a MeterProvider to the s3 client, the underlying retryer seems to be creating a metrics-instrument on every API call.

Following the OTEL model (and other similar metrics-libraries) I would have expected instruments to be creating once, cached, and then getting new observations on existing instruments on each API call.

For example, from the OTEL documentation on instrumenting a Go application: https://opentelemetry.io/docs/languages/go/instrumentation/#using-counters

Click here!
import (
	"net/http"

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

func init() {
	apiCounter, err := meter.Int64Counter(
		"api.counter",
		metric.WithDescription("Number of API calls."),
		metric.WithUnit("{call}"),
	)
	if err != nil {
		panic(err)
	}
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		apiCounter.Add(r.Context(), 1)

		// do some work in an API call
	})
}

Excerpt of my test code:

	ctx := context.Background()

	cfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return fmt.Errorf("loading aws config: %s", err)
	}

	// setup demo meter provider (which logs calls)

	s3c := s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.MeterProvider = &demoMeterProvider{}
	})

	// make a few API calls

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

Full code sample:

Click here!
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/smithy-go/metrics"
)

func main() {
	if err := mainErr(); err != nil {
		fmt.Fprintf(os.Stderr, "error: %s\n", err)
		os.Exit(1)
	}
}

func mainErr() error {

	ctx := context.Background()

	cfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return fmt.Errorf("loading aws config: %s", err)
	}

	// setup demo meter provider (which logs calls)

	s3c := s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.MeterProvider = &demoMeterProvider{}
	})

	// make a few API calls

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

	_, err = s3c.ListBuckets(ctx, &s3.ListBucketsInput{})
	if err != nil {
		return fmt.Errorf("list buckets: %s", err)
	}

	return nil
}

type demoMeterProvider struct{}

// Meter implements metrics.MeterProvider.
func (d *demoMeterProvider) Meter(scope string, opts ...metrics.MeterOption) metrics.Meter {
	return &demoMeter{scope}
}

type demoMeter struct {
	scope string
}

// Float64AsyncCounter implements metrics.Meter.
func (d *demoMeter) Float64AsyncCounter(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Float64AsyncCounter", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64AsyncGauge implements metrics.Meter.
func (d *demoMeter) Float64AsyncGauge(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Float64AsyncGauge", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64AsyncUpDownCounter implements metrics.Meter.
func (d *demoMeter) Float64AsyncUpDownCounter(name string, callback metrics.Float64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Float64AsyncUpDownCounter", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64Counter implements metrics.Meter.
func (d *demoMeter) Float64Counter(name string, opts ...metrics.InstrumentOption) (metrics.Float64Counter, error) {
	d.logInstrument("Float64Counter", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64Gauge implements metrics.Meter.
func (d *demoMeter) Float64Gauge(name string, opts ...metrics.InstrumentOption) (metrics.Float64Gauge, error) {
	d.logInstrument("Float64Gauge", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64Histogram implements metrics.Meter.
func (d *demoMeter) Float64Histogram(name string, opts ...metrics.InstrumentOption) (metrics.Float64Histogram, error) {
	d.logInstrument("Float64Histogram", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Float64UpDownCounter implements metrics.Meter.
func (d *demoMeter) Float64UpDownCounter(name string, opts ...metrics.InstrumentOption) (metrics.Float64UpDownCounter, error) {
	d.logInstrument("Float64UpDownCounter", name)
	return &demoFloatInstrument{d.scope, name}, nil
}

// Int64AsyncCounter implements metrics.Meter.
func (d *demoMeter) Int64AsyncCounter(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Int64AsyncCounter", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64AsyncGauge implements metrics.Meter.
func (d *demoMeter) Int64AsyncGauge(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Int64AsyncGauge", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64AsyncUpDownCounter implements metrics.Meter.
func (d *demoMeter) Int64AsyncUpDownCounter(name string, callback metrics.Int64Callback, opts ...metrics.InstrumentOption) (metrics.AsyncInstrument, error) {
	d.logInstrument("Int64AsyncUpDownCounter", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64Counter implements metrics.Meter.
func (d *demoMeter) Int64Counter(name string, opts ...metrics.InstrumentOption) (metrics.Int64Counter, error) {
	d.logInstrument("Int64Counter", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64Gauge implements metrics.Meter.
func (d *demoMeter) Int64Gauge(name string, opts ...metrics.InstrumentOption) (metrics.Int64Gauge, error) {
	d.logInstrument("Int64Gauge", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64Histogram implements metrics.Meter.
func (d *demoMeter) Int64Histogram(name string, opts ...metrics.InstrumentOption) (metrics.Int64Histogram, error) {
	d.logInstrument("Int64Histogram", name)
	return &demoIntInstrument{d.scope, name}, nil
}

// Int64UpDownCounter implements metrics.Meter.
func (d *demoMeter) Int64UpDownCounter(name string, opts ...metrics.InstrumentOption) (metrics.Int64UpDownCounter, error) {
	d.logInstrument("Int64UpDownCounter", name)
	return &demoIntInstrument{d.scope, name}, nil
}

func (d *demoMeter) logInstrument(typ, name string) {
	log.Printf("creating %s instrument: %s", typ, name)
}

type demoFloatInstrument struct {
	scope string
	name  string
}

// Record implements metrics.Float64Histogram.
func (d *demoFloatInstrument) Record(context.Context, float64, ...metrics.RecordMetricOption) {
	log.Printf("%s Record", d.name)
}

// Sample implements metrics.Float64Gauge.
func (d *demoFloatInstrument) Sample(context.Context, float64, ...metrics.RecordMetricOption) {
	log.Printf("%s Sample", d.name)
}

// Add implements metrics.Float64Counter.
func (d *demoFloatInstrument) Add(context.Context, float64, ...metrics.RecordMetricOption) {
	log.Printf("%s Add", d.name)
}

// Stop implements metrics.AsyncInstrument.
func (d *demoFloatInstrument) Stop() {
	log.Printf("%s Stop", d.name)
}

type demoIntInstrument struct {
	scope string
	name  string
}

// Record implements metrics.Int64Histogram.
func (d *demoIntInstrument) Record(context.Context, int64, ...metrics.RecordMetricOption) {
	log.Printf("%s Record", d.name)
}

// Sample implements metrics.Int64Gauge.
func (d *demoIntInstrument) Sample(context.Context, int64, ...metrics.RecordMetricOption) {
	log.Printf("%s Sample", d.name)
}

// Add implements metrics.Int64Counter.
func (d *demoIntInstrument) Add(context.Context, int64, ...metrics.RecordMetricOption) {
	log.Printf("%s Add", d.name)
}

// Stop implements metrics.AsyncInstrument.
func (d *demoIntInstrument) Stop() {
	log.Printf("%s Stop", d.name)
}

Regression Issue

  • Select this option if this issue appears to be a regression.

Expected Behavior

I expect my demo-meter-provider to log each instrument creation only once.

Current Behavior

The instruments were creating three times - once per API call I made to the S3 service:

Click here!
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.duration
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.serialization_duration
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.auth.resolve_identity_duration
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.resolve_endpoint_duration
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.auth.signing_duration
2024/10/04 13:32:26 creating Float64Histogram instrument: client.call.deserialization_duration
2024/10/04 13:32:26 client.call.serialization_duration Record
2024/10/04 13:32:27 client.call.auth.resolve_identity_duration Record
2024/10/04 13:32:27 client.call.resolve_endpoint_duration Record
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.attempts
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.errors
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.attempt_duration
2024/10/04 13:32:27 client.call.attempts Add
2024/10/04 13:32:27 client.call.auth.signing_duration Record
2024/10/04 13:32:27 client.call.deserialization_duration Record
2024/10/04 13:32:27 client.call.attempt_duration Record
2024/10/04 13:32:27 client.call.duration Record
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.serialization_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.auth.resolve_identity_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.resolve_endpoint_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.auth.signing_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.deserialization_duration
2024/10/04 13:32:27 client.call.serialization_duration Record
2024/10/04 13:32:27 client.call.auth.resolve_identity_duration Record
2024/10/04 13:32:27 client.call.resolve_endpoint_duration Record
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.attempts
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.errors
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.attempt_duration
2024/10/04 13:32:27 client.call.attempts Add
2024/10/04 13:32:27 client.call.auth.signing_duration Record
2024/10/04 13:32:27 client.call.deserialization_duration Record
2024/10/04 13:32:27 client.call.attempt_duration Record
2024/10/04 13:32:27 client.call.duration Record
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.serialization_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.auth.resolve_identity_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.resolve_endpoint_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.auth.signing_duration
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.deserialization_duration
2024/10/04 13:32:27 client.call.serialization_duration Record
2024/10/04 13:32:27 client.call.auth.resolve_identity_duration Record
2024/10/04 13:32:27 client.call.resolve_endpoint_duration Record
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.attempts
2024/10/04 13:32:27 creating Int64Counter instrument: client.call.errors
2024/10/04 13:32:27 creating Float64Histogram instrument: client.call.attempt_duration
2024/10/04 13:32:27 client.call.attempts Add
2024/10/04 13:32:27 client.call.auth.signing_duration Record
2024/10/04 13:32:27 client.call.deserialization_duration Record
2024/10/04 13:32:27 client.call.attempt_duration Record
2024/10/04 13:32:27 client.call.duration Record

Reproduction Steps

$ AWS_REGION=us-east-1 go run ./

Possible Solution

No response

Additional Information/Context

No response

AWS Go SDK V2 Module Versions Used

$ grep github.com/aws/ go.mod
	github.com/aws/aws-sdk-go-v2/config v1.27.40
	github.com/aws/aws-sdk-go-v2/service/s3 v1.64.1
	github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect
	github.com/aws/aws-sdk-go-v2/credentials v1.17.38 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.18 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.18 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.20 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.18 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.23.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.4 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.31.4 // indirect
	github.com/aws/smithy-go v1.21.0 // indirect

Compiler and Version used

go version go1.23.1 linux/amd64

Operating System and version

Arch Linux x86_64, Linux 6.10.10-arch1-1

Metadata

Metadata

Assignees

Labels

bugThis issue is a bug.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions