go.temporal.io/server@v1.23.0/common/metrics/otel_metrics_handler.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package metrics
    26  
    27  import (
    28  	"context"
    29  	"fmt"
    30  	"sync"
    31  	"time"
    32  
    33  	"go.opentelemetry.io/otel/attribute"
    34  	"go.opentelemetry.io/otel/metric"
    35  
    36  	"go.temporal.io/server/common/log"
    37  	"go.temporal.io/server/common/log/tag"
    38  )
    39  
    40  // otelMetricsHandler is an adapter around an OpenTelemetry [metric.Meter] that implements the [Handler] interface.
    41  type (
    42  	otelMetricsHandler struct {
    43  		l           log.Logger
    44  		set         attribute.Set
    45  		provider    OpenTelemetryProvider
    46  		excludeTags map[string]map[string]struct{}
    47  		catalog     catalog
    48  		gauges      *sync.Map // string -> *gaugeAdapter. note: shared between multiple otelMetricsHandlers
    49  	}
    50  
    51  	// This is to work around the lack of synchronous gauge:
    52  	// https://github.com/open-telemetry/opentelemetry-specification/issues/2318
    53  	// Basically, otel gauges only support getting a value with a callback, they can't store a
    54  	// value for us. So we have to store it ourselves and supply it to a callback.
    55  	gaugeAdapter struct {
    56  		lock   sync.Mutex
    57  		values map[attribute.Distinct]gaugeValue
    58  	}
    59  	gaugeValue struct {
    60  		value float64
    61  		// In practice, we can use attribute.Set itself as the map key in gaugeAdapter and it
    62  		// works, but according to the API we should use attribute.Distinct. But we can't get a
    63  		// Set back from a Distinct, so we have to store the set here also.
    64  		set attribute.Set
    65  	}
    66  	gaugeAdapterGauge struct {
    67  		omp     *otelMetricsHandler
    68  		adapter *gaugeAdapter
    69  	}
    70  )
    71  
    72  var _ Handler = (*otelMetricsHandler)(nil)
    73  
    74  // NewOtelMetricsHandler returns a new Handler that uses the provided OpenTelemetry [metric.Meter] to record metrics.
    75  // This OTel handler supports metric descriptions for metrics registered with the New*Def functions. However, those
    76  // functions must be called before this constructor. Otherwise, the descriptions will be empty. This is because the
    77  // OTel metric descriptions are generated from the globalRegistry. You may also record metrics that are not registered
    78  // via the New*Def functions. In that case, the metric description will be the OTel default (the metric name itself).
    79  func NewOtelMetricsHandler(
    80  	l log.Logger,
    81  	o OpenTelemetryProvider,
    82  	cfg ClientConfig,
    83  ) (*otelMetricsHandler, error) {
    84  	c, err := globalRegistry.buildCatalog()
    85  	if err != nil {
    86  		return nil, fmt.Errorf("failed to build metrics catalog: %w", err)
    87  	}
    88  	return &otelMetricsHandler{
    89  		l:           l,
    90  		set:         makeInitialSet(cfg.Tags),
    91  		provider:    o,
    92  		excludeTags: configExcludeTags(cfg),
    93  		catalog:     c,
    94  		gauges:      new(sync.Map),
    95  	}, nil
    96  }
    97  
    98  // WithTags creates a new Handler with the provided Tag list.
    99  // Tags are merged with the existing tags.
   100  func (omp *otelMetricsHandler) WithTags(tags ...Tag) Handler {
   101  	newHandler := *omp
   102  	newHandler.set = newHandler.makeSet(tags)
   103  	return &newHandler
   104  }
   105  
   106  // Counter obtains a counter for the given name.
   107  func (omp *otelMetricsHandler) Counter(counter string) CounterIface {
   108  	opts := addOptions(omp, counterOptions{}, counter)
   109  	c, err := omp.provider.GetMeter().Int64Counter(counter, opts...)
   110  	if err != nil {
   111  		omp.l.Error("error getting metric", tag.NewStringTag("MetricName", counter), tag.Error(err))
   112  		return CounterFunc(func(i int64, t ...Tag) {})
   113  	}
   114  
   115  	return CounterFunc(func(i int64, t ...Tag) {
   116  		option := metric.WithAttributeSet(omp.makeSet(t))
   117  		c.Add(context.Background(), i, option)
   118  	})
   119  }
   120  
   121  func (omp *otelMetricsHandler) getGaugeAdapter(gauge string) (*gaugeAdapter, error) {
   122  	if v, ok := omp.gauges.Load(gauge); ok {
   123  		return v.(*gaugeAdapter), nil
   124  	}
   125  	adapter := &gaugeAdapter{
   126  		values: make(map[attribute.Distinct]gaugeValue),
   127  	}
   128  	if v, wasLoaded := omp.gauges.LoadOrStore(gauge, adapter); wasLoaded {
   129  		return v.(*gaugeAdapter), nil
   130  	}
   131  
   132  	opts := addOptions(omp, gaugeOptions{
   133  		metric.WithFloat64Callback(adapter.callback),
   134  	}, gauge)
   135  	// Register the gauge with otel. It will call our callback when it wants to read the values.
   136  	_, err := omp.provider.GetMeter().Float64ObservableGauge(gauge, opts...)
   137  	if err != nil {
   138  		omp.gauges.Delete(gauge)
   139  		omp.l.Error("error getting metric", tag.NewStringTag("MetricName", gauge), tag.Error(err))
   140  		return nil, err
   141  	}
   142  
   143  	return adapter, nil
   144  }
   145  
   146  // Gauge obtains a gauge for the given name.
   147  func (omp *otelMetricsHandler) Gauge(gauge string) GaugeIface {
   148  	adapter, err := omp.getGaugeAdapter(gauge)
   149  	if err != nil {
   150  		return GaugeFunc(func(i float64, t ...Tag) {})
   151  	}
   152  	return &gaugeAdapterGauge{
   153  		omp:     omp,
   154  		adapter: adapter,
   155  	}
   156  }
   157  
   158  func (a *gaugeAdapter) callback(ctx context.Context, o metric.Float64Observer) error {
   159  	a.lock.Lock()
   160  	defer a.lock.Unlock()
   161  	for _, v := range a.values {
   162  		o.Observe(v.value, metric.WithAttributeSet(v.set))
   163  	}
   164  	return nil
   165  }
   166  
   167  func (g *gaugeAdapterGauge) Record(v float64, tags ...Tag) {
   168  	set := g.omp.makeSet(tags)
   169  	g.adapter.lock.Lock()
   170  	defer g.adapter.lock.Unlock()
   171  	g.adapter.values[set.Equivalent()] = gaugeValue{value: v, set: set}
   172  }
   173  
   174  // Timer obtains a timer for the given name.
   175  func (omp *otelMetricsHandler) Timer(timer string) TimerIface {
   176  	opts := addOptions(omp, histogramOptions{metric.WithUnit(Milliseconds)}, timer)
   177  	c, err := omp.provider.GetMeter().Int64Histogram(timer, opts...)
   178  	if err != nil {
   179  		omp.l.Error("error getting metric", tag.NewStringTag("MetricName", timer), tag.Error(err))
   180  		return TimerFunc(func(i time.Duration, t ...Tag) {})
   181  	}
   182  
   183  	return TimerFunc(func(i time.Duration, t ...Tag) {
   184  		option := metric.WithAttributeSet(omp.makeSet(t))
   185  		c.Record(context.Background(), i.Milliseconds(), option)
   186  	})
   187  }
   188  
   189  // Histogram obtains a histogram for the given name.
   190  func (omp *otelMetricsHandler) Histogram(histogram string, unit MetricUnit) HistogramIface {
   191  	opts := addOptions(omp, histogramOptions{metric.WithUnit(string(unit))}, histogram)
   192  	c, err := omp.provider.GetMeter().Int64Histogram(histogram, opts...)
   193  	if err != nil {
   194  		omp.l.Error("error getting metric", tag.NewStringTag("MetricName", histogram), tag.Error(err))
   195  		return HistogramFunc(func(i int64, t ...Tag) {})
   196  	}
   197  
   198  	return HistogramFunc(func(i int64, t ...Tag) {
   199  		option := metric.WithAttributeSet(omp.makeSet(t))
   200  		c.Record(context.Background(), i, option)
   201  	})
   202  }
   203  
   204  func (omp *otelMetricsHandler) Stop(l log.Logger) {
   205  	omp.provider.Stop(l)
   206  }
   207  
   208  // makeSet returns an otel attribute.Set with the given tags merged with the
   209  // otelMetricsHandler's tags.
   210  func (omp *otelMetricsHandler) makeSet(tags []Tag) attribute.Set {
   211  	if len(tags) == 0 {
   212  		return omp.set
   213  	}
   214  	attrs := make([]attribute.KeyValue, 0, omp.set.Len()+len(tags))
   215  	for i := omp.set.Iter(); i.Next(); {
   216  		attrs = append(attrs, i.Attribute())
   217  	}
   218  	for _, t := range tags {
   219  		attrs = append(attrs, omp.convertTag(t))
   220  	}
   221  	return attribute.NewSet(attrs...)
   222  }
   223  
   224  func (omp *otelMetricsHandler) convertTag(tag Tag) attribute.KeyValue {
   225  	if vals, ok := omp.excludeTags[tag.Key()]; ok {
   226  		if _, ok := vals[tag.Value()]; !ok {
   227  			return attribute.String(tag.Key(), tagExcludedValue)
   228  		}
   229  	}
   230  	return attribute.String(tag.Key(), tag.Value())
   231  }
   232  
   233  func makeInitialSet(tags map[string]string) attribute.Set {
   234  	if len(tags) == 0 {
   235  		return *attribute.EmptySet()
   236  	}
   237  	var attrs []attribute.KeyValue
   238  	for k, v := range tags {
   239  		attrs = append(attrs, attribute.String(k, v))
   240  	}
   241  	return attribute.NewSet(attrs...)
   242  }