Skip to content

Commit

Permalink
implement new runtime metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
dashpole committed Jun 18, 2024
1 parent 9a4f5db commit 2883e51
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 65 deletions.
76 changes: 76 additions & 0 deletions instrumentation/runtime/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"

import (
"time"

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

// config contains optional settings for reporting runtime metrics.
type config struct {
// MinimumReadMemStatsInterval sets the minimum interval
// between calls to runtime.ReadMemStats(). Negative values
// are ignored.
MinimumReadMemStatsInterval time.Duration

// MeterProvider sets the metric.MeterProvider. If nil, the global
// Provider will be used.
MeterProvider metric.MeterProvider
}

// Option supports configuring optional settings for runtime metrics.
type Option interface {
apply(*config)
}

// DefaultMinimumReadMemStatsInterval is the default minimum interval
// between calls to runtime.ReadMemStats(). Use the
// WithMinimumReadMemStatsInterval() option to modify this setting in
// Start().
const DefaultMinimumReadMemStatsInterval time.Duration = 15 * time.Second

// WithMinimumReadMemStatsInterval sets a minimum interval between calls to
// runtime.ReadMemStats(), which is a relatively expensive call to make
// frequently. This setting is ignored when `d` is negative.
func WithMinimumReadMemStatsInterval(d time.Duration) Option {
return minimumReadMemStatsIntervalOption(d)

Check warning on line 40 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L39-L40

Added lines #L39 - L40 were not covered by tests
}

type minimumReadMemStatsIntervalOption time.Duration

func (o minimumReadMemStatsIntervalOption) apply(c *config) {
if o >= 0 {
c.MinimumReadMemStatsInterval = time.Duration(o)

Check warning on line 47 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L45-L47

Added lines #L45 - L47 were not covered by tests
}
}

// 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.
func WithMeterProvider(provider metric.MeterProvider) Option {
return metricProviderOption{provider}

Check warning on line 55 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L54-L55

Added lines #L54 - L55 were not covered by tests
}

type metricProviderOption struct{ metric.MeterProvider }

func (o metricProviderOption) apply(c *config) {
if o.MeterProvider != nil {
c.MeterProvider = o.MeterProvider

Check warning on line 62 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L60-L62

Added lines #L60 - L62 were not covered by tests
}
}

// newConfig computes a config from the supplied Options.
func newConfig(opts ...Option) config {
c := config{
MeterProvider: otel.GetMeterProvider(),
MinimumReadMemStatsInterval: DefaultMinimumReadMemStatsInterval,

Check warning on line 70 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L67-L70

Added lines #L67 - L70 were not covered by tests
}
for _, opt := range opts {
opt.apply(&c)

Check warning on line 73 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L72-L73

Added lines #L72 - L73 were not covered by tests
}
return c

Check warning on line 75 in instrumentation/runtime/options.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/options.go#L75

Added line #L75 was not covered by tests
}
243 changes: 178 additions & 65 deletions instrumentation/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
package runtime // import "go.opentelemetry.io/contrib/instrumentation/runtime"

import (
"context"
"math"
"runtime/metrics"
"sync"
"time"

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

"go.opentelemetry.io/contrib/instrumentation/runtime/internal/deprecatedruntime"
Expand All @@ -16,70 +21,18 @@ import (
// ScopeName is the instrumentation scope name.
const ScopeName = "go.opentelemetry.io/contrib/instrumentation/runtime"

// config contains optional settings for reporting runtime metrics.
type config struct {
// MinimumReadMemStatsInterval sets the minimum interval
// between calls to runtime.ReadMemStats(). Negative values
// are ignored.
MinimumReadMemStatsInterval time.Duration

// MeterProvider sets the metric.MeterProvider. If nil, the global
// Provider will be used.
MeterProvider metric.MeterProvider
}

// Option supports configuring optional settings for runtime metrics.
type Option interface {
apply(*config)
}

// DefaultMinimumReadMemStatsInterval is the default minimum interval
// between calls to runtime.ReadMemStats(). Use the
// WithMinimumReadMemStatsInterval() option to modify this setting in
// Start().
const DefaultMinimumReadMemStatsInterval time.Duration = 15 * time.Second

// WithMinimumReadMemStatsInterval sets a minimum interval between calls to
// runtime.ReadMemStats(), which is a relatively expensive call to make
// frequently. This setting is ignored when `d` is negative.
func WithMinimumReadMemStatsInterval(d time.Duration) Option {
return minimumReadMemStatsIntervalOption(d)
}

type minimumReadMemStatsIntervalOption time.Duration

func (o minimumReadMemStatsIntervalOption) apply(c *config) {
if o >= 0 {
c.MinimumReadMemStatsInterval = time.Duration(o)
}
}

// 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.
func WithMeterProvider(provider metric.MeterProvider) Option {
return metricProviderOption{provider}
}

type metricProviderOption struct{ metric.MeterProvider }

func (o metricProviderOption) apply(c *config) {
if o.MeterProvider != nil {
c.MeterProvider = o.MeterProvider
}
}

// newConfig computes a config from the supplied Options.
func newConfig(opts ...Option) config {
c := config{
MeterProvider: otel.GetMeterProvider(),
MinimumReadMemStatsInterval: DefaultMinimumReadMemStatsInterval,
}
for _, opt := range opts {
opt.apply(&c)
}
return c
}
const (
goTotalMemory = "/memory/classes/total:bytes"
goMemoryReleased = "/memory/classes/heap/released:bytes"
goHeapMemory = "/memory/classes/heap/stacks:bytes"
goMemoryLimit = "/gc/gomemlimit:bytes"
goMemoryAllocated = "/gc/heap/allocs:bytes"
goMemoryAllocations = "/gc/heap/allocs:objects"
goMemoryGoal = "/gc/heap/goal:bytes"
goGoroutines = "/sched/goroutines:goroutines"
goMaxProcs = "/sched/gomaxprocs:threads"
goConfigGC = "/gc/gogc:percent"
)

// Start initializes reporting of runtime metrics using the supplied config.
func Start(opts ...Option) error {
Expand All @@ -97,6 +50,166 @@ func Start(opts ...Option) error {
if x.DeprecatedRuntimeMetrics.Enabled() {
return deprecatedruntime.Start(meter, c.MinimumReadMemStatsInterval)
}
// TODO (#5655) Implement new runtime conventions
memoryUsedInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.used",
metric.WithUnit("By"),
metric.WithDescription("Memory used by the Go runtime."),
)
if err != nil {
return err

Check warning on line 59 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L53-L59

Added lines #L53 - L59 were not covered by tests
}
memoryLimitInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.limit",
metric.WithUnit("By"),
metric.WithDescription("Go runtime memory limit configured by the user, if a limit exists."),
)
if err != nil {
return err

Check warning on line 67 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L61-L67

Added lines #L61 - L67 were not covered by tests
}
memoryAllocatedInstrument, err := meter.Int64ObservableCounter(
"go.memory.allocated",
metric.WithUnit("By"),
metric.WithDescription("Memory allocated to the heap by the application."),
)
if err != nil {
return err

Check warning on line 75 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L69-L75

Added lines #L69 - L75 were not covered by tests
}
memoryAllocationsInstrument, err := meter.Int64ObservableCounter(
"go.memory.allocations",
metric.WithUnit("{allocation}"),
metric.WithDescription("Count of allocations to the heap by the application."),
)
if err != nil {
return err

Check warning on line 83 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L77-L83

Added lines #L77 - L83 were not covered by tests
}
memoryGCGoalInstrument, err := meter.Int64ObservableUpDownCounter(
"go.memory.gc.goal",
metric.WithUnit("By"),
metric.WithDescription("Heap size target for the end of the GC cycle."),
)
if err != nil {
return err

Check warning on line 91 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L85-L91

Added lines #L85 - L91 were not covered by tests
}
goroutineCountInstrument, err := meter.Int64ObservableUpDownCounter(
"go.goroutine.count",
metric.WithUnit("{goroutine}"),
metric.WithDescription("Count of live goroutines."),
)
if err != nil {
return err

Check warning on line 99 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L93-L99

Added lines #L93 - L99 were not covered by tests
}
processorLimitInstrument, err := meter.Int64ObservableUpDownCounter(
"go.processor.limit",
metric.WithUnit("{thread}"),
metric.WithDescription("The number of OS threads that can execute user-level Go code simultaneously."),
)
if err != nil {
return err

Check warning on line 107 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L101-L107

Added lines #L101 - L107 were not covered by tests
}
gogcConfigInstrument, err := meter.Int64ObservableUpDownCounter(
"go.config.gogc",
metric.WithUnit("%"),
metric.WithDescription("Heap size target percentage configured by the user, otherwise 100."),
)
if err != nil {
return err

Check warning on line 115 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L109-L115

Added lines #L109 - L115 were not covered by tests
}

otherMemoryOpt := metric.WithAttributeSet(
attribute.NewSet(attribute.String("go.memory.type", "other")),
)
stackMemoryOpt := metric.WithAttributeSet(
attribute.NewSet(attribute.String("go.memory.type", "stack")),
)
collector := newCollector(c.MinimumReadMemStatsInterval)
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)
o.ObserveInt64(memoryUsedInstrument, stackMemory, stackMemoryOpt)
totalMemory := collector.get(goTotalMemory) - collector.get(goMemoryReleased)
otherMemory := totalMemory - stackMemory
o.ObserveInt64(memoryUsedInstrument, otherMemory, otherMemoryOpt)

Check warning on line 135 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L118-L135

Added lines #L118 - L135 were not covered by tests
// Only observe the limit metric if a limit exists
if limit := collector.get(goMemoryLimit); limit != math.MaxInt64 {
o.ObserveInt64(memoryLimitInstrument, limit)

Check warning on line 138 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L137-L138

Added lines #L137 - L138 were not covered by tests
}
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))
return nil

Check warning on line 146 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L140-L146

Added lines #L140 - L146 were not covered by tests
},
memoryUsedInstrument,
memoryLimitInstrument,
memoryAllocatedInstrument,
memoryAllocationsInstrument,
memoryGCGoalInstrument,
goroutineCountInstrument,
processorLimitInstrument,
gogcConfigInstrument,
)
if err != nil {
return err

Check warning on line 158 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L157-L158

Added lines #L157 - L158 were not covered by tests
}
// TODO (#5655) support go.schedule.duration
return nil
}

// These are the metrics we actually fetch from the go runtime.
var runtimeMetrics = []string{
goTotalMemory,
goMemoryReleased,
goHeapMemory,
goMemoryLimit,
goMemoryAllocated,
goMemoryAllocations,
goMemoryGoal,
goGoroutines,
goMaxProcs,
goConfigGC,
}

type goCollector struct {
lastCollect time.Time
minimumInterval time.Duration
// sampleBuffer is populated by runtime/metrics
sampleBuffer []metrics.Sample
// sampleMap allows us to easily get the value of a single metric
sampleMap map[string]*metrics.Sample
}

func newCollector(minimumInterval time.Duration) *goCollector {
g := &goCollector{
sampleBuffer: make([]metrics.Sample, 0, len(runtimeMetrics)),
sampleMap: make(map[string]*metrics.Sample, len(runtimeMetrics)),

Check warning on line 190 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L187-L190

Added lines #L187 - L190 were not covered by tests
}
for _, runtimeMetric := range runtimeMetrics {
g.sampleBuffer = append(g.sampleBuffer, metrics.Sample{Name: runtimeMetric})
g.sampleMap[runtimeMetric] = &g.sampleBuffer[len(g.sampleBuffer)-1]

Check warning on line 194 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L192-L194

Added lines #L192 - L194 were not covered by tests
}
return g

Check warning on line 196 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L196

Added line #L196 was not covered by tests
}

func (g *goCollector) refresh() {
now := time.Now()
if now.Sub(g.lastCollect) < g.minimumInterval {

Check warning on line 201 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L199-L201

Added lines #L199 - L201 were not covered by tests
// refresh was invoked more frequently than allowed by the minimum
// interval. Do nothing.
return

Check warning on line 204 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L204

Added line #L204 was not covered by tests
}
metrics.Read(g.sampleBuffer)
g.lastCollect = now

Check warning on line 207 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L206-L207

Added lines #L206 - L207 were not covered by tests
}

func (g *goCollector) get(name string) int64 {
if s, ok := g.sampleMap[name]; ok {
return int64(s.Value.Uint64())

Check warning on line 212 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L210-L212

Added lines #L210 - L212 were not covered by tests
}
return 0

Check warning on line 214 in instrumentation/runtime/runtime.go

View check run for this annotation

Codecov / codecov/patch

instrumentation/runtime/runtime.go#L214

Added line #L214 was not covered by tests
}

0 comments on commit 2883e51

Please sign in to comment.