Skip to content

Commit

Permalink
event/otel: metrics
Browse files Browse the repository at this point in the history
Add an event handler for OpenTelemetry metrics.

The first time it sees an event.Metric, the handler creates a matching
otel instrument and caches it. On each call, it uses the instrument to
record the metric value.

Change-Id: I07d6f40601c7d2a801ed9fbe3cf7c24d5698f3f1
Reviewed-on: https://go-review.googlesource.com/c/exp/+/320350
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
  • Loading branch information
jba committed Feb 2, 2022
1 parent 8176bd3 commit a36d682
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 123 deletions.
2 changes: 1 addition & 1 deletion event/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

const (
MetricKey = "metric"
MetricKey = interfaceKey("metric")
MetricVal = "metricValue"
DurationMetric = interfaceKey("durationMetric")
)
Expand Down
13 changes: 7 additions & 6 deletions event/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !disable_events
// +build !disable_events

package event_test
Expand All @@ -24,9 +25,9 @@ var (
l1 = event.Int64("l1", 1)
l2 = event.Int64("l2", 2)
l3 = event.Int64("l3", 3)
counter = event.NewCounter("hits", "cache hits")
gauge = event.NewFloatGauge("temperature", "CPU board temperature in Celsius")
latency = event.NewDuration("latency", "how long it took")
counter = event.NewCounter("hits", nil)
gauge = event.NewFloatGauge("temperature", nil)
latency = event.NewDuration("latency", nil)
err = errors.New("an error")
)

Expand Down Expand Up @@ -275,7 +276,7 @@ func (t *testTraceHandler) Event(ctx context.Context, ev *event.Event) context.C

func TestTraceDuration(t *testing.T) {
// Verify that a trace can can emit a latency metric.
dur := event.NewDuration("test", "")
dur := event.NewDuration("test", nil)
want := time.Second

check := func(t *testing.T, h *testTraceDurationHandler) {
Expand Down Expand Up @@ -313,7 +314,7 @@ type testTraceDurationHandler struct {

func (t *testTraceDurationHandler) Event(ctx context.Context, ev *event.Event) context.Context {
for _, l := range ev.Labels {
if l.Name == event.MetricVal {
if l.Name == string(event.MetricVal) {
t.got = l
}
}
Expand All @@ -322,7 +323,7 @@ func (t *testTraceDurationHandler) Event(ctx context.Context, ev *event.Event) c

func BenchmarkBuildContext(b *testing.B) {
// How long does it take to deliver an event from a nested context?
c := event.NewCounter("c", "")
c := event.NewCounter("c", nil)
for _, depth := range []int{1, 5, 7, 10} {
b.Run(fmt.Sprintf("depth %d", depth), func(b *testing.B) {
ctx := event.WithExporter(context.Background(), event.NewExporter(nopHandler{}, eventtest.ExporterOptions()))
Expand Down
148 changes: 66 additions & 82 deletions event/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,81 +6,71 @@ package event

import (
"context"
"fmt"
"time"
)

// A Unit is a unit of measurement for a metric.
type Unit string

const (
UnitDimensionless Unit = "1"
UnitBytes Unit = "By"
UnitMilliseconds Unit = "ms"
)

// A Metric represents a kind of recorded measurement.
type Metric interface {
Descriptor() *MetricDescriptor
Name() string
Options() MetricOptions
}

// A MetricDescriptor describes a metric.
type MetricDescriptor struct {
namespace string
name string
description string
// TODO: deal with units. Follow otel, or define Go types for common units.
// We don't need a time unit because we'll use time.Duration, and the only
// other unit otel currently defines (besides dimensionless) is bytes.
type MetricOptions struct {
// A string that should be common for all metrics of an application or
// service. Defaults to the import path of the package calling
// the metric construction function (NewCounter, etc.).
Namespace string

// Optional description of the metric.
Description string

// Optional unit for the metric. Defaults to UnitDimensionless.
Unit Unit
}

// NewMetricDescriptor creates a MetricDescriptor with the given name.
// The namespace defaults to the import path of the caller of NewMetricDescriptor.
// Use SetNamespace to provide a different one.
// Neither the name nor the namespace can be empty.
func NewMetricDescriptor(name, description string) *MetricDescriptor {
return newMetricDescriptor(name, description)
// A Counter is a metric that counts something cumulatively.
type Counter struct {
name string
opts MetricOptions
}

func newMetricDescriptor(name, description string) *MetricDescriptor {
if name == "" {
panic("name cannot be empty")
func initOpts(popts *MetricOptions) MetricOptions {
var opts MetricOptions
if popts != nil {
opts = *popts
}
return &MetricDescriptor{
name: name,
namespace: scanStack().Space,
description: description,
if opts.Namespace == "" {
opts.Namespace = scanStack().Space
}
}

// SetNamespace sets the namespace of m to a non-empty string.
func (m *MetricDescriptor) SetNamespace(ns string) {
if ns == "" {
panic("namespace cannot be empty")
if opts.Unit == "" {
opts.Unit = UnitDimensionless
}
m.namespace = ns
}

func (m *MetricDescriptor) String() string {
return fmt.Sprintf("Metric(\"%s/%s\")", m.namespace, m.name)
}

func (m *MetricDescriptor) Name() string { return m.name }
func (m *MetricDescriptor) Namespace() string { return m.namespace }
func (m *MetricDescriptor) Description() string { return m.description }

// A Counter is a metric that counts something cumulatively.
type Counter struct {
*MetricDescriptor
return opts
}

// NewCounter creates a counter with the given name.
func NewCounter(name, description string) *Counter {
return &Counter{newMetricDescriptor(name, description)}
func NewCounter(name string, opts *MetricOptions) *Counter {
return &Counter{name, initOpts(opts)}
}

// Descriptor returns the receiver's MetricDescriptor.
func (c *Counter) Descriptor() *MetricDescriptor {
return c.MetricDescriptor
}
func (c *Counter) Name() string { return c.name }
func (c *Counter) Options() MetricOptions { return c.opts }

// Record delivers a metric event with the given metric, value and labels to the
// exporter in the context.
func (c *Counter) Record(ctx context.Context, v int64, labels ...Label) {
ev := New(ctx, MetricKind)
if ev != nil {
record(ev, c, Int64(MetricVal, v))
record(ev, c, Int64(string(MetricVal), v))
ev.Labels = append(ev.Labels, labels...)
ev.Deliver()
}
Expand All @@ -89,26 +79,24 @@ func (c *Counter) Record(ctx context.Context, v int64, labels ...Label) {
// A FloatGauge records a single floating-point value that may go up or down.
// TODO(generics): Gauge[T]
type FloatGauge struct {
*MetricDescriptor
name string
opts MetricOptions
}

// NewFloatGauge creates a new FloatGauge with the given name.
func NewFloatGauge(name, description string) *FloatGauge {
return &FloatGauge{newMetricDescriptor(name, description)}
func NewFloatGauge(name string, opts *MetricOptions) *FloatGauge {
return &FloatGauge{name, initOpts(opts)}
}

// Descriptor returns the receiver's MetricDescriptor.
func (g *FloatGauge) Descriptor() *MetricDescriptor {
return g.MetricDescriptor
}
func (g *FloatGauge) Name() string { return g.name }
func (g *FloatGauge) Options() MetricOptions { return g.opts }

// Record converts its argument into a Value and returns a MetricValue with the
// receiver and the value. It is intended to be used as an argument to
// Builder.Metric.
// receiver and the value.
func (g *FloatGauge) Record(ctx context.Context, v float64, labels ...Label) {
ev := New(ctx, MetricKind)
if ev != nil {
record(ev, g, Float64(MetricVal, v))
record(ev, g, Float64(string(MetricVal), v))
ev.Labels = append(ev.Labels, labels...)
ev.Deliver()
}
Expand All @@ -117,58 +105,54 @@ func (g *FloatGauge) Record(ctx context.Context, v float64, labels ...Label) {
// A DurationDistribution records a distribution of durations.
// TODO(generics): Distribution[T]
type DurationDistribution struct {
*MetricDescriptor
name string
opts MetricOptions
}

// NewDuration creates a new Duration with the given name.
func NewDuration(name, description string) *DurationDistribution {
return &DurationDistribution{newMetricDescriptor(name, description)}
func NewDuration(name string, opts *MetricOptions) *DurationDistribution {
return &DurationDistribution{name, initOpts(opts)}
}

// Descriptor returns the receiver's MetricDescriptor.
func (d *DurationDistribution) Descriptor() *MetricDescriptor {
return d.MetricDescriptor
}
func (d *DurationDistribution) Name() string { return d.name }
func (d *DurationDistribution) Options() MetricOptions { return d.opts }

// Record converts its argument into a Value and returns a MetricValue with the
// receiver and the value. It is intended to be used as an argument to
// Builder.Metric.
// receiver and the value.
func (d *DurationDistribution) Record(ctx context.Context, v time.Duration, labels ...Label) {
ev := New(ctx, MetricKind)
if ev != nil {
record(ev, d, Duration(MetricVal, v))
record(ev, d, Duration(string(MetricVal), v))
ev.Labels = append(ev.Labels, labels...)
ev.Deliver()
}
}

// An IntDistribution records a distribution of int64s.
type IntDistribution struct {
*MetricDescriptor
name string
opts MetricOptions
}

// NewIntDistribution creates a new IntDistribution with the given name.
func NewIntDistribution(name, description string) *IntDistribution {
return &IntDistribution{newMetricDescriptor(name, description)}
}
func (d *IntDistribution) Name() string { return d.name }
func (d *IntDistribution) Options() MetricOptions { return d.opts }

// Descriptor returns the receiver's MetricDescriptor.
func (d *IntDistribution) Descriptor() *MetricDescriptor {
return d.MetricDescriptor
// NewIntDistribution creates a new IntDistribution with the given name.
func NewIntDistribution(name string, opts *MetricOptions) *IntDistribution {
return &IntDistribution{name, initOpts(opts)}
}

// Record converts its argument into a Value and returns a MetricValue with the
// receiver and the value. It is intended to be used as an argument to
// Builder.Metric.
// receiver and the value.
func (d *IntDistribution) Record(ctx context.Context, v int64, labels ...Label) {
ev := New(ctx, MetricKind)
if ev != nil {
record(ev, d, Int64(MetricVal, v))
record(ev, d, Int64(string(MetricVal), v))
ev.Labels = append(ev.Labels, labels...)
ev.Deliver()
}
}

func record(ev *Event, m Metric, l Label) {
ev.Labels = append(ev.Labels, l, Value(MetricKey, m))
ev.Labels = append(ev.Labels, l, MetricKey.Of(m))
}
Loading

0 comments on commit a36d682

Please sign in to comment.