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

Runtime metrics support for Go-1.20 #301

Merged
merged 10 commits into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from 8 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
186 changes: 57 additions & 129 deletions lightstep/instrumentation/runtime/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"context"
"fmt"
"runtime/metrics"
"strings"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
Expand All @@ -28,6 +27,9 @@ import (
"go.opentelemetry.io/otel/metric/unit"
)

// namePrefix is prefixed onto OTel instrument names.
const namePrefix = "process.runtime.go"

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

Expand Down Expand Up @@ -80,22 +82,28 @@ func Start(opts ...Option) error {
)

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

// allFunc is the function signature of metrics.All()
type allFunc = func() []metrics.Description

// readFunc is the function signature of metrics.Read()
type readFunc = func([]metrics.Sample)

// builtinRuntime instruments all supported kinds of runtime/metrics.
type builtinRuntime struct {
meter metric.Meter
allFunc allFunc
readFunc readFunc
}

// int64Observer is any async int64 instrument.
type int64Observer interface {
Observe(ctx context.Context, x int64, attrs ...attribute.KeyValue)
}

// float64Observer is any async float64 instrument.
type float64Observer interface {
Observe(ctx context.Context, x float64, attrs ...attribute.KeyValue)
}
Expand All @@ -108,165 +116,85 @@ func newBuiltinRuntime(meter metric.Meter, af allFunc, rf readFunc) *builtinRunt
}
}

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

func getTotalizedMetricName(n, u string) string {
if !strings.HasSuffix(n, ".classes") {
return n
}

s := n[:len(n)-len("classes")]

// Note that ".classes" is (apparently) intended as a generic
// suffix, while ".cycles" is an exception.
// The ideal name depends on what we know.
switch u {
case "bytes":
return s + "usage"
case "cpu-seconds":
return s + "time"
default:
panic("unrecognized metric suffix")
}
}

func (r *builtinRuntime) register() error {
// register parses each name and registers metric instruments for all
// the recognized instruments.
func (r *builtinRuntime) register(desc *builtinDescriptor) 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
var samples []metrics.Sample
var instAttrs [][]attribute.KeyValue

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

if strings.HasSuffix(n, ".total") {
// each should match one
mname, munit, pattern, attrs, kind, err := desc.findMatch(m.Name)
if err != nil {
// skip unrecognized metric names
otel.Handle(fmt.Errorf("unrecognized runtime/metrics name: %s", m.Name))
continue
}

var u string
switch statedUnits {
case "bytes", "seconds":
// Real units
u = statedUnits
default:
// Pseudo-units
u = "{" + statedUnits + "}"
if kind == builtinSkip {
// skip e.g., totalized metrics
continue
}

// 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 = getTotalizedMetricName(totalize[:len(totalize)-1], u)
break
if kind == builtinHistogram {
// skip unsupported data types
if m.Kind != metrics.KindFloat64Histogram {
otel.Handle(fmt.Errorf("expected histogram runtime/metrics: %s", mname))
}
continue
}

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,
))
}
}
description := fmt.Sprintf("%s from runtime/metrics", pattern)

opts := []instrument.Option{
instrument.WithUnit(unit.Unit(u)),
instrument.WithDescription(m.Description),
instrument.WithUnit(unit.Unit(munit)),
instrument.WithDescription(description),
}
var inst instrument.Asynchronous
var err error
if m.Cumulative {
switch kind {
case builtinCounter:
switch m.Kind {
case metrics.KindUint64:
inst, err = r.meter.AsyncInt64().Counter(n, opts...)
// e.g., alloc bytes
inst, err = r.meter.AsyncInt64().Counter(mname, opts...)
case metrics.KindFloat64:
inst, err = r.meter.AsyncFloat64().Counter(n, opts...)
case metrics.KindFloat64Histogram:
// Not implemented Histogram[float64].
continue
// e.g., cpu time (1.20)
inst, err = r.meter.AsyncFloat64().Counter(mname, opts...)
}
} else {
case builtinUpDownCounter:
switch m.Kind {
case metrics.KindUint64:
inst, err = r.meter.AsyncInt64().UpDownCounter(n, opts...)
// e.g., memory size
inst, err = r.meter.AsyncInt64().UpDownCounter(mname, 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
// not used through 1.20
inst, err = r.meter.AsyncFloat64().UpDownCounter(mname, opts...)
}
case builtinGauge:
switch m.Kind {
case metrics.KindUint64:
inst, err = r.meter.AsyncInt64().Gauge(mname, opts...)
case metrics.KindFloat64:
// not used through 1.20
inst, err = r.meter.AsyncFloat64().Gauge(mname, opts...)
}
}
if err != nil {
return err
}
if inst == nil {
otel.Handle(fmt.Errorf("unexpected runtime/metrics %v: %s", kind, mname))
continue
}

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(getTotalizedAttributeName(n), totalAttrVal),
})
}
instAttrs = append(instAttrs, attrs)
}

if err := r.meter.RegisterCallback(instruments, func(ctx context.Context) {
Expand All @@ -276,9 +204,9 @@ func (r *builtinRuntime) register() error {

switch samp.Value.Kind() {
case metrics.KindUint64:
instruments[idx].(int64Observer).Observe(ctx, int64(samp.Value.Uint64()), totalAttrs[idx]...)
instruments[idx].(int64Observer).Observe(ctx, int64(samp.Value.Uint64()), instAttrs[idx]...)
case metrics.KindFloat64:
instruments[idx].(float64Observer).Observe(ctx, samp.Value.Float64(), totalAttrs[idx]...)
instruments[idx].(float64Observer).Observe(ctx, samp.Value.Float64(), instAttrs[idx]...)
default:
// KindFloat64Histogram (unsupported in OTel) and KindBad
// (unsupported by runtime/metrics). Neither should happen
Expand Down
30 changes: 0 additions & 30 deletions lightstep/instrumentation/runtime/builtin_118_test.go

This file was deleted.

34 changes: 0 additions & 34 deletions lightstep/instrumentation/runtime/builtin_119_test.go

This file was deleted.

Loading