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

WIP Add runtime/metrics source option to instrumentation/runtime metrics #2643

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 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
135 changes: 135 additions & 0 deletions instrumentation/runtime/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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 "go.opentelemetry.io/contrib/instrumentation/runtime"

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

"github.com/hashicorp/go-multierror"

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

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

type builtinRuntime struct {
meter metric.Meter
samples []metrics.Sample
instruments []instrument.Asynchronous
}

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,
}
}

func (r *builtinRuntime) register() error {
all := metrics.All()
counts := map[string]int{}

for _, m := range all {
counts[m.Name]++
}

var rerr error
for _, m := range all {
n, u, _ := strings.Cut(m.Name, ":")

n = "process.runtime.go" + strings.ReplaceAll(n, "/", ".")
u = "{" + u + "}"

if counts[n] > 1 {
// When the names conflict, leave the unit in the name.
// Let it be unitless, so that OTLP->PRW->OTLP will roundtrip.
n = n + "." + u[1:len(u)-1]
u = ""
}

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 {
rerr = multierror.Append(rerr, err)
}

samp := metrics.Sample{
Name: m.Name,
}
r.samples = append(r.samples, samp)
r.instruments = append(r.instruments, inst)
}

if err := r.meter.RegisterCallback(r.instruments, func(ctx context.Context) {
metrics.Read(r.samples)
for idx, samp := range r.samples {
switch samp.Value.Kind() {
case metrics.KindUint64:
r.instruments[idx].(int64Observer).Observe(ctx, int64(samp.Value.Uint64()))
case metrics.KindFloat64:
r.instruments[idx].(float64Observer).Observe(ctx, samp.Value.Float64())
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 {
rerr = multierror.Append(rerr, err)
}
return rerr
}
167 changes: 167 additions & 0 deletions instrumentation/runtime/builtin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// 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 (
"context"
"runtime/metrics"
"testing"

"github.com/stretchr/testify/require"

"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric/metrictest"
)

var expectLib = metrictest.Library{
InstrumentationName: LibraryName,
InstrumentationVersion: SemVersion(),
SchemaURL: "",
}

// TestBuiltinRuntimeMetrics tests the real output of the library to
// ensure expected prefix, instrumentation scope, and empty
// attributes.
func TestBuiltinRuntimeMetrics(t *testing.T) {
provider, exp := metrictest.NewTestMeterProvider()

err := Start(
WithUseGoRuntimeMetrics(true),
WithMeterProvider(provider),
)

require.NoError(t, err)

require.NoError(t, exp.Collect(context.Background()))

const prefix = "process.runtime.go."

// Note: metrictest library lacks a way to distinguish
// monotonic vs not or to test the unit. This will be fixed in
// the new SDK, all the pieces untested here.
// TODO: add testing in the new SDK's metrictest.
for _, rec := range exp.Records {
require.Regexp(t, `^process\.runtime\.go\..+`, rec.InstrumentName)
require.Equal(t, expectLib, rec.InstrumentationLibrary)
require.Equal(t, []attribute.KeyValue(nil), rec.Attributes)
}

}

func makeTestCase() (allFunc, readFunc, map[string]metrics.Value) {
// Note: the library provides no way to generate values, so use the
// builtin library to get some. Since we can't generate a Float64 value
// we can't even test the Gauge logic in this package.
ints := map[metrics.Value]bool{}

real := metrics.All()
realSamples := make([]metrics.Sample, len(real))
for i := range real {
realSamples[i].Name = real[i].Name
}
metrics.Read(realSamples)
for i, rs := range realSamples {
switch real[i].Kind {
case metrics.KindUint64:
ints[rs.Value] = true
default:
// Histograms and Floats are not tested.
// The 1.19 runtime generates no Floats and
// exports no test constructors.
}
}

var allInts []metrics.Value

for iv := range ints {
allInts = append(allInts, iv)
}

af := func() []metrics.Description {
return []metrics.Description{
{
Name: "/cntr/things:things",
Description: "a counter of things",
Kind: metrics.KindUint64,
Cumulative: true,
},
{
Name: "/updowncntr/things:things",
Description: "an updowncounter of things",
Kind: metrics.KindUint64,
Cumulative: false,
},
{
Name: "/process/count:things",
Description: "a process counter of things",
Kind: metrics.KindUint64,
Cumulative: true,
},
{
Name: "/process/count:parts",
Description: "a process counter of parts",
Kind: metrics.KindUint64,
Cumulative: true,
},
}
}
mapping := map[string]metrics.Value{
"/cntr/things:things": allInts[0],
"/updowncntr/things:things": allInts[1],
"/process/cntr:things": allInts[2],
"/process/cntr:parts": allInts[3],
}
rf := func(samples []metrics.Sample) {
for i := range samples {
v, ok := mapping[samples[i].Name]
if ok {
samples[i].Value = v
}
}
}
return af, rf, map[string]metrics.Value{
"cntr.things": allInts[0],
"updowncntr.things": allInts[1],
"process.cntr.things": allInts[2],
"process.cntr.parts": allInts[3],
}
}

// TestMetricTranslation validates the translation logic using
// synthetic metric names and values.
func TestMetricTranslation(t *testing.T) {
provider, exp := metrictest.NewTestMeterProvider()

af, rf, mapping := makeTestCase()
br := newBuiltinRuntime(provider.Meter("test"), af, rf)
br.register()

const prefix = "process.runtime.go."

for _, rec := range exp.Records {
require.Regexp(t, `^process\.runtime\.go\..+`, rec.InstrumentName)
require.Equal(t, expectLib, rec.InstrumentationLibrary)
require.Equal(t, []attribute.KeyValue(nil), rec.Attributes)

name := rec.InstrumentName[len("process.runtime.go."):]

// Note: only int64 is tested, we have no way to
// generate Float64 values and Float64Hist values are
// not implemented for testing.

require.Equal(t, mapping[name].Uint64, uint64(rec.Sum.AsInt64()))
}

}
89 changes: 74 additions & 15 deletions instrumentation/runtime/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,80 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// package runtime implements the conventional runtime metrics specified by OpenTelemetry.
// package runtime implements two forms of runtime metrics for Golang OpenTelemetry users.
//
// The original implementation in this package uses ReadMemStats()
// directly to report metric names.
//
// The metric events produced are:
// runtime.go.cgo.calls - Number of cgo calls made by the current process
// runtime.go.gc.count - Number of completed garbage collection cycles
// runtime.go.gc.pause_ns (ns) Amount of nanoseconds in GC stop-the-world pauses
// runtime.go.gc.pause_total_ns (ns) Cumulative nanoseconds in GC stop-the-world pauses since the program started
// runtime.go.goroutines - Number of goroutines that currently exist
// runtime.go.lookups - Number of pointer lookups performed by the runtime
// runtime.go.mem.heap_alloc (bytes) Bytes of allocated heap objects
// runtime.go.mem.heap_idle (bytes) Bytes in idle (unused) spans
// runtime.go.mem.heap_inuse (bytes) Bytes in in-use spans
// runtime.go.mem.heap_objects - Number of allocated heap objects
// runtime.go.mem.heap_released (bytes) Bytes of idle spans whose physical memory has been returned to the OS
// runtime.go.mem.heap_sys (bytes) Bytes of heap memory obtained from the OS
// runtime.go.mem.live_objects - Number of live objects is the number of cumulative Mallocs - Frees
// runtime.uptime (ms) Milliseconds since application was initialized
// process.runtime.go.cgo.calls - Number of cgo calls made by the current process
// process.runtime.go.gc.count - Number of completed garbage collection cycles
// process.runtime.go.gc.pause_ns (ns) Amount of nanoseconds in GC stop-the-world pauses
jmacd marked this conversation as resolved.
Show resolved Hide resolved
// process.runtime.go.gc.pause_total_ns (ns) Cumulative nanoseconds in GC stop-the-world pauses since the program started
// process.runtime.go.goroutines - Number of goroutines that currently exist
// process.runtime.go.lookups - Number of pointer lookups performed by the runtime
// process.runtime.go.mem.heap_alloc (bytes) Bytes of allocated heap objects
// process.runtime.go.mem.heap_idle (bytes) Bytes in idle (unused) spans
// process.runtime.go.mem.heap_inuse (bytes) Bytes in in-use spans
// process.runtime.go.mem.heap_objects - Number of allocated heap objects
// process.runtime.go.mem.heap_released (bytes) Bytes of idle spans whose physical memory has been returned to the OS
// process.runtime.go.mem.heap_sys (bytes) Bytes of heap memory obtained from the OS
// process.runtime.go.mem.live_objects - Number of live objects is the number of cumulative Mallocs - Frees
// runtime.uptime (ms) Milliseconds since application was initialized
//
// The Go-1.16 release featured a new runtime/metrics package that gives formal
// metric names to the various quantities. This package supports the new metrics
// under their Go-specified names by setting `WithUseGoRuntimeMetrics(true)`.
// These metrics are documented at https://pkg.go.dev/runtime/metrics#hdr-Supported_metrics.
//
// The `runtime/metrics` implementation will replace the older
// implementation as the default no sooner than January 2023. The
// older implementation will be removed no sooner than January 2024.
//
// The following metrics are generated in go-1.19.
//
// Name Unit Instrument
// -----------------------------------------------------------------------------
// process.runtime.go.cgo.go-to-c-calls {calls} Counter[int64]
// process.runtime.go.gc.cycles.automatic {gc-cycles} Counter[int64]
// process.runtime.go.gc.cycles.forced {gc-cycles} Counter[int64]
// process.runtime.go.gc.cycles.total {gc-cycles} Counter[int64]
jmacd marked this conversation as resolved.
Show resolved Hide resolved
// process.runtime.go.gc.heap.allocs.bytes (*) Counter[int64]
// process.runtime.go.gc.heap.allocs.objects (*) Counter[int64]
jmacd marked this conversation as resolved.
Show resolved Hide resolved
// process.runtime.go.gc.heap.allocs-by-size {bytes} Histogram[float64] (**)
// process.runtime.go.gc.heap.frees.bytes (*) Counter[int64]
// process.runtime.go.gc.heap.frees.objects (*) Counter[int64]
// process.runtime.go.gc.heap.frees-by-size {bytes} Histogram[float64] (**)
// process.runtime.go.gc.heap.goal {bytes} UpDownCounter[int64]
// process.runtime.go.gc.heap.objects {objects} UpDownCounter[int64]
// process.runtime.go.gc.heap.tiny.allocs {objects} Counter[int64]
// process.runtime.go.gc.limiter.last-enabled {gc-cycle} UpDownCounter[int64]
// process.runtime.go.gc.pauses {seconds} Histogram[float64] (**)
// process.runtime.go.gc.stack.starting-size {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.heap.free {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.heap.objects {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.heap.released {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.heap.stacks {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.heap.unused {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.metadata.mcache.free {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.metadata.mcache.inuse {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.metadata.mspan.free {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.metadata.mspan.inuse {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.metadata.other {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.os-stacks {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.other {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.profiling.buckets {bytes} UpDownCounter[int64]
// process.runtime.go.memory.classes.total {bytes} UpDownCounter[int64]
// process.runtime.go.sched.gomaxprocs {threads} UpDownCounter[int64]
// process.runtime.go.sched.goroutines {goroutines} UpDownCounter[int64]
// process.runtime.go.sched.latencies {seconds} GaugeHistogram[float64] (**)
//
// (*) Empty unit strings are cases where runtime/metric produces
// duplicate names ignoring the unit string; here we leave the unit in the name
// and set the unit to empty.
// (**) Histograms are not currently implemented, see the related
// issues for an explanation:
// https://github.com/open-telemetry/opentelemetry-specification/issues/2713
// https://github.com/open-telemetry/opentelemetry-specification/issues/2714

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