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  }