Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New runtime/metrics instrumentation #267

Merged
merged 7 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## Unreleased

### Added

- 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

- Revert the default change of temporality to "cumulative" from #258.
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