github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/instrument/config_prometheus.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package instrument
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"log"
    29  	"net"
    30  	"net/http"
    31  	"os"
    32  	"strings"
    33  
    34  	prom "github.com/m3db/prometheus_client_golang/prometheus"
    35  	"github.com/m3db/prometheus_client_golang/prometheus/promhttp"
    36  	dto "github.com/m3db/prometheus_client_model/go"
    37  	extprom "github.com/prometheus/client_golang/prometheus"
    38  	"github.com/uber-go/tally/prometheus"
    39  )
    40  
    41  // PrometheusConfiguration is a configuration for a Prometheus reporter.
    42  type PrometheusConfiguration struct {
    43  	// HandlerPath if specified will be used instead of using the default
    44  	// HTTP handler path "/metrics".
    45  	HandlerPath string `yaml:"handlerPath"`
    46  
    47  	// ListenAddress if specified will be used instead of just registering the
    48  	// handler on the default HTTP serve mux without listening.
    49  	ListenAddress string `yaml:"listenAddress"`
    50  
    51  	// TimerType is the default Prometheus type to use for Tally timers.
    52  	TimerType string `yaml:"timerType"`
    53  
    54  	// DefaultHistogramBuckets if specified will set the default histogram
    55  	// buckets to be used by the reporter.
    56  	DefaultHistogramBuckets []prometheus.HistogramObjective `yaml:"defaultHistogramBuckets"`
    57  
    58  	// DefaultSummaryObjectives if specified will set the default summary
    59  	// objectives to be used by the reporter.
    60  	DefaultSummaryObjectives []prometheus.SummaryObjective `yaml:"defaultSummaryObjectives"`
    61  
    62  	// OnError specifies what to do when an error either with listening
    63  	// on the specified listen address or registering a metric with the
    64  	// Prometheus. By default the registerer will panic.
    65  	OnError string `yaml:"onError"`
    66  }
    67  
    68  // HistogramObjective is a Prometheus histogram bucket.
    69  // See: https://godoc.org/github.com/prometheus/client_golang/prometheus#HistogramOpts
    70  type HistogramObjective struct {
    71  	Upper float64 `yaml:"upper"`
    72  }
    73  
    74  // SummaryObjective is a Prometheus summary objective.
    75  // See: https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts
    76  type SummaryObjective struct {
    77  	Percentile   float64 `yaml:"percentile"`
    78  	AllowedError float64 `yaml:"allowedError"`
    79  }
    80  
    81  // PrometheusConfigurationOptions allows some programatic options, such as using a
    82  // specific registry and what error callback to register.
    83  type PrometheusConfigurationOptions struct {
    84  	// Registry if not nil will specify the specific registry to use
    85  	// for registering metrics.
    86  	Registry *prom.Registry
    87  	// ExternalRegistries if set (with combination of a specified Registry)
    88  	// will also
    89  	ExternalRegistries []PrometheusExternalRegistry
    90  	// HandlerListener is the listener to register the server handler on.
    91  	HandlerListener net.Listener
    92  	// DefaultServeMux is the ServeMux to use if no HandlerListener or
    93  	// ListenAddress on PrometheusConfiguration is specified.
    94  	DefaultServeMux *http.ServeMux
    95  	// HandlerOpts is the reporter HTTP handler options, not specifying will
    96  	// use defaults.
    97  	HandlerOpts promhttp.HandlerOpts
    98  	// OnError allows for customization of what to do when a metric
    99  	// registration error fails, the default is to panic.
   100  	OnError func(e error)
   101  	// CommonLabels will be appended to every metric gathered.
   102  	CommonLabels map[string]string
   103  }
   104  
   105  // PrometheusExternalRegistry is an external Prometheus registry
   106  // to also expose as part of the handler.
   107  type PrometheusExternalRegistry struct {
   108  	// Registry is the external prometheus registry to list.
   109  	Registry *extprom.Registry
   110  	// SubScope will add a prefix to all metric names exported by
   111  	// this registry.
   112  	SubScope string
   113  }
   114  
   115  type reporterCloser struct {
   116  	closeFn func() error
   117  }
   118  
   119  func newReporterCloser() reporterCloser {
   120  	return reporterCloser{
   121  		closeFn: func() error {
   122  			return nil
   123  		},
   124  	}
   125  }
   126  
   127  func (r reporterCloser) Close() error {
   128  	return r.closeFn()
   129  }
   130  
   131  // NewReporter creates a new M3 Prometheus reporter from this configuration.
   132  func (c PrometheusConfiguration) NewReporter(
   133  	configOpts PrometheusConfigurationOptions,
   134  ) (prometheus.Reporter, io.Closer, error) {
   135  	registry := configOpts.Registry
   136  	if registry == nil {
   137  		registry = prom.NewRegistry()
   138  	}
   139  	opts := prometheus.Options{
   140  		Registerer: registry,
   141  		Gatherer:   registry,
   142  	}
   143  
   144  	if configOpts.OnError != nil {
   145  		opts.OnRegisterError = configOpts.OnError
   146  	} else {
   147  		switch c.OnError {
   148  		case "stderr":
   149  			opts.OnRegisterError = func(err error) {
   150  				fmt.Fprintf(os.Stderr, "tally prometheus reporter error: %v\n", err)
   151  			}
   152  		case "log":
   153  			opts.OnRegisterError = func(err error) {
   154  				log.Printf("tally prometheus reporter error: %v\n", err)
   155  			}
   156  		case "none":
   157  			opts.OnRegisterError = func(err error) {}
   158  		default:
   159  			opts.OnRegisterError = func(err error) {
   160  				panic(err)
   161  			}
   162  		}
   163  	}
   164  
   165  	switch c.TimerType {
   166  	case "summary":
   167  		opts.DefaultTimerType = prometheus.SummaryTimerType
   168  	case "histogram":
   169  		opts.DefaultTimerType = prometheus.HistogramTimerType
   170  	}
   171  
   172  	if len(c.DefaultHistogramBuckets) > 0 {
   173  		values := make([]float64, 0, len(c.DefaultHistogramBuckets))
   174  		for _, value := range c.DefaultHistogramBuckets {
   175  			values = append(values, value.Upper)
   176  		}
   177  		opts.DefaultHistogramBuckets = values
   178  	}
   179  
   180  	if len(c.DefaultSummaryObjectives) > 0 {
   181  		values := make(map[float64]float64, len(c.DefaultSummaryObjectives))
   182  		for _, value := range c.DefaultSummaryObjectives {
   183  			values[value.Percentile] = value.AllowedError
   184  		}
   185  		opts.DefaultSummaryObjectives = values
   186  	}
   187  
   188  	reporter := prometheus.NewReporter(opts)
   189  
   190  	path := "/metrics"
   191  	if handlerPath := strings.TrimSpace(c.HandlerPath); handlerPath != "" {
   192  		path = handlerPath
   193  	}
   194  
   195  	gatherer := newMultiGatherer(registry, configOpts.ExternalRegistries, configOpts.CommonLabels)
   196  	handler := promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{})
   197  
   198  	addr := strings.TrimSpace(c.ListenAddress)
   199  	closer := newReporterCloser()
   200  	if addr == "" && configOpts.HandlerListener == nil {
   201  		// If address not specified and server not specified, register
   202  		// on default mux.
   203  		if configOpts.DefaultServeMux == nil {
   204  			return nil, nil, errors.New(
   205  				"must specify a DefaultServeMux option when not specifying a listener",
   206  			)
   207  		}
   208  		configOpts.DefaultServeMux.Handle(path, handler)
   209  	} else {
   210  		mux := http.NewServeMux()
   211  		mux.Handle(path, handler)
   212  
   213  		listener := configOpts.HandlerListener
   214  		if listener == nil {
   215  			// Address must be specified if server was nil.
   216  			var err error
   217  			listener, err = net.Listen("tcp", addr)
   218  			if err != nil {
   219  				return nil, nil, fmt.Errorf(
   220  					"prometheus handler listen address error: %v", err)
   221  			}
   222  		}
   223  
   224  		server := &http.Server{Handler: mux}
   225  		go func() {
   226  			if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
   227  				opts.OnRegisterError(err)
   228  			}
   229  		}()
   230  		closer.closeFn = func() error {
   231  			return server.Shutdown(context.Background())
   232  		}
   233  	}
   234  
   235  	return reporter, closer, nil
   236  }
   237  
   238  func newMultiGatherer(
   239  	primary *prom.Registry,
   240  	ext []PrometheusExternalRegistry,
   241  	commonLabels map[string]string,
   242  ) prom.Gatherer {
   243  	return &multiGatherer{
   244  		primary:      primary,
   245  		ext:          ext,
   246  		commonLabels: commonLabels,
   247  	}
   248  }
   249  
   250  var _ prom.Gatherer = (*multiGatherer)(nil)
   251  
   252  type multiGatherer struct {
   253  	primary      *prom.Registry
   254  	ext          []PrometheusExternalRegistry
   255  	commonLabels map[string]string
   256  }
   257  
   258  func (g *multiGatherer) Gather() ([]*dto.MetricFamily, error) {
   259  	results, err := g.primary.Gather()
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	appendLabelsToMetrics(g.commonLabels, results)
   265  
   266  	if len(g.ext) == 0 {
   267  		return results, nil
   268  	}
   269  
   270  	for _, secondary := range g.ext {
   271  		gathered, err := secondary.Registry.Gather()
   272  		if err != nil {
   273  			return nil, err
   274  		}
   275  
   276  		for _, elem := range gathered {
   277  			entry := &dto.MetricFamily{
   278  				Name:   elem.Name,
   279  				Help:   elem.Help,
   280  				Metric: make([]*dto.Metric, 0, len(elem.Metric)),
   281  			}
   282  
   283  			if secondary.SubScope != "" && entry.Name != nil {
   284  				scopedName := fmt.Sprintf("%s_%s", secondary.SubScope, *entry.Name)
   285  				entry.Name = &scopedName
   286  			}
   287  
   288  			if v := elem.Type; v != nil {
   289  				metricType := dto.MetricType(*v)
   290  				entry.Type = &metricType
   291  			}
   292  
   293  			for _, metricElem := range elem.Metric {
   294  				metricEntry := &dto.Metric{
   295  					Label:       make([]*dto.LabelPair, 0, len(metricElem.Label)),
   296  					TimestampMs: metricElem.TimestampMs,
   297  				}
   298  
   299  				if v := metricElem.Gauge; v != nil {
   300  					metricEntry.Gauge = &dto.Gauge{
   301  						Value: v.Value,
   302  					}
   303  				}
   304  
   305  				if v := metricElem.Counter; v != nil {
   306  					metricEntry.Counter = &dto.Counter{
   307  						Value: v.Value,
   308  					}
   309  				}
   310  
   311  				if v := metricElem.Summary; v != nil {
   312  					metricEntry.Summary = &dto.Summary{
   313  						SampleCount: v.SampleCount,
   314  						SampleSum:   v.SampleSum,
   315  						Quantile:    make([]*dto.Quantile, 0, len(v.Quantile)),
   316  					}
   317  
   318  					for _, quantileElem := range v.Quantile {
   319  						quantileEntry := &dto.Quantile{
   320  							Quantile: quantileElem.Quantile,
   321  							Value:    quantileElem.Value,
   322  						}
   323  						metricEntry.Summary.Quantile =
   324  							append(metricEntry.Summary.Quantile, quantileEntry)
   325  					}
   326  				}
   327  
   328  				if v := metricElem.Untyped; v != nil {
   329  					metricEntry.Untyped = &dto.Untyped{
   330  						Value: v.Value,
   331  					}
   332  				}
   333  
   334  				if v := metricElem.Histogram; v != nil {
   335  					metricEntry.Histogram = &dto.Histogram{
   336  						SampleCount: v.SampleCount,
   337  						SampleSum:   v.SampleSum,
   338  						Bucket:      make([]*dto.Bucket, 0, len(v.Bucket)),
   339  					}
   340  
   341  					for _, bucketElem := range v.Bucket {
   342  						bucketEntry := &dto.Bucket{
   343  							CumulativeCount: bucketElem.CumulativeCount,
   344  							UpperBound:      bucketElem.UpperBound,
   345  						}
   346  						metricEntry.Histogram.Bucket =
   347  							append(metricEntry.Histogram.Bucket, bucketEntry)
   348  					}
   349  				}
   350  
   351  				for _, labelElem := range metricElem.Label {
   352  					labelEntry := &dto.LabelPair{
   353  						Name:  labelElem.Name,
   354  						Value: labelElem.Value,
   355  					}
   356  
   357  					metricEntry.Label = append(metricEntry.Label, labelEntry)
   358  				}
   359  
   360  				appendLabels(g.commonLabels, metricEntry)
   361  
   362  				entry.Metric = append(entry.Metric, metricEntry)
   363  			}
   364  
   365  			results = append(results, entry)
   366  		}
   367  	}
   368  
   369  	return results, nil
   370  }
   371  
   372  func appendLabels(commonLabels map[string]string, metric *dto.Metric) {
   373  	for name, value := range commonLabels {
   374  		name := name
   375  		value := value
   376  		metric.Label = append(metric.Label, &dto.LabelPair{Name: &name, Value: &value})
   377  	}
   378  }
   379  
   380  func appendLabelsToMetrics(commonLabels map[string]string, results []*dto.MetricFamily) {
   381  	if len(commonLabels) > 0 {
   382  		for _, metricFamily := range results {
   383  			for _, metric := range metricFamily.Metric {
   384  				appendLabels(commonLabels, metric)
   385  			}
   386  		}
   387  	}
   388  }