pkg/xcontext/metrics/prometheus/metrics.go (354 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
package prometheus
import (
"fmt"
"sort"
"strconv"
"strings"
"sync"
"github.com/facebookincubator/contest/pkg/xcontext/fields"
"github.com/facebookincubator/contest/pkg/xcontext/metrics"
"github.com/prometheus/client_golang/prometheus"
)
var _ metrics.Metrics = &Metrics{}
type Fields = fields.Fields
func mergeSortedStrings(dst []string, add ...string) []string {
if len(add) == 0 {
return dst
}
if len(dst) == 0 {
return append([]string{}, add...)
}
dstLen := len(dst)
if !sort.StringsAreSorted(dst) || !sort.StringsAreSorted(add) {
panic(fmt.Sprintf("%v %v", sort.StringsAreSorted(dst), sort.StringsAreSorted(add)))
}
i, j := 0, 0
for i < dstLen && j < len(add) {
switch strings.Compare(dst[i], add[j]) {
case -1:
i++
case 0:
i++
j++
case 1:
dst = append(dst, add[j])
j++
}
}
dst = append(dst, add[j:]...)
sort.Strings(dst)
return dst
}
func labelsWithPlaceholders(labels prometheus.Labels, placeholders []string) prometheus.Labels {
if len(labels) == len(placeholders) {
return labels
}
result := make(prometheus.Labels, len(placeholders))
for k, v := range labels {
result[k] = v
}
for _, s := range placeholders {
if _, ok := result[s]; ok {
continue
}
result[s] = ""
}
return result
}
type CounterVec struct {
*prometheus.CounterVec
Key string
PossibleLabels []string
}
func (v *CounterVec) AddPossibleLabels(newLabels []string) {
v.PossibleLabels = mergeSortedStrings(v.PossibleLabels, newLabels...)
}
func (v *CounterVec) GetMetricWith(labels prometheus.Labels) (prometheus.Counter, error) {
return v.CounterVec.GetMetricWith(labelsWithPlaceholders(labels, v.PossibleLabels))
}
type GaugeVec struct {
*prometheus.GaugeVec
Key string
PossibleLabels []string
}
func (v *GaugeVec) AddPossibleLabels(newLabels []string) {
v.PossibleLabels = mergeSortedStrings(v.PossibleLabels, newLabels...)
}
func (v *GaugeVec) GetMetricWith(labels prometheus.Labels) (prometheus.Gauge, error) {
return v.GaugeVec.GetMetricWith(labelsWithPlaceholders(labels, v.PossibleLabels))
}
type storage struct {
locker sync.Mutex
registerer prometheus.Registerer
count map[string]*CounterVec
gauge map[string]*GaugeVec
intGauge map[string]*GaugeVec
}
// Metrics implements a wrapper of prometheus metrics to implement
// metrics.Metrics.
//
// Pretty slow and naive implementation. Could be improved by on-need basis.
// If you need a faster implementation, then try `tsmetrics`.
//
// Warning! This implementation does not remove automatically metrics, thus
// if a metric was created once, it will be kept in memory forever.
// If you need a version of metrics which automatically removes metrics
// non-used for a long time, then try `tsmetrics`.
//
// Warning! Prometheus does not support changing amount of labels for a metric,
// therefore we delete and create a metric from scratch if it is required to
// extend the set of labels. This procedure leads to a reset of the metric
// value.
// If you need a version of metrics which does not have such flaw, then
// try `tsmetrics` or `simplemetrics`.
type Metrics struct {
*storage
labels prometheus.Labels
labelNames []string
}
// New returns a new instance of Metrics
func New(registerer prometheus.Registerer) *Metrics {
m := &Metrics{
storage: &storage{
registerer: registerer,
count: map[string]*CounterVec{},
gauge: map[string]*GaugeVec{},
intGauge: map[string]*GaugeVec{},
},
}
return m
}
func (m *Metrics) List() []prometheus.Collector {
m.storage.locker.Lock()
defer m.storage.locker.Unlock()
result := make([]prometheus.Collector, 0, len(m.storage.count)+len(m.storage.gauge)+len(m.storage.intGauge))
for _, count := range m.storage.count {
result = append(result, count.CounterVec)
}
for _, gauge := range m.storage.gauge {
result = append(result, gauge.GaugeVec)
}
for _, intGauge := range m.storage.intGauge {
result = append(result, intGauge.GaugeVec)
}
return result
}
func (m *Metrics) Registerer() prometheus.Registerer {
return m.registerer
}
func (m *Metrics) getOrCreateCountVec(key string, possibleLabelNames []string) *CounterVec {
counterVec := m.count[key]
if counterVec != nil {
return counterVec
}
counterVec = &CounterVec{
CounterVec: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: key + "_count",
}, possibleLabelNames),
Key: key,
PossibleLabels: possibleLabelNames,
}
m.count[key] = counterVec
if m.registerer != nil {
err := m.registerer.Register(counterVec)
if err != nil {
panic(fmt.Sprintf("key: '%v', err: %v", key, err))
}
}
return counterVec
}
func (m *Metrics) deleteCountVec(counterVec *CounterVec) {
if m.registerer != nil {
if !unregister(m.registerer, counterVec.CounterVec) {
panic(counterVec)
}
}
delete(m.count, counterVec.Key)
}
// Count implements context.Metrics (see the description in the interface).
func (m *Metrics) Count(key string) metrics.Count {
m.storage.locker.Lock()
defer m.storage.locker.Unlock()
counterVec := m.getOrCreateCountVec(key, m.labelNames)
counter, err := counterVec.GetMetricWith(m.labels)
if err != nil {
m.deleteCountVec(counterVec)
counterVec.AddPossibleLabels(m.labelNames)
counterVec = m.getOrCreateCountVec(key, counterVec.PossibleLabels)
counter, err = counterVec.GetMetricWith(m.labels)
if err != nil {
panic(err)
}
}
return &Count{Metrics: m, Key: key, CounterVec: counterVec, Counter: counter}
}
func (m *Metrics) getOrCreateGaugeVec(key string, possibleLabelNames []string) *GaugeVec {
gaugeVec := m.gauge[key]
if gaugeVec != nil {
return gaugeVec
}
_gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: key + "_float",
}, possibleLabelNames)
gaugeVec = &GaugeVec{
GaugeVec: _gaugeVec,
Key: key,
PossibleLabels: possibleLabelNames,
}
m.gauge[key] = gaugeVec
if m.registerer != nil {
err := m.registerer.Register(gaugeVec)
if err != nil {
panic(fmt.Sprintf("key: '%v', err: %v", key, err))
}
}
return gaugeVec
}
func (m *Metrics) deleteGaugeVec(gaugeVec *GaugeVec) {
if m.registerer != nil {
if !unregister(m.registerer, gaugeVec.GaugeVec) {
panic(gaugeVec)
}
}
delete(m.gauge, gaugeVec.Key)
}
// Gauge implements context.Metrics (see the description in the interface).
func (m *Metrics) Gauge(key string) metrics.Gauge {
m.storage.locker.Lock()
defer m.storage.locker.Unlock()
gaugeVec := m.getOrCreateGaugeVec(key, m.labelNames)
gauge, err := gaugeVec.GetMetricWith(m.labels)
if err != nil {
m.deleteGaugeVec(gaugeVec)
gaugeVec.AddPossibleLabels(m.labelNames)
gaugeVec = m.getOrCreateGaugeVec(key, gaugeVec.PossibleLabels)
gauge, err = gaugeVec.GetMetricWith(m.labels)
if err != nil {
panic(err)
}
}
return &Gauge{Metrics: m, Key: key, GaugeVec: gaugeVec, Gauge: gauge}
}
func (m *Metrics) getOrCreateIntGaugeVec(key string, possibleLabelNames []string) *GaugeVec {
gaugeVec := m.intGauge[key]
if gaugeVec != nil {
return gaugeVec
}
_gaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: key + "_int",
}, possibleLabelNames)
gaugeVec = &GaugeVec{
GaugeVec: _gaugeVec,
Key: key,
PossibleLabels: possibleLabelNames,
}
m.intGauge[key] = gaugeVec
if m.registerer != nil {
err := m.registerer.Register(gaugeVec)
if err != nil {
panic(fmt.Sprintf("key: '%v', err: %v", key, err))
}
}
return gaugeVec
}
func (m *Metrics) deleteIntGaugeVec(intGaugeVec *GaugeVec) {
if m.registerer != nil {
if !unregister(m.registerer, intGaugeVec) {
panic(intGaugeVec)
}
}
delete(m.intGauge, intGaugeVec.Key)
}
// IntGauge implements context.Metrics (see the description in the interface).
func (m *Metrics) IntGauge(key string) metrics.IntGauge {
m.storage.locker.Lock()
defer m.storage.locker.Unlock()
gaugeVec := m.getOrCreateIntGaugeVec(key, m.labelNames)
gauge, err := gaugeVec.GetMetricWith(m.labels)
if err != nil {
m.deleteIntGaugeVec(gaugeVec)
gaugeVec.AddPossibleLabels(m.labelNames)
gaugeVec = m.getOrCreateIntGaugeVec(key, gaugeVec.PossibleLabels)
gauge, err = gaugeVec.GetMetricWith(m.labels)
if err != nil {
panic(err)
}
}
return &IntGauge{Metrics: m, Key: key, GaugeVec: gaugeVec, Gauge: gauge}
}
// WithTag implements context.Metrics (see the description in the interface).
func (m *Metrics) WithTag(key string, value interface{}) metrics.Metrics {
result := &Metrics{
storage: m.storage,
}
result.labels = make(prometheus.Labels, len(m.labels))
for k, v := range m.labels {
result.labels[k] = v
}
result.labels[key] = TagValueToString(value)
labelNames := make([]string, 0, len(result.labels))
for k := range result.labels {
labelNames = append(labelNames, k)
}
sort.Strings(labelNames)
result.labelNames = labelNames
return result
}
// WithTags implements context.Metrics (see the description in the interface).
func (m *Metrics) WithTags(tags Fields) metrics.Metrics {
result := &Metrics{
storage: m.storage,
}
if tags == nil {
return result
}
result.labels = make(prometheus.Labels, len(m.labels)+len(tags))
for k, v := range m.labels {
result.labels[k] = v
}
for k, v := range tags {
result.labels[k] = TagValueToString(v)
}
labelNames := make([]string, 0, len(result.labels))
for k := range result.labels {
labelNames = append(labelNames, k)
}
sort.Strings(labelNames)
result.labelNames = labelNames
return result
}
func tagsToLabels(tags Fields) prometheus.Labels {
labels := make(prometheus.Labels, len(tags))
for k, v := range tags {
labels[k] = TagValueToString(v)
}
return labels
}
const prebakeMax = 65536
var prebackedString [prebakeMax * 2]string
func init() {
for i := -prebakeMax; i < prebakeMax; i++ {
prebackedString[i+prebakeMax] = strconv.FormatInt(int64(i), 10)
}
}
func getPrebakedString(v int32) string {
if v >= prebakeMax || -v <= -prebakeMax {
return ""
}
return prebackedString[v+prebakeMax]
}
// TagValueToString converts any value to a string, which could be used
// as label value in prometheus.
func TagValueToString(vI interface{}) string {
switch v := vI.(type) {
case int:
r := getPrebakedString(int32(v))
if len(r) != 0 {
return r
}
return strconv.FormatInt(int64(v), 10)
case uint64:
r := getPrebakedString(int32(v))
if len(r) != 0 {
return r
}
return strconv.FormatUint(v, 10)
case int64:
r := getPrebakedString(int32(v))
if len(r) != 0 {
return r
}
return strconv.FormatInt(v, 10)
case string:
return strings.Replace(v, ",", "_", -1)
case bool:
switch v {
case true:
return "true"
case false:
return "false"
}
case []byte:
return string(v)
case nil:
return "null"
case interface{ String() string }:
return strings.Replace(v.String(), ",", "_", -1)
}
return "<unknown_type>"
}