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 }