go.temporal.io/server@v1.23.0/common/metrics/metricstest/metricstest.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 metricstest 26 27 import ( 28 "errors" 29 "fmt" 30 "net/http" 31 "net/http/httptest" 32 "strings" 33 34 "github.com/prometheus/client_golang/prometheus" 35 "github.com/prometheus/client_golang/prometheus/promhttp" 36 dto "github.com/prometheus/client_model/go" 37 "github.com/prometheus/common/expfmt" 38 exporters "go.opentelemetry.io/otel/exporters/prometheus" 39 "go.opentelemetry.io/otel/metric" 40 sdkmetrics "go.opentelemetry.io/otel/sdk/metric" 41 "golang.org/x/exp/maps" 42 43 "go.temporal.io/server/common/log" 44 "go.temporal.io/server/common/metrics" 45 ) 46 47 type ( 48 Handler struct { 49 metrics.Handler 50 reg *prometheus.Registry 51 } 52 53 sample struct { 54 metricType dto.MetricType 55 labelValues map[string]string 56 sampleValue float64 57 } 58 59 HistogramBucket struct { 60 value float64 61 upperBound float64 62 } 63 64 histogramSample struct { 65 metricType dto.MetricType 66 labelValues map[string]string 67 buckets []HistogramBucket 68 } 69 70 Snapshot struct { 71 samples map[string]sample 72 histogramSamples map[string]histogramSample 73 } 74 ) 75 76 // Potential errors that the test handler can return trying to find a metric to return. 77 var ( 78 ErrMetricNotFound = errors.New("metric not found") 79 ErrMetricTypeMismatch = errors.New("metric is not the expected type") 80 ErrMetricLabelMismatch = errors.New("metric labels do not match expected labels") 81 ) 82 83 func NewHandler(logger log.Logger, clientConfig metrics.ClientConfig) (*Handler, error) { 84 registry := prometheus.NewRegistry() 85 exporter, err := exporters.New(exporters.WithRegisterer(registry)) 86 if err != nil { 87 return nil, err 88 } 89 90 // Set any custom histogram bucket configuration. 91 var views []sdkmetrics.View 92 for _, u := range []string{metrics.Dimensionless, metrics.Bytes, metrics.Milliseconds} { 93 views = append(views, sdkmetrics.NewView( 94 sdkmetrics.Instrument{ 95 Kind: sdkmetrics.InstrumentKindHistogram, 96 Unit: u, 97 }, 98 sdkmetrics.Stream{ 99 Aggregation: sdkmetrics.AggregationExplicitBucketHistogram{ 100 Boundaries: clientConfig.PerUnitHistogramBoundaries[u], 101 }, 102 }, 103 )) 104 } 105 provider := sdkmetrics.NewMeterProvider( 106 sdkmetrics.WithReader(exporter), 107 sdkmetrics.WithView(views...), 108 ) 109 meter := provider.Meter("temporal") 110 111 otelHandler, err := metrics.NewOtelMetricsHandler(logger, &otelProvider{meter: meter}, clientConfig) 112 if err != nil { 113 return nil, err 114 } 115 metricsHandler := &Handler{ 116 Handler: otelHandler, 117 reg: registry, 118 } 119 120 return metricsHandler, nil 121 } 122 123 func (*Handler) Stop(log.Logger) {} 124 125 func (h *Handler) Snapshot() (Snapshot, error) { 126 rec := httptest.NewRecorder() 127 req := httptest.NewRequest("GET", "/metrics", nil) 128 handler := http.NewServeMux() 129 handler.HandleFunc("/metrics", promhttp.HandlerFor(h.reg, promhttp.HandlerOpts{Registry: h.reg}).ServeHTTP) 130 handler.ServeHTTP(rec, req) 131 132 var tp expfmt.TextParser 133 families, err := tp.TextToMetricFamilies(rec.Body) 134 if err != nil { 135 return Snapshot{}, err 136 } 137 samples := map[string]sample{} 138 histogramSamples := map[string]histogramSample{} 139 for name, family := range families { 140 for _, m := range family.GetMetric() { 141 collectSamples(name, family, m, samples, histogramSamples) 142 } 143 } 144 return Snapshot{ 145 samples: samples, 146 histogramSamples: histogramSamples, 147 }, nil 148 } 149 150 func collectSamples(name string, family *dto.MetricFamily, m *dto.Metric, samples map[string]sample, histogramSamples map[string]histogramSample) { 151 labelvalues := map[string]string{} 152 for _, lp := range m.GetLabel() { 153 labelvalues[lp.GetName()] = lp.GetValue() 154 } 155 // This only records the last sample if there 156 // are multiple samples recorded. 157 switch family.GetType() { 158 default: 159 // Not yet supporting summary, untyped. 160 case dto.MetricType_HISTOGRAM: 161 buckets := m.Histogram.GetBucket() 162 hbs := []HistogramBucket{} 163 for _, bucket := range buckets { 164 hb := HistogramBucket{ 165 value: float64(bucket.GetCumulativeCount()), 166 upperBound: bucket.GetUpperBound(), 167 } 168 hbs = append(hbs, hb) 169 } 170 histogramSamples[name] = histogramSample{ 171 metricType: family.GetType(), 172 labelValues: labelvalues, 173 buckets: hbs, 174 } 175 case dto.MetricType_COUNTER: 176 samples[name] = sample{ 177 metricType: family.GetType(), 178 labelValues: labelvalues, 179 sampleValue: m.Counter.GetValue(), 180 } 181 case dto.MetricType_GAUGE: 182 samples[name] = sample{ 183 metricType: family.GetType(), 184 labelValues: labelvalues, 185 sampleValue: m.Gauge.GetValue(), 186 } 187 } 188 } 189 190 var _ metrics.OpenTelemetryProvider = (*otelProvider)(nil) 191 192 type otelProvider struct { 193 meter metric.Meter 194 } 195 196 func (m *otelProvider) GetMeter() metric.Meter { 197 return m.meter 198 } 199 200 func (m *otelProvider) Stop(log.Logger) {} 201 202 func (s Snapshot) getValue(name string, metricType dto.MetricType, tags ...metrics.Tag) (float64, error) { 203 labelValues := map[string]string{} 204 for _, tag := range tags { 205 labelValues[tag.Key()] = tag.Value() 206 } 207 sample, ok := s.samples[name] 208 if !ok { 209 return 0, fmt.Errorf("%w: %q", ErrMetricNotFound, name) 210 } 211 if sample.metricType != metricType { 212 return 0, fmt.Errorf("%w: %q is a %s, not a %s", ErrMetricTypeMismatch, name, sample.metricType, metricType) 213 } 214 if !maps.Equal(sample.labelValues, labelValues) { 215 return 0, fmt.Errorf("%w: %q has %v, asked for %v", ErrMetricLabelMismatch, name, sample.labelValues, labelValues) 216 } 217 return sample.sampleValue, nil 218 } 219 220 func (s Snapshot) Counter(name string, tags ...metrics.Tag) (float64, error) { 221 return s.getValue(name, dto.MetricType_COUNTER, tags...) 222 } 223 224 func (s Snapshot) Gauge(name string, tags ...metrics.Tag) (float64, error) { 225 return s.getValue(name, dto.MetricType_GAUGE, tags...) 226 } 227 228 func (s Snapshot) Histogram(name string, tags ...metrics.Tag) ([]HistogramBucket, error) { 229 labelValues := map[string]string{} 230 for _, tag := range tags { 231 labelValues[tag.Key()] = tag.Value() 232 } 233 234 sample, ok := s.histogramSamples[name] 235 if !ok { 236 return nil, fmt.Errorf("%w: %q", ErrMetricNotFound, name) 237 } 238 if sample.metricType != dto.MetricType_HISTOGRAM { 239 return nil, fmt.Errorf("%w: %q is a %s, not a %s", ErrMetricTypeMismatch, name, sample.metricType, dto.MetricType_HISTOGRAM) 240 } 241 if !maps.Equal(sample.labelValues, labelValues) { 242 return nil, fmt.Errorf("%w: %q has %v, asked for %v", ErrMetricLabelMismatch, name, sample.labelValues, labelValues) 243 } 244 return sample.buckets, nil 245 } 246 247 func (s Snapshot) String() string { 248 var b strings.Builder 249 for n, s := range s.samples { 250 _, _ = b.WriteString(fmt.Sprintf("%v %v %v %v\n", n, s.labelValues, s.sampleValue, s.metricType)) 251 } 252 for n, s := range s.histogramSamples { 253 _, _ = b.WriteString(fmt.Sprintf("%v %v %v\n", n, s.labelValues, s.metricType)) 254 for _, bucket := range s.buckets { 255 _, _ = b.WriteString(fmt.Sprintf(" %v: %v \n", bucket.upperBound, bucket.value)) 256 } 257 } 258 return b.String() 259 }