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 }