github.com/msales/pkg/v3@v3.24.0/stats/prometheus.go (about)

     1  package stats
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/prometheus/client_golang/prometheus"
    12  	"github.com/prometheus/client_golang/prometheus/promhttp"
    13  )
    14  
    15  // Prometheus represents a promethus stats collector.
    16  type Prometheus struct {
    17  	prefix string
    18  
    19  	reg      *prometheus.Registry
    20  	counters map[string]*prometheus.CounterVec
    21  	gauges   map[string]*prometheus.GaugeVec
    22  	timings  map[string]*prometheus.SummaryVec
    23  }
    24  
    25  // NewPrometheus creates a new Prometheus stats instance.
    26  func NewPrometheus(prefix string) *Prometheus {
    27  	return &Prometheus{
    28  		prefix:   prefix,
    29  		reg:      prometheus.NewRegistry(),
    30  		counters: map[string]*prometheus.CounterVec{},
    31  		gauges:   map[string]*prometheus.GaugeVec{},
    32  		timings:  map[string]*prometheus.SummaryVec{},
    33  	}
    34  }
    35  
    36  // Handler gets the prometheus HTTP handler.
    37  func (s *Prometheus) Handler() http.Handler {
    38  	return promhttp.HandlerFor(s.reg, promhttp.HandlerOpts{})
    39  }
    40  
    41  // Inc increments a count by the value.
    42  func (s *Prometheus) Inc(name string, value int64, rate float32, tags ...interface{}) error {
    43  	lblNames, lbls := formatPrometheusTags(tags)
    44  
    45  	key := s.createKey(name, lblNames)
    46  	m, ok := s.counters[key]
    47  	if !ok {
    48  		m = prometheus.NewCounterVec(
    49  			prometheus.CounterOpts{
    50  				Namespace: s.formatFQN(s.prefix),
    51  				Name:      s.formatFQN(name),
    52  				Help:      name,
    53  			},
    54  			lblNames,
    55  		)
    56  
    57  		err := s.reg.Register(m)
    58  		if err == nil {
    59  			s.counters[key] = m
    60  		} else {
    61  			existsErr, ok := err.(prometheus.AlreadyRegisteredError)
    62  			if !ok {
    63  				return err
    64  			}
    65  
    66  			m, ok = existsErr.ExistingCollector.(*prometheus.CounterVec)
    67  			if !ok {
    68  				return fmt.Errorf("stats: expected the collector to be instance of *CounterVec, got %T instead", existsErr.ExistingCollector)
    69  			}
    70  		}
    71  	}
    72  
    73  	m.With(lbls).Add(float64(value))
    74  
    75  	return nil
    76  }
    77  
    78  // Dec decrements a count by the value.
    79  func (s *Prometheus) Dec(name string, value int64, rate float32, tags ...interface{}) error {
    80  	return errors.New("prometheus: decrement not supported")
    81  }
    82  
    83  // Gauge measures the value of a metric.
    84  func (s *Prometheus) Gauge(name string, value float64, rate float32, tags ...interface{}) error {
    85  	lblNames, lbls := formatPrometheusTags(tags)
    86  
    87  	key := s.createKey(name, lblNames)
    88  	m, ok := s.gauges[key]
    89  	if !ok {
    90  		m = prometheus.NewGaugeVec(
    91  			prometheus.GaugeOpts{
    92  				Namespace: s.formatFQN(s.prefix),
    93  				Name:      s.formatFQN(name),
    94  				Help:      name,
    95  			},
    96  			lblNames,
    97  		)
    98  
    99  		err := s.reg.Register(m)
   100  		if err == nil {
   101  			s.gauges[key] = m
   102  		} else {
   103  			existsErr, ok := err.(prometheus.AlreadyRegisteredError)
   104  			if !ok {
   105  				return err
   106  			}
   107  
   108  			m, ok = existsErr.ExistingCollector.(*prometheus.GaugeVec)
   109  			if !ok {
   110  				return fmt.Errorf("stats: expected the collector to be instance of *GaugeVec, got %T instead", existsErr.ExistingCollector)
   111  			}
   112  		}
   113  	}
   114  
   115  	m.With(lbls).Set(value)
   116  
   117  	return nil
   118  }
   119  
   120  // Timing sends the value of a Duration.
   121  func (s *Prometheus) Timing(name string, value time.Duration, rate float32, tags ...interface{}) error {
   122  	lblNames, lbls := formatPrometheusTags(tags)
   123  
   124  	key := s.createKey(name, lblNames)
   125  	m, ok := s.timings[key]
   126  	if !ok {
   127  		m = prometheus.NewSummaryVec(
   128  			prometheus.SummaryOpts{
   129  				Namespace:  s.formatFQN(s.prefix),
   130  				Name:       s.formatFQN(name),
   131  				Help:       name,
   132  				Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
   133  			},
   134  			lblNames,
   135  		)
   136  
   137  		err := s.reg.Register(m)
   138  		if err == nil {
   139  			s.timings[key] = m
   140  		} else {
   141  			existsErr, ok := err.(prometheus.AlreadyRegisteredError)
   142  			if !ok {
   143  				return err
   144  			}
   145  
   146  			m, ok = existsErr.ExistingCollector.(*prometheus.SummaryVec)
   147  			if !ok {
   148  				return fmt.Errorf("stats: expected the collector to be instance of *SummaryVec, got %T instead", existsErr.ExistingCollector)
   149  			}
   150  		}
   151  	}
   152  
   153  	m.With(lbls).Observe(float64(value) / float64(time.Millisecond))
   154  
   155  	return nil
   156  }
   157  
   158  // Close closes the client and flushes buffered stats, if applicable
   159  func (s *Prometheus) Close() error {
   160  	return nil
   161  }
   162  
   163  // createKey creates a unique metric key.
   164  func (s *Prometheus) createKey(name string, lblNames []string) string {
   165  	return name + strings.Join(lblNames, ":")
   166  }
   167  
   168  // formatFQN formats FQN strings.
   169  func (s *Prometheus) formatFQN(name string) string {
   170  	r := strings.NewReplacer(".", "_", "-", "_")
   171  
   172  	return r.Replace(name)
   173  }
   174  
   175  // formatPrometheusTags create a prometheus Label map from tags.
   176  func formatPrometheusTags(tags []interface{}) ([]string, prometheus.Labels) {
   177  	tags = deduplicateTags(normalizeTags(tags))
   178  
   179  	b := make([]byte, 0, 65) // The largest needed buffer is 65 bytes for a signed int64.
   180  
   181  	names := make([]string, 0, len(tags)/2)
   182  	lbls := make(prometheus.Labels, len(tags)/2)
   183  	for i := 0; i < len(tags); i += 2 {
   184  		key, ok := tags[i].(string) // The stats key must be a string.
   185  		if !ok {
   186  			key = fmt.Sprintf("STATS_ERROR: key %v is not a string", tags[i])
   187  		}
   188  		names = append(names, key)
   189  
   190  		b, lbl := toString(tags[i+1], b[:0])
   191  		if b != nil {
   192  			lbl = string(b)
   193  		}
   194  		lbls[key] = lbl
   195  	}
   196  
   197  	return names, lbls
   198  }
   199  
   200  // toString converts the given value to a string. It either returns the new string and true
   201  // or fills the passed byte slice and returns an empty string and false. The user needs to check
   202  // the returned boolean and take the string (if true) or get data from the slice.
   203  // This is the optimization: filling the buffer allows to re-use the memory and avoid
   204  // allocations when converting floats. Returning the string directly avoids copying strings.
   205  // The function falls back to fmt.Sprintf to format unknown values.
   206  func toString(v interface{}, b []byte) ([]byte, string) {
   207  	switch vv := v.(type) {
   208  	case string:
   209  		return nil, vv
   210  	case bool:
   211  		return strconv.AppendBool(b, vv), ""
   212  	case float32:
   213  		return strconv.AppendFloat(b, float64(vv), 'f', -1, 64), ""
   214  	case float64:
   215  		return strconv.AppendFloat(b, vv, 'f', -1, 64), ""
   216  	case int:
   217  		return strconv.AppendInt(b, int64(vv), 10), ""
   218  	case int8:
   219  		return strconv.AppendInt(b, int64(vv), 10), ""
   220  	case int16:
   221  		return strconv.AppendInt(b, int64(vv), 10), ""
   222  	case int32:
   223  		return strconv.AppendInt(b, int64(vv), 10), ""
   224  	case int64:
   225  		return strconv.AppendInt(b, vv, 10), ""
   226  	case uint:
   227  		return strconv.AppendUint(b, uint64(vv), 10), ""
   228  	case uint8:
   229  		return strconv.AppendUint(b, uint64(vv), 10), ""
   230  	case uint16:
   231  		return strconv.AppendUint(b, uint64(vv), 10), ""
   232  	case uint32:
   233  		return strconv.AppendUint(b, uint64(vv), 10), ""
   234  	case uint64:
   235  		return strconv.AppendUint(b, vv, 10), ""
   236  	default:
   237  		return nil, fmt.Sprintf("%v", vv)
   238  	}
   239  }