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 }