Skip to content

Commit

Permalink
New runtime/metrics instrumentation (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmacd authored Sep 2, 2022
1 parent 9f0728e commit 4b4be81
Show file tree
Hide file tree
Showing 7 changed files with 622 additions and 1 deletion.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## Unreleased

### Added

- Add `WithMetricsBuiltinsEnabled()` option and environment variable
`LS_METRICS_BUILTINS_ENABLED`, which defaults to true. When metrics
builtins are enabled,
[runtime](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/runtime)
and
[host](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/instrumentation/host)
metrics instrumentation will be reported automatically.
[#265](https://github.com/lightstep/otel-launcher-go/pull/265)
- Proposed replacement for go-contrib instrumentation/runtime added as lightstep/instrumentation/runtime.
[#267](https://github.com/lightstep/otel-launcher-go/pull/267)

## [1.10.1](https://github.com/lightstep/otel-launcher-go/releases/tag/v1.10.1) - 2022-08-29

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel/metric v0.31.0
go.opentelemetry.io/otel/sdk v1.9.0
go.opentelemetry.io/otel/sdk/metric v0.31.1-0.20220826135333-55b49c407e07
go.opentelemetry.io/otel/trace v1.9.0
)

Expand Down Expand Up @@ -39,7 +40,6 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.31.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.9.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.9.0 // indirect
go.opentelemetry.io/otel/sdk/metric v0.31.1-0.20220826135333-55b49c407e07 // indirect
go.opentelemetry.io/proto/otlp v0.18.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
Expand Down
273 changes: 273 additions & 0 deletions lightstep/instrumentation/runtime/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package runtime // import "github.com/lightstep/otel-launcher-go/lightstep/instrumentation/runtime"

import (
"context"
"fmt"
"runtime/metrics"
"strings"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/unit"
)

// LibraryName is the value of instrumentation.Library.Name.
const LibraryName = "otel-launcher-go/runtime"

// config contains optional settings for reporting runtime metrics.
type config struct {
// 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)
}

// 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: global.MeterProvider(),
}
for _, opt := range opts {
opt.apply(&c)
}
return c
}

// Start initializes reporting of runtime metrics using the supplied config.
func Start(opts ...Option) error {
c := newConfig(opts...)
if c.MeterProvider == nil {
c.MeterProvider = global.MeterProvider()
}
meter := c.MeterProvider.Meter(
LibraryName,
)

r := newBuiltinRuntime(meter, metrics.All, metrics.Read)
return r.register()
}

type allFunc = func() []metrics.Description
type readFunc = func([]metrics.Sample)

type builtinRuntime struct {
meter metric.Meter
allFunc allFunc
readFunc readFunc
}

type int64Observer interface {
Observe(ctx context.Context, x int64, attrs ...attribute.KeyValue)
}

type float64Observer interface {
Observe(ctx context.Context, x float64, attrs ...attribute.KeyValue)
}

func newBuiltinRuntime(meter metric.Meter, af allFunc, rf readFunc) *builtinRuntime {
return &builtinRuntime{
meter: meter,
allFunc: af,
readFunc: rf,
}
}

func getAttributeName(n string) string {
x := strings.Split(n, ".")
// It's a plural, make it singular.
switch x[len(x)-1] {
case "cycles":
return "cycle"
case "classes":
return "class"
}
panic("unrecognized attribute name")
}

func (r *builtinRuntime) register() error {
all := r.allFunc()
totals := map[string]bool{}
counts := map[string]int{}
toName := func(in string) (string, string) {
n, statedUnits, _ := strings.Cut(in, ":")
n = "process.runtime.go" + strings.ReplaceAll(n, "/", ".")
return n, statedUnits
}

for _, m := range all {
name, _ := toName(m.Name)

// Totals map includes the '.' suffix.
if strings.HasSuffix(name, ".total") {
totals[name[:len(name)-len("total")]] = true
}

counts[name]++
}

var samples []metrics.Sample
var instruments []instrument.Asynchronous
var totalAttrs [][]attribute.KeyValue

for _, m := range all {
n, statedUnits := toName(m.Name)

if strings.HasSuffix(n, ".total") {
continue
}

var u string
switch statedUnits {
case "bytes", "seconds":
// Real units
u = statedUnits
default:
// Pseudo-units
u = "{" + statedUnits + "}"
}

// Remove any ".total" suffix, this is redundant for Prometheus.
var totalAttrVal string
for totalize := range totals {
if strings.HasPrefix(n, totalize) {
// Units is unchanged.
// Name becomes the overall prefix.
// Remember which attribute to use.
totalAttrVal = n[len(totalize):]
n = totalize[:len(totalize)-1]
break
}
}

if counts[n] > 1 {
if totalAttrVal != "" {
// This has not happened, hopefully never will.
// Indicates the special case for objects/bytes
// overlaps with the special case for total.
panic("special case collision")
}

// This is treated as a special case, we know this happens
// with "objects" and "bytes" in the standard Go 1.19 runtime.
switch statedUnits {
case "objects":
// In this case, use `.objects` suffix.
n = n + ".objects"
u = "{objects}"
case "bytes":
// In this case, use no suffix. In Prometheus this will
// be appended as a suffix.
default:
panic(fmt.Sprint(
"unrecognized duplicate metrics names, ",
"attention required: ",
n,
))
}
}

opts := []instrument.Option{
instrument.WithUnit(unit.Unit(u)),
instrument.WithDescription(m.Description),
}
var inst instrument.Asynchronous
var err error
if m.Cumulative {
switch m.Kind {
case metrics.KindUint64:
inst, err = r.meter.AsyncInt64().Counter(n, opts...)
case metrics.KindFloat64:
inst, err = r.meter.AsyncFloat64().Counter(n, opts...)
case metrics.KindFloat64Histogram:
// Not implemented Histogram[float64].
continue
}
} else {
switch m.Kind {
case metrics.KindUint64:
inst, err = r.meter.AsyncInt64().UpDownCounter(n, opts...)
case metrics.KindFloat64:
// Note: this has never been used.
inst, err = r.meter.AsyncFloat64().Gauge(n, opts...)
case metrics.KindFloat64Histogram:
// Not implemented GaugeHistogram[float64].
continue
}
}
if err != nil {
return err
}

samp := metrics.Sample{
Name: m.Name,
}
samples = append(samples, samp)
instruments = append(instruments, inst)
if totalAttrVal == "" {
totalAttrs = append(totalAttrs, nil)
} else {
// Append a singleton list.
totalAttrs = append(totalAttrs, []attribute.KeyValue{
attribute.String(getAttributeName(n), totalAttrVal),
})
}
}

if err := r.meter.RegisterCallback(instruments, func(ctx context.Context) {
r.readFunc(samples)

for idx, samp := range samples {

switch samp.Value.Kind() {
case metrics.KindUint64:
instruments[idx].(int64Observer).Observe(ctx, int64(samp.Value.Uint64()), totalAttrs[idx]...)
case metrics.KindFloat64:
instruments[idx].(float64Observer).Observe(ctx, samp.Value.Float64(), totalAttrs[idx]...)
default:
// KindFloat64Histogram (unsupported in OTel) and KindBad
// (unsupported by runtime/metrics). Neither should happen
// if runtime/metrics and the code above are working correctly.
otel.Handle(fmt.Errorf("invalid runtime/metrics value kind: %v", samp.Value.Kind()))
}
}
}); err != nil {
return err
}
return nil
}
30 changes: 30 additions & 0 deletions lightstep/instrumentation/runtime/builtin_118_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build go1.18 && !go1.19

package runtime

var expectRuntimeMetrics = map[string]int{
"gc.cycles": 2,
"gc.heap.allocs": 1,
"gc.heap.allocs.objects": 1,
"gc.heap.frees": 1,
"gc.heap.frees.objects": 1,
"gc.heap.goal": 1,
"gc.heap.objects": 1,
"gc.heap.tiny.allocs": 1,
"memory.classes": 13,
"sched.goroutines": 1,
}
34 changes: 34 additions & 0 deletions lightstep/instrumentation/runtime/builtin_119_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build go1.19

package runtime

var expectRuntimeMetrics = map[string]int{
"cgo-to-c-calls": 1,
"gc.cycles": 2,
"gc.heap.allocs": 1,
"gc.heap.allocs.objects": 1,
"gc.heap.frees": 1,
"gc.heap.frees.objects": 1,
"gc.heap.goal": 1,
"gc.heap.objects": 1,
"gc.heap.tiny.allocs": 1,
"gc.limiter.last-enabled": 1,
"gc.stack.starting-size": 1,
"memory.classes": 13,
"sched.gomaxprocs": 1,
"sched.goroutines": 1,
}
Loading

0 comments on commit 4b4be81

Please sign in to comment.