blob: 910cc949b61923e931d08bf525df6bf7edb1ce97 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package otel
import (
"context"
"errors"
"fmt"
"sync"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
otelunit "go.opentelemetry.io/otel/metric/unit"
"golang.org/x/exp/event"
)
// MetricHandler is an event.Handler for OpenTelemetry metrics.
// Its Event method handles Metric events and ignores all others.
type MetricHandler struct {
meter metric.MeterMust
mu sync.Mutex
// A map from event.Metrics to, effectively, otel Meters.
// But since the only thing we need from the Meter is recording a value, we
// use a function for that that closes over the Meter itself.
recordFuncs map[event.Metric]recordFunc
}
type recordFunc func(context.Context, event.Label, []attribute.KeyValue)
var _ event.Handler = (*MetricHandler)(nil)
// NewMetricHandler creates a new MetricHandler.
func NewMetricHandler(m metric.Meter) *MetricHandler {
return &MetricHandler{
meter: metric.Must(m),
recordFuncs: map[event.Metric]recordFunc{},
}
}
func (m *MetricHandler) Event(ctx context.Context, e *event.Event) context.Context {
if e.Kind != event.MetricKind {
return ctx
}
// Get the otel instrument corresponding to the event's MetricDescriptor,
// or create a new one.
mi, ok := event.MetricKey.Find(e)
if !ok {
panic(errors.New("no metric key for metric event"))
}
em := mi.(event.Metric)
lval := e.Find(event.MetricVal)
if !lval.HasValue() {
panic(errors.New("no metric value for metric event"))
}
rf := m.getRecordFunc(em)
if rf == nil {
panic(fmt.Errorf("unable to record for metric %v", em))
}
rf(ctx, lval, labelsToAttributes(e.Labels))
return ctx
}
func (m *MetricHandler) getRecordFunc(em event.Metric) recordFunc {
m.mu.Lock()
defer m.mu.Unlock()
if f, ok := m.recordFuncs[em]; ok {
return f
}
f := m.newRecordFunc(em)
m.recordFuncs[em] = f
return f
}
func (m *MetricHandler) newRecordFunc(em event.Metric) recordFunc {
opts := em.Options()
name := opts.Namespace + "/" + em.Name()
otelOpts := []metric.InstrumentOption{
metric.WithDescription(opts.Description),
metric.WithUnit(otelunit.Unit(opts.Unit)), // cast OK: same strings
}
switch em.(type) {
case *event.Counter:
c := m.meter.NewInt64Counter(name, otelOpts...)
return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
c.Add(ctx, l.Int64(), attrs...)
}
case *event.FloatGauge:
g := m.meter.NewFloat64UpDownCounter(name, otelOpts...)
return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
g.Add(ctx, l.Float64(), attrs...)
}
case *event.DurationDistribution:
r := m.meter.NewInt64Histogram(name, otelOpts...)
return func(ctx context.Context, l event.Label, attrs []attribute.KeyValue) {
r.Record(ctx, l.Duration().Nanoseconds(), attrs...)
}
default:
return nil
}
}
func labelsToAttributes(ls []event.Label) []attribute.KeyValue {
var attrs []attribute.KeyValue
for _, l := range ls {
if l.Name == string(event.MetricKey) || l.Name == string(event.MetricVal) {
continue
}
attrs = append(attrs, labelToAttribute(l))
}
return attrs
}
func labelToAttribute(l event.Label) attribute.KeyValue {
switch {
case l.IsString():
return attribute.String(l.Name, l.String())
case l.IsInt64():
return attribute.Int64(l.Name, l.Int64())
case l.IsFloat64():
return attribute.Float64(l.Name, l.Float64())
case l.IsBool():
return attribute.Bool(l.Name, l.Bool())
default: // including uint64
panic(fmt.Errorf("cannot convert label value of type %T to attribute.KeyValue", l.Interface()))
}
}