diff --git a/CHANGELOG.md b/CHANGELOG.md index 564503d1493..4912a5b27de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Add `NewProducer` to `go.opentelemetry.io/contrib/instrumentation/runtime`, which allows collecting the `go.schedule.duration` histogram metric from the Go runtime. (#5991) + ### Removed - Drop support for [Go 1.21]. (#6046, #6047) diff --git a/instrumentation/runtime/example/doc.go b/instrumentation/runtime/example/doc.go deleted file mode 100644 index 93292afd9ce..00000000000 --- a/instrumentation/runtime/example/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package main provides an example use of the runtime instrumentation. -package main diff --git a/instrumentation/runtime/example/go.mod b/instrumentation/runtime/example/go.mod deleted file mode 100644 index c798d503304..00000000000 --- a/instrumentation/runtime/example/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module go.opentelemetry.io/contrib/instrumentation/runtime/example - -go 1.22 - -replace go.opentelemetry.io/contrib/instrumentation/runtime => ../ - -require ( - go.opentelemetry.io/contrib/instrumentation/runtime v0.54.0 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 - go.opentelemetry.io/otel/sdk v1.29.0 - go.opentelemetry.io/otel/sdk/metric v1.29.0 -) - -require ( - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/sys v0.24.0 // indirect -) diff --git a/instrumentation/runtime/example/go.sum b/instrumentation/runtime/example/go.sum deleted file mode 100644 index b093e28b815..00000000000 --- a/instrumentation/runtime/example/go.sum +++ /dev/null @@ -1,31 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/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.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/runtime/example/main.go b/instrumentation/runtime/example/main.go deleted file mode 100644 index faebd5dd431..00000000000 --- a/instrumentation/runtime/example/main.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -//go:build go1.18 -// +build go1.18 - -package main - -import ( - "context" - "log" - "os" - "os/signal" - "time" - - "go.opentelemetry.io/contrib/instrumentation/runtime" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" - "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.17.0" -) - -var res = resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceName("runtime-instrumentation-example"), -) - -func main() { - exp, err := stdoutmetric.New() - if err != nil { - log.Fatal(err) - } - - // Register the exporter with an SDK via a periodic reader. - read := metric.NewPeriodicReader(exp, metric.WithInterval(1*time.Second)) - provider := metric.NewMeterProvider(metric.WithResource(res), metric.WithReader(read)) - defer func() { - err := provider.Shutdown(context.Background()) - if err != nil { - log.Fatal(err) - } - }() - otel.SetMeterProvider(provider) - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) - defer cancel() - - log.Print("Starting runtime instrumentation:") - err = runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) - if err != nil { - log.Fatal(err) - } - - <-ctx.Done() -} diff --git a/instrumentation/runtime/example_test.go b/instrumentation/runtime/example_test.go new file mode 100644 index 00000000000..08a438bcf6a --- /dev/null +++ b/instrumentation/runtime/example_test.go @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package runtime_test + +import ( + "context" + "log" + "time" + + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/metric" +) + +func Example() { + // This reader is used as a stand-in for a reader that will actually export + // data. See https://pkg.go.dev/go.opentelemetry.io/otel/exporters for + // exporters that can be used as or with readers. + reader := metric.NewManualReader( + // Add the runtime producer to get histograms from the Go runtime. + metric.WithProducer(runtime.NewProducer()), + ) + provider := metric.NewMeterProvider(metric.WithReader(reader)) + defer func() { + err := provider.Shutdown(context.Background()) + if err != nil { + log.Fatal(err) + } + }() + otel.SetMeterProvider(provider) + + // Start go runtime metric collection. + err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)) + if err != nil { + log.Fatal(err) + } +} diff --git a/instrumentation/runtime/go.mod b/instrumentation/runtime/go.mod index bf605c43768..c57593af323 100644 --- a/instrumentation/runtime/go.mod +++ b/instrumentation/runtime/go.mod @@ -6,13 +6,17 @@ require ( github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/metric v1.29.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/sdk/metric v1.29.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/sys v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/instrumentation/runtime/go.sum b/instrumentation/runtime/go.sum index f196cac294d..f646bd1a6f2 100644 --- a/instrumentation/runtime/go.sum +++ b/instrumentation/runtime/go.sum @@ -7,6 +7,8 @@ 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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -15,8 +17,14 @@ go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/instrumentation/runtime/options.go b/instrumentation/runtime/options.go index 30046ab3509..580dc5dcd53 100644 --- a/instrumentation/runtime/options.go +++ b/instrumentation/runtime/options.go @@ -27,6 +27,13 @@ type Option interface { apply(*config) } +// ProducerOption supports configuring optional settings for runtime metrics using a +// metric producer in addition to standard instrumentation. +type ProducerOption interface { + Option + applyProducer(*config) +} + // DefaultMinimumReadMemStatsInterval is the default minimum interval // between calls to runtime.ReadMemStats(). Use the // WithMinimumReadMemStatsInterval() option to modify this setting in @@ -48,6 +55,8 @@ func (o minimumReadMemStatsIntervalOption) apply(c *config) { } } +func (o minimumReadMemStatsIntervalOption) applyProducer(c *config) { o.apply(c) } + // WithMeterProvider sets the Metric implementation to use for // reporting. If this option is not used, the global metric.MeterProvider // will be used. `provider` must be non-nil. @@ -66,11 +75,25 @@ func (o metricProviderOption) apply(c *config) { // newConfig computes a config from the supplied Options. func newConfig(opts ...Option) config { c := config{ - MeterProvider: otel.GetMeterProvider(), - MinimumReadMemStatsInterval: DefaultMinimumReadMemStatsInterval, + MeterProvider: otel.GetMeterProvider(), } for _, opt := range opts { opt.apply(&c) } + if c.MinimumReadMemStatsInterval <= 0 { + c.MinimumReadMemStatsInterval = DefaultMinimumReadMemStatsInterval + } + return c +} + +// newConfig computes a config from the supplied ProducerOptions. +func newProducerConfig(opts ...ProducerOption) config { + c := config{} + for _, opt := range opts { + opt.applyProducer(&c) + } + if c.MinimumReadMemStatsInterval <= 0 { + c.MinimumReadMemStatsInterval = DefaultMinimumReadMemStatsInterval + } return c } diff --git a/instrumentation/runtime/producer.go b/instrumentation/runtime/producer.go new file mode 100644 index 00000000000..0697208db98 --- /dev/null +++ b/instrumentation/runtime/producer.go @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime" + +import ( + "context" + "fmt" + "math" + "runtime/metrics" + "sync" + "time" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +var startTime time.Time + +func init() { + startTime = time.Now() +} + +var histogramMetrics = []string{goSchedLatencies} + +// Producer is a metric.Producer, which provides precomputed histogram metrics from the go runtime. +type Producer struct { + lock sync.Mutex + collector *goCollector +} + +var _ metric.Producer = (*Producer)(nil) + +// NewProducer creates a Producer which provides precomputed histogram metrics from the go runtime. +func NewProducer(opts ...ProducerOption) *Producer { + c := newProducerConfig(opts...) + return &Producer{ + collector: newCollector(c.MinimumReadMemStatsInterval, histogramMetrics), + } +} + +// Produce returns precomputed histogram metrics from the go runtime, or an error if unsuccessful. +func (p *Producer) Produce(context.Context) ([]metricdata.ScopeMetrics, error) { + p.lock.Lock() + p.collector.refresh() + schedHist := p.collector.getHistogram(goSchedLatencies) + p.lock.Unlock() + // Use the last collection time (which may or may not be now) for the timestamp. + histDp := convertRuntimeHistogram(schedHist, p.collector.lastCollect) + if len(histDp) == 0 { + return nil, fmt.Errorf("unable to obtain go.schedule.duration metric from the runtime") + } + return []metricdata.ScopeMetrics{ + { + Scope: instrumentation.Scope{ + Name: ScopeName, + Version: Version(), + }, + Metrics: []metricdata.Metrics{ + { + Name: "go.schedule.duration", + Description: "The time goroutines have spent in the scheduler in a runnable state before actually running.", + Unit: "s", + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: histDp, + }, + }, + }, + }, + }, nil +} + +var emptySet = attribute.EmptySet() + +func convertRuntimeHistogram(runtimeHist *metrics.Float64Histogram, ts time.Time) []metricdata.HistogramDataPoint[float64] { + if runtimeHist == nil { + return nil + } + bounds := runtimeHist.Buckets + counts := runtimeHist.Counts + if len(bounds) < 2 { + // runtime histograms are guaranteed to have at least two bucket boundaries. + return nil + } + // trim the first bucket since it is a lower bound. OTel histogram boundaries only have an upper bound. + bounds = bounds[1:] + if bounds[len(bounds)-1] == math.Inf(1) { + // trim the last bucket if it is +Inf, since the +Inf boundary is implicit in OTel. + bounds = bounds[:len(bounds)-1] + } else { + // if the last bucket is not +Inf, append an extra zero count since + // the implicit +Inf bucket won't have any observations. + counts = append(counts, 0) + } + count := uint64(0) + sum := float64(0) + for i, c := range counts { + count += c + // This computed sum is an underestimate, since it assumes each + // observation happens at the bucket's lower bound. + if i > 0 && count != 0 { + sum += bounds[i-1] * float64(count) + } + } + + return []metricdata.HistogramDataPoint[float64]{ + { + StartTime: startTime, + Count: count, + Sum: sum, + Time: ts, + Bounds: bounds, + BucketCounts: counts, + Attributes: *emptySet, + }, + } +} diff --git a/instrumentation/runtime/producer_test.go b/instrumentation/runtime/producer_test.go new file mode 100644 index 00000000000..1e467928c5e --- /dev/null +++ b/instrumentation/runtime/producer_test.go @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime" + +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" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" +) + +func TestNewProducer(t *testing.T) { + reader := metric.NewManualReader(metric.WithProducer(NewProducer())) + _ = metric.NewMeterProvider(metric.WithReader(reader)) + rm := metricdata.ResourceMetrics{} + err := reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + require.Len(t, rm.ScopeMetrics[0].Metrics, 1) + + expectedScopeMetric := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/contrib/instrumentation/runtime", + Version: Version(), + }, + Metrics: []metricdata.Metrics{ + { + Name: "go.schedule.duration", + Description: "The time goroutines have spent in the scheduler in a runnable state before actually running.", + Unit: "s", + Data: metricdata.Histogram[float64]{ + Temporality: metricdata.CumulativeTemporality, + DataPoints: []metricdata.HistogramDataPoint[float64]{ + {}, + }, + }, + }, + }, + } + metricdatatest.AssertEqual(t, expectedScopeMetric, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) +} diff --git a/instrumentation/runtime/runtime.go b/instrumentation/runtime/runtime.go index f50d1189579..2c4efefe5f2 100644 --- a/instrumentation/runtime/runtime.go +++ b/instrumentation/runtime/runtime.go @@ -10,7 +10,6 @@ import ( "sync" "time" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" @@ -32,17 +31,12 @@ const ( goGoroutines = "/sched/goroutines:goroutines" goMaxProcs = "/sched/gomaxprocs:threads" goConfigGC = "/gc/gogc:percent" + goSchedLatencies = "/sched/latencies:seconds" ) // Start initializes reporting of runtime metrics using the supplied config. func Start(opts ...Option) error { c := newConfig(opts...) - if c.MinimumReadMemStatsInterval < 0 { - c.MinimumReadMemStatsInterval = DefaultMinimumReadMemStatsInterval - } - if c.MeterProvider == nil { - c.MeterProvider = otel.GetMeterProvider() - } meter := c.MeterProvider.Meter( ScopeName, metric.WithInstrumentationVersion(Version()), @@ -121,28 +115,28 @@ func Start(opts ...Option) error { stackMemoryOpt := metric.WithAttributeSet( attribute.NewSet(attribute.String("go.memory.type", "stack")), ) - collector := newCollector(c.MinimumReadMemStatsInterval) + collector := newCollector(c.MinimumReadMemStatsInterval, runtimeMetrics) var lock sync.Mutex _, err = meter.RegisterCallback( func(ctx context.Context, o metric.Observer) error { lock.Lock() defer lock.Unlock() collector.refresh() - stackMemory := collector.get(goHeapMemory) + stackMemory := collector.getInt(goHeapMemory) o.ObserveInt64(memoryUsedInstrument, stackMemory, stackMemoryOpt) - totalMemory := collector.get(goTotalMemory) - collector.get(goMemoryReleased) + totalMemory := collector.getInt(goTotalMemory) - collector.getInt(goMemoryReleased) otherMemory := totalMemory - stackMemory o.ObserveInt64(memoryUsedInstrument, otherMemory, otherMemoryOpt) // Only observe the limit metric if a limit exists - if limit := collector.get(goMemoryLimit); limit != math.MaxInt64 { + if limit := collector.getInt(goMemoryLimit); limit != math.MaxInt64 { o.ObserveInt64(memoryLimitInstrument, limit) } - o.ObserveInt64(memoryAllocatedInstrument, collector.get(goMemoryAllocated)) - o.ObserveInt64(memoryAllocationsInstrument, collector.get(goMemoryAllocations)) - o.ObserveInt64(memoryGCGoalInstrument, collector.get(goMemoryGoal)) - o.ObserveInt64(goroutineCountInstrument, collector.get(goGoroutines)) - o.ObserveInt64(processorLimitInstrument, collector.get(goMaxProcs)) - o.ObserveInt64(gogcConfigInstrument, collector.get(goConfigGC)) + o.ObserveInt64(memoryAllocatedInstrument, collector.getInt(goMemoryAllocated)) + o.ObserveInt64(memoryAllocationsInstrument, collector.getInt(goMemoryAllocations)) + o.ObserveInt64(memoryGCGoalInstrument, collector.getInt(goMemoryGoal)) + o.ObserveInt64(goroutineCountInstrument, collector.getInt(goGoroutines)) + o.ObserveInt64(processorLimitInstrument, collector.getInt(goMaxProcs)) + o.ObserveInt64(gogcConfigInstrument, collector.getInt(goConfigGC)) return nil }, memoryUsedInstrument, @@ -157,7 +151,6 @@ func Start(opts ...Option) error { if err != nil { return err } - // TODO (#5655) support go.schedule.duration return nil } @@ -188,19 +181,19 @@ type goCollector struct { sampleMap map[string]*metrics.Sample } -func newCollector(minimumInterval time.Duration) *goCollector { +func newCollector(minimumInterval time.Duration, metricNames []string) *goCollector { g := &goCollector{ - sampleBuffer: make([]metrics.Sample, 0, len(runtimeMetrics)), - sampleMap: make(map[string]*metrics.Sample, len(runtimeMetrics)), + sampleBuffer: make([]metrics.Sample, 0, len(metricNames)), + sampleMap: make(map[string]*metrics.Sample, len(metricNames)), minimumInterval: minimumInterval, now: time.Now, } - for _, runtimeMetric := range runtimeMetrics { - g.sampleBuffer = append(g.sampleBuffer, metrics.Sample{Name: runtimeMetric}) + for _, metricName := range metricNames { + g.sampleBuffer = append(g.sampleBuffer, metrics.Sample{Name: metricName}) // sampleMap references a position in the sampleBuffer slice. If an // element is appended to sampleBuffer, it must be added to sampleMap // for the sample to be accessible in sampleMap. - g.sampleMap[runtimeMetric] = &g.sampleBuffer[len(g.sampleBuffer)-1] + g.sampleMap[metricName] = &g.sampleBuffer[len(g.sampleBuffer)-1] } return g } @@ -216,7 +209,7 @@ func (g *goCollector) refresh() { g.lastCollect = now } -func (g *goCollector) get(name string) int64 { +func (g *goCollector) getInt(name string) int64 { if s, ok := g.sampleMap[name]; ok && s.Value.Kind() == metrics.KindUint64 { v := s.Value.Uint64() if v > math.MaxInt64 { @@ -226,3 +219,10 @@ func (g *goCollector) get(name string) int64 { } return 0 } + +func (g *goCollector) getHistogram(name string) *metrics.Float64Histogram { + if s, ok := g.sampleMap[name]; ok && s.Value.Kind() == metrics.KindFloat64Histogram { + return s.Value.Float64Histogram() + } + return nil +} diff --git a/instrumentation/runtime/runtime_test.go b/instrumentation/runtime/runtime_test.go index 8b27e347d27..78b3b52b784 100644 --- a/instrumentation/runtime/runtime_test.go +++ b/instrumentation/runtime/runtime_test.go @@ -4,40 +4,47 @@ package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime" import ( + "context" + "fmt" + "math" + "runtime/debug" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" ) func TestRefreshGoCollector(t *testing.T) { // buffer for allocating memory var buffer [][]byte - collector := newCollector(10 * time.Second) + collector := newCollector(10*time.Second, runtimeMetrics) testClock := newClock() collector.now = testClock.now // before the first refresh, all counters are zero - assert.Zero(t, collector.get(goMemoryAllocations)) + assert.Zero(t, collector.getInt(goMemoryAllocations)) // after the first refresh, counters are non-zero buffer = allocateMemory(buffer) collector.refresh() - initialAllocations := collector.get(goMemoryAllocations) + initialAllocations := collector.getInt(goMemoryAllocations) assert.NotZero(t, initialAllocations) // if less than the refresh time has elapsed, the value is not updated // on refresh. testClock.increment(9 * time.Second) collector.refresh() buffer = allocateMemory(buffer) - assert.Equal(t, initialAllocations, collector.get(goMemoryAllocations)) + assert.Equal(t, initialAllocations, collector.getInt(goMemoryAllocations)) // if greater than the refresh time has elapsed, the value changes. testClock.increment(2 * time.Second) collector.refresh() _ = allocateMemory(buffer) - assert.NotEqual(t, initialAllocations, collector.get(goMemoryAllocations)) -} - -func allocateMemory(buffer [][]byte) [][]byte { - return append(buffer, make([]byte, 1000000)) + assert.NotEqual(t, initialAllocations, collector.getInt(goMemoryAllocations)) } func newClock() *clock { @@ -51,3 +58,138 @@ type clock struct { func (c *clock) now() time.Time { return c.current } func (c *clock) increment(d time.Duration) { c.current = c.current.Add(d) } + +func TestRuntimeWithLimit(t *testing.T) { + // buffer for allocating memory + var buffer [][]byte + _ = allocateMemory(buffer) + t.Setenv("OTEL_GO_X_DEPRECATED_RUNTIME_METRICS", "false") + debug.SetMemoryLimit(1234567890) + // reset to default + defer debug.SetMemoryLimit(math.MaxInt64) + + reader := metric.NewManualReader() + mp := metric.NewMeterProvider(metric.WithReader(reader)) + err := Start(WithMeterProvider(mp)) + assert.NoError(t, err) + rm := metricdata.ResourceMetrics{} + err = reader.Collect(context.Background(), &rm) + assert.NoError(t, err) + require.Len(t, rm.ScopeMetrics, 1) + require.Len(t, rm.ScopeMetrics[0].Metrics, 8) + + expectedScopeMetric := metricdata.ScopeMetrics{ + Scope: instrumentation.Scope{ + Name: "go.opentelemetry.io/contrib/instrumentation/runtime", + Version: Version(), + }, + Metrics: []metricdata.Metrics{ + { + Name: "go.memory.used", + Description: "Memory used by the Go runtime.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{ + { + Attributes: attribute.NewSet(attribute.String("go.memory.type", "stack")), + }, + { + Attributes: attribute.NewSet(attribute.String("go.memory.type", "other")), + }, + }, + }, + }, + { + Name: "go.memory.limit", + Description: "Go runtime memory limit configured by the user, if a limit exists.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.memory.allocated", + Description: "Memory allocated to the heap by the application.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.memory.allocations", + Description: "Count of allocations to the heap by the application.", + Unit: "{allocation}", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: true, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.memory.gc.goal", + Description: "Heap size target for the end of the GC cycle.", + Unit: "By", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.goroutine.count", + Description: "Count of live goroutines.", + Unit: "{goroutine}", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.processor.limit", + Description: "The number of OS threads that can execute user-level Go code simultaneously.", + Unit: "{thread}", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + { + Name: "go.config.gogc", + Description: "Heap size target percentage configured by the user, otherwise 100.", + Unit: "%", + Data: metricdata.Sum[int64]{ + Temporality: metricdata.CumulativeTemporality, + IsMonotonic: false, + DataPoints: []metricdata.DataPoint[int64]{{}}, + }, + }, + }, + } + metricdatatest.AssertEqual(t, expectedScopeMetric, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) + assertNonZeroValues(t, rm.ScopeMetrics[0]) +} + +func assertNonZeroValues(t *testing.T, sm metricdata.ScopeMetrics) { + for _, m := range sm.Metrics { + switch a := m.Data.(type) { + case metricdata.Sum[int64]: + for _, dp := range a.DataPoints { + assert.True(t, dp.Value > 0, fmt.Sprintf("Metric %q should have a non-zero value for point with attributes %+v", m.Name, dp.Attributes)) + } + default: + t.Fatalf("unexpected data type %v", a) + } + } +} + +func allocateMemory(buffer [][]byte) [][]byte { + return append(buffer, make([]byte, 1000000)) +} diff --git a/instrumentation/runtime/test/go.mod b/instrumentation/runtime/test/go.mod deleted file mode 100644 index 98ce8eb7479..00000000000 --- a/instrumentation/runtime/test/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module go.opentelemetry.io/contrib/instrumentation/runtime/test - -go 1.22 - -require ( - github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/contrib/instrumentation/runtime v0.54.0 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/sdk v1.29.0 - go.opentelemetry.io/otel/sdk/metric v1.29.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/sys v0.24.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace go.opentelemetry.io/contrib/instrumentation/runtime => ../ diff --git a/instrumentation/runtime/test/go.sum b/instrumentation/runtime/test/go.sum deleted file mode 100644 index f646bd1a6f2..00000000000 --- a/instrumentation/runtime/test/go.sum +++ /dev/null @@ -1,31 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/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.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= -go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= -go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= -go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/runtime/test/runtime_test.go b/instrumentation/runtime/test/runtime_test.go deleted file mode 100644 index 936df2d54f5..00000000000 --- a/instrumentation/runtime/test/runtime_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime/test" - -import ( - "context" - "fmt" - "math" - "runtime/debug" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "go.opentelemetry.io/contrib/instrumentation/runtime" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/sdk/instrumentation" - "go.opentelemetry.io/otel/sdk/metric" - "go.opentelemetry.io/otel/sdk/metric/metricdata" - "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest" -) - -func TestRuntimeWithLimit(t *testing.T) { - // buffer for allocating memory - var buffer [][]byte - _ = allocateMemory(buffer) - t.Setenv("OTEL_GO_X_DEPRECATED_RUNTIME_METRICS", "false") - debug.SetMemoryLimit(1234567890) - // reset to default - defer debug.SetMemoryLimit(math.MaxInt64) - - reader := metric.NewManualReader() - mp := metric.NewMeterProvider(metric.WithReader(reader)) - err := runtime.Start(runtime.WithMeterProvider(mp)) - assert.NoError(t, err) - rm := metricdata.ResourceMetrics{} - err = reader.Collect(context.Background(), &rm) - assert.NoError(t, err) - require.Len(t, rm.ScopeMetrics, 1) - require.Len(t, rm.ScopeMetrics[0].Metrics, 8) - - expectedScopeMetric := metricdata.ScopeMetrics{ - Scope: instrumentation.Scope{ - Name: "go.opentelemetry.io/contrib/instrumentation/runtime", - Version: runtime.Version(), - }, - Metrics: []metricdata.Metrics{ - { - Name: "go.memory.used", - Description: "Memory used by the Go runtime.", - Unit: "By", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{ - { - Attributes: attribute.NewSet(attribute.String("go.memory.type", "stack")), - }, - { - Attributes: attribute.NewSet(attribute.String("go.memory.type", "other")), - }, - }, - }, - }, - { - Name: "go.memory.limit", - Description: "Go runtime memory limit configured by the user, if a limit exists.", - Unit: "By", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.memory.allocated", - Description: "Memory allocated to the heap by the application.", - Unit: "By", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: true, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.memory.allocations", - Description: "Count of allocations to the heap by the application.", - Unit: "{allocation}", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: true, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.memory.gc.goal", - Description: "Heap size target for the end of the GC cycle.", - Unit: "By", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.goroutine.count", - Description: "Count of live goroutines.", - Unit: "{goroutine}", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.processor.limit", - Description: "The number of OS threads that can execute user-level Go code simultaneously.", - Unit: "{thread}", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - { - Name: "go.config.gogc", - Description: "Heap size target percentage configured by the user, otherwise 100.", - Unit: "%", - Data: metricdata.Sum[int64]{ - Temporality: metricdata.CumulativeTemporality, - IsMonotonic: false, - DataPoints: []metricdata.DataPoint[int64]{{}}, - }, - }, - }, - } - metricdatatest.AssertEqual(t, expectedScopeMetric, rm.ScopeMetrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) - assertNonZeroValues(t, rm.ScopeMetrics[0]) -} - -func assertNonZeroValues(t *testing.T, sm metricdata.ScopeMetrics) { - for _, m := range sm.Metrics { - switch a := m.Data.(type) { - case metricdata.Sum[int64]: - for _, dp := range a.DataPoints { - assert.True(t, dp.Value > 0, fmt.Sprintf("Metric %q should have a non-zero value for point with attributes %+v", m.Name, dp.Attributes)) - } - default: - t.Fatalf("unexpected data type %v", a) - } - } -} - -func allocateMemory(buffer [][]byte) [][]byte { - return append(buffer, make([]byte, 1000000)) -} diff --git a/versions.yaml b/versions.yaml index f82048eec51..d7902caf28d 100644 --- a/versions.yaml +++ b/versions.yaml @@ -60,8 +60,6 @@ module-sets: - go.opentelemetry.io/contrib/instrumentation/host - go.opentelemetry.io/contrib/instrumentation/host/example - go.opentelemetry.io/contrib/instrumentation/runtime - - go.opentelemetry.io/contrib/instrumentation/runtime/example - - go.opentelemetry.io/contrib/instrumentation/runtime/test - go.opentelemetry.io/contrib/zpages experimental-samplers: version: v0.23.0