Skip to content

Commit

Permalink
Use generics for OTEL options
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelSnowden committed Jun 8, 2023
1 parent b1c090c commit f4a8e2a
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 41 deletions.
6 changes: 4 additions & 2 deletions .golangci.yml
Expand Up @@ -59,14 +59,16 @@ linters-settings:
disabled: true
- name: unexported-naming
disabled: true
- name: unexported-return
disabled: true
- name: unused-parameter
disabled: true
- name: unused-receiver
disabled: true
- name: use-any
disabled: true
- name: var-naming
disabled: true
- name: unused-parameter
disabled: true

# Rule tuning
- name: argument-limit
Expand Down
24 changes: 16 additions & 8 deletions common/metrics/catalog_builder.go
Expand Up @@ -30,20 +30,28 @@ import (
"sync"
)

var errMetricAlreadyExists = errors.New("metric already exists")

type metricCatalog map[string]MetricDefinition
type (
// metricCatalog is a map of metric name to metric definition
metricCatalog map[string]MetricDefinition
// catalogBuilder tracks a list of [MetricDefinition] objects added with addMetric and then builds a metricCatalog
// of them using the build method. See globalCatalogBuilder for more.
catalogBuilder struct {
sync.Mutex
definitions []MetricDefinition
}
)

type catalogBuilder struct {
sync.Mutex
definitions []MetricDefinition
}
// errMetricAlreadyExists is returned by catalogBuilder.build when a metric is defined twice.
var errMetricAlreadyExists = errors.New("metric already exists")

// addMetric adds a metric definition to the list of pending metric definitions.
func (c *catalogBuilder) addMetric(name string, opts ...Option) MetricDefinition {
c.Lock()
defer c.Unlock()
d := MetricDefinition{
name: name,
name: name,
description: "",
unit: "",
}
for _, opt := range opts {
opt.apply(&d)
Expand Down
23 changes: 21 additions & 2 deletions common/metrics/defs.go
Expand Up @@ -48,6 +48,27 @@ const (
Bytes = "By"
)

// globalCatalogBuilder is a catalogBuilder which tracks metrics defined via the New*Def methods. We use a global
// variable here so that clients may continue to refer to global metric variables defined in this package without
// requiring access to another object, while still allowing us to iterate over all metrics defined in the package to
// register them with the metrics system. The order of operations is this:
//
// 1. When the metrics package is initialized, the New*Def are called to define metrics,
// which adds them metric to the global catalog builder.
// 2. Before a Handler object is constructed, one this package's fx provider functions will call catalogBuilder.build to
// retrieve the metrics defined in this package.
// 3. The constructed metricCatalog is passed to the Handler so that it knows the metadata for all metrics defined in
// this package.
// 4. Clients call methods on the Handler to obtain metric objects like Handler.Counter and Handler.Timer.
// 5. Those methods retrieve the metadata from the catalog and use it to construct the metric object using a third-party
// metrics library, e.g. OpenTelemetry. This is where most of the work happens.
// 6. Clients record a metric using that metrics object, e.g. by calling CounterFunc, and the sample is recorded.
// 7. At some point, the /metrics endpoint is scraped, and the Prometheus handler we register will iterate over all of
// the aggregated samples and definitions and write them to the response. The metric metadata we passed to the
// third-party metrics library in step 5 is used here and rendered in the response as comments like
// # HELP <metric name> <metric description>.
var globalCatalogBuilder catalogBuilder

func (md MetricDefinition) GetMetricName() string {
return md.name
}
Expand Down Expand Up @@ -75,5 +96,3 @@ func NewCounterDef(name string, opts ...Option) MetricDefinition {
func NewGaugeDef(name string, opts ...Option) MetricDefinition {
return globalCatalogBuilder.addMetric(name, opts...)
}

var globalCatalogBuilder catalogBuilder
43 changes: 14 additions & 29 deletions common/metrics/otel_metrics_handler.go
Expand Up @@ -35,15 +35,16 @@ import (
"go.temporal.io/server/common/log/tag"
)

// MetricsHandler is an event.Handler for OpenTelemetry metrics.
// Its Event method handles Metric events and ignores all others.
type otelMetricsHandler struct {
l log.Logger
tags []Tag
provider OpenTelemetryProvider
excludeTags excludeTags
catalog metricCatalog
}
type (
// otelMetricsHandler is a Handler for OpenTelemetry metrics.
otelMetricsHandler struct {
l log.Logger
tags []Tag
provider OpenTelemetryProvider
excludeTags excludeTags
catalog metricCatalog
}
)

var _ Handler = (*otelMetricsHandler)(nil)

Expand All @@ -69,10 +70,7 @@ func (omp *otelMetricsHandler) WithTags(tags ...Tag) Handler {

// Counter obtains a counter for the given name and MetricOptions.
func (omp *otelMetricsHandler) Counter(counter string) CounterIface {
var opts []metric.Int64CounterOption
if description := omp.getMetadata(counter); description != "" {
opts = append(opts, metric.WithDescription(description))
}
opts := addOptionsFromCatalog(omp, int64CounterOptions{}, counter)
c, err := omp.provider.GetMeter().Int64Counter(counter, opts...)
if err != nil {
omp.l.Fatal("error getting metric", tag.NewStringTag("MetricName", counter), tag.Error(err))
Expand All @@ -86,10 +84,7 @@ func (omp *otelMetricsHandler) Counter(counter string) CounterIface {

// Gauge obtains a gauge for the given name and MetricOptions.
func (omp *otelMetricsHandler) Gauge(gauge string) GaugeIface {
var opts []metric.Float64ObservableGaugeOption
if description := omp.getMetadata(gauge); description != "" {
opts = append(opts, metric.WithDescription(description))
}
opts := addOptionsFromCatalog(omp, float64ObservableGaugeOptions{}, gauge)
c, err := omp.provider.GetMeter().Float64ObservableGauge(gauge, opts...)
if err != nil {
omp.l.Fatal("error getting metric", tag.NewStringTag("MetricName", gauge), tag.Error(err))
Expand All @@ -109,10 +104,7 @@ func (omp *otelMetricsHandler) Gauge(gauge string) GaugeIface {

// Timer obtains a timer for the given name and MetricOptions.
func (omp *otelMetricsHandler) Timer(timer string) TimerIface {
opts := []metric.Int64HistogramOption{metric.WithUnit(Milliseconds)}
if description := omp.getMetadata(timer); description != "" {
opts = append(opts, metric.WithDescription(description))
}
opts := addOptionsFromCatalog(omp, int64HistogramOptions{metric.WithUnit(Milliseconds)}, timer)
c, err := omp.provider.GetMeter().Int64Histogram(timer, opts...)
if err != nil {
omp.l.Fatal("error getting metric", tag.NewStringTag("MetricName", timer), tag.Error(err))
Expand All @@ -126,10 +118,7 @@ func (omp *otelMetricsHandler) Timer(timer string) TimerIface {

// Histogram obtains a histogram for the given name and MetricOptions.
func (omp *otelMetricsHandler) Histogram(histogram string, unit MetricUnit) HistogramIface {
opts := []metric.Int64HistogramOption{metric.WithUnit(string(unit))}
if description := omp.getMetadata(histogram); description != "" {
opts = append(opts, metric.WithDescription(description))
}
opts := addOptionsFromCatalog(omp, int64HistogramOptions{metric.WithUnit(string(unit))}, histogram)
c, err := omp.provider.GetMeter().Int64Histogram(histogram, opts...)
if err != nil {
omp.l.Fatal("error getting metric", tag.NewStringTag("MetricName", histogram), tag.Error(err))
Expand All @@ -145,10 +134,6 @@ func (omp *otelMetricsHandler) Stop(l log.Logger) {
omp.provider.Stop(l)
}

func (omp *otelMetricsHandler) getMetadata(metricName string) string {
return omp.catalog[metricName].description
}

// tagsToAttributes helper to merge registred tags and additional tags converting to attribute.KeyValue struct
func tagsToAttributes(t1 []Tag, t2 []Tag, e excludeTags) []attribute.KeyValue {
var attrs []attribute.KeyValue
Expand Down
69 changes: 69 additions & 0 deletions common/metrics/otel_options.go
@@ -0,0 +1,69 @@
// The MIT License
//
// Copyright (c) 2020 Temporal Technologies Inc. All rights reserved.
//
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package metrics

import (
"go.opentelemetry.io/otel/metric"
)

type (
// optionSet represents a slice of metric options. We need it to be able to add options of the
// [metric.InstrumentOption] type to slices which may be of any other type implemented by metric.InstrumentOption.
// Normally, you could do something like `T metric.InstrumentOption` here, but the type dependency here is reversed.
// We need a generic type T that is implemented *by* metric.InstrumentOption, not the other way around.
// This is the only solution which avoids duplicating all the logic of the addOptionsFromCatalog function without
// relying on reflection, an error-prone type assertion, or a type switch with a runtime error for unhandled cases.
optionSet[T any] interface {
addOption(option metric.InstrumentOption) T
}
int64HistogramOptions []metric.Int64HistogramOption
int64CounterOptions []metric.Int64CounterOption
float64ObservableGaugeOptions []metric.Float64ObservableGaugeOption
)

func addOptionsFromCatalog[T optionSet[T]](omp *otelMetricsHandler, opts T, metricName string) T {
entry, ok := omp.catalog[metricName]
if !ok {
return opts
}

if description := entry.description; description != "" {
opts = opts.addOption(metric.WithDescription(description))
}

return opts
}

func (opts float64ObservableGaugeOptions) addOption(option metric.InstrumentOption) float64ObservableGaugeOptions {
return append(opts, option)
}

func (opts int64CounterOptions) addOption(option metric.InstrumentOption) int64CounterOptions {
return append(opts, option)
}

func (opts int64HistogramOptions) addOption(option metric.InstrumentOption) int64HistogramOptions {
return append(opts, option)
}

0 comments on commit f4a8e2a

Please sign in to comment.