github.com/anycable/anycable-go@v1.5.1/metrics/metrics.go (about) 1 package metrics 2 3 import ( 4 "context" 5 "fmt" 6 "log/slog" 7 "net/http" 8 "strconv" 9 "sync" 10 "time" 11 12 "github.com/anycable/anycable-go/server" 13 ) 14 15 const DefaultRotateInterval = 15 16 17 // IntervalHandler describe a periodical metrics writer interface 18 type IntervalWriter interface { 19 Run(interval int) error 20 Stop() 21 Write(m *Metrics) error 22 } 23 24 //go:generate mockery --name Instrumenter --output "../mocks" --outpkg mocks 25 type Instrumenter interface { 26 CounterIncrement(name string) 27 CounterAdd(name string, val uint64) 28 GaugeIncrement(name string) 29 GaugeDecrement(name string) 30 GaugeSet(name string, val uint64) 31 RegisterCounter(name string, desc string) 32 RegisterGauge(name string, desc string) 33 } 34 35 // Metrics stores some useful stats about node 36 type Metrics struct { 37 mu sync.RWMutex 38 writers []IntervalWriter 39 server *server.HTTPServer 40 httpPath string 41 rotateInterval time.Duration 42 tags map[string]string 43 counters map[string]*Counter 44 gauges map[string]*Gauge 45 shutdownCh chan struct{} 46 closed bool 47 log *slog.Logger 48 } 49 50 var _ Instrumenter = (*Metrics)(nil) 51 52 // NewFromConfig creates a new metrics instance from the prodived configuration 53 func NewFromConfig(config *Config, l *slog.Logger) (*Metrics, error) { 54 var metricsPrinter IntervalWriter 55 56 writers := []IntervalWriter{} 57 58 if config.LogEnabled() { 59 if config.LogFormatterEnabled() { 60 customPrinter, err := NewCustomPrinter(config.LogFormatter, l) 61 62 if err == nil { 63 metricsPrinter = customPrinter 64 } else { 65 return nil, err 66 } 67 } else { 68 metricsPrinter = NewBasePrinter(config.LogFilter, l) 69 } 70 71 writers = append(writers, metricsPrinter) 72 } 73 74 instance := NewMetrics(writers, config.RotateInterval, l) 75 76 if config.Tags != nil { 77 instance.tags = config.Tags 78 } 79 80 if config.HTTPEnabled() { 81 if config.Host != "" && config.Host != server.Host { 82 srv, err := server.NewServer(config.Host, strconv.Itoa(config.Port), server.SSL, 0) 83 if err != nil { 84 return nil, err 85 } 86 instance.server = srv 87 } else { 88 srv, err := server.ForPort(strconv.Itoa(config.Port)) 89 if err != nil { 90 return nil, err 91 } 92 instance.server = srv 93 } 94 95 instance.httpPath = config.HTTP 96 instance.server.SetupHandler(instance.httpPath, http.HandlerFunc(instance.PrometheusHandler)) 97 } 98 99 return instance, nil 100 } 101 102 // NewMetrics build new metrics struct 103 func NewMetrics(writers []IntervalWriter, rotateIntervalSeconds int, l *slog.Logger) *Metrics { 104 rotateInterval := time.Duration(rotateIntervalSeconds) * time.Second 105 106 return &Metrics{ 107 writers: writers, 108 rotateInterval: rotateInterval, 109 counters: make(map[string]*Counter), 110 gauges: make(map[string]*Gauge), 111 shutdownCh: make(chan struct{}), 112 log: l.With("context", "metrics"), 113 } 114 } 115 116 func (m *Metrics) DefaultTags(tags map[string]string) { 117 m.tags = tags 118 } 119 120 func (m *Metrics) RegisterWriter(w IntervalWriter) { 121 m.writers = append(m.writers, w) 122 } 123 124 // Run periodically updates counters delta (and logs metrics if necessary) 125 func (m *Metrics) Run() error { 126 if m.server != nil { 127 m.log.Info(fmt.Sprintf("Serve metrics at %s%s", m.server.Address(), m.httpPath)) 128 129 if err := m.server.StartAndAnnounce("Metrics server"); err != nil { 130 if !m.server.Stopped() { 131 return fmt.Errorf("metrics HTTP server at %s stopped: %v", m.server.Address(), err) 132 } 133 } 134 } 135 136 if len(m.writers) == 0 { 137 m.log.Debug("no metrics writers, disabling metrics rotation") 138 return nil 139 } 140 141 if m.rotateInterval == 0 { 142 m.rotateInterval = DefaultRotateInterval * time.Second 143 } 144 145 for _, writer := range m.writers { 146 if err := writer.Run(int(m.rotateInterval.Seconds())); err != nil { 147 return err 148 } 149 } 150 151 for { 152 select { 153 case <-m.shutdownCh: 154 return nil 155 case <-time.After(m.rotateInterval): 156 m.log.Debug("rotate metrics", "interval", m.rotateInterval) 157 m.rotate() 158 159 for _, writer := range m.writers { 160 if err := writer.Write(m); err != nil { 161 m.log.Error("metrics writer failed to write", "error", err) 162 } 163 } 164 } 165 } 166 } 167 168 // Shutdown stops metrics updates 169 func (m *Metrics) Shutdown(ctx context.Context) (err error) { 170 m.mu.Lock() 171 defer m.mu.Unlock() 172 173 if m.closed { 174 return 175 } 176 177 m.closed = true 178 179 close(m.shutdownCh) 180 181 if m.server != nil { 182 m.server.Shutdown(ctx) //nolint:errcheck 183 } 184 185 for _, writer := range m.writers { 186 writer.Stop() 187 } 188 189 return 190 } 191 192 // RegisterCounter adds new counter to the registry 193 func (m *Metrics) RegisterCounter(name string, desc string) { 194 m.mu.Lock() 195 defer m.mu.Unlock() 196 197 m.counters[name] = NewCounter(name, desc) 198 } 199 200 // RegisterGauge adds new gauge to the registry 201 func (m *Metrics) RegisterGauge(name string, desc string) { 202 m.mu.Lock() 203 defer m.mu.Unlock() 204 205 m.gauges[name] = NewGauge(name, desc) 206 } 207 208 // GaugeIncrement increments the given gauge 209 func (m *Metrics) GaugeIncrement(name string) { 210 m.gauges[name].Inc() 211 } 212 213 // GaugeDecrement increments the given gauge 214 func (m *Metrics) GaugeDecrement(name string) { 215 m.gauges[name].Dec() 216 } 217 218 // GaugeSet sets the given gauge 219 func (m *Metrics) GaugeSet(name string, val uint64) { 220 m.gauges[name].Set64(val) 221 } 222 223 // Counter returns counter by name 224 func (m *Metrics) Counter(name string) *Counter { 225 return m.counters[name] 226 } 227 228 // CounterIncrement increments the given counter 229 func (m *Metrics) CounterIncrement(name string) { 230 m.counters[name].Inc() 231 } 232 233 // CounterAdd adds a value to the given counter 234 func (m *Metrics) CounterAdd(name string, val uint64) { 235 m.counters[name].Add(val) 236 } 237 238 // EachCounter applies function f(*Gauge) to each gauge in a set 239 func (m *Metrics) EachCounter(f func(c *Counter)) { 240 m.mu.RLock() 241 defer m.mu.RUnlock() 242 243 for _, counter := range m.counters { 244 f(counter) 245 } 246 } 247 248 // Gauge returns gauge by name 249 func (m *Metrics) Gauge(name string) *Gauge { 250 return m.gauges[name] 251 } 252 253 // EachGauge applies function f(*Gauge) to each gauge in a set 254 func (m *Metrics) EachGauge(f func(g *Gauge)) { 255 m.mu.RLock() 256 defer m.mu.RUnlock() 257 258 for _, gauge := range m.gauges { 259 f(gauge) 260 } 261 } 262 263 // IntervalSnapshot returns recorded interval metrics snapshot 264 func (m *Metrics) IntervalSnapshot() map[string]uint64 { 265 m.mu.RLock() 266 defer m.mu.RUnlock() 267 268 snapshot := make(map[string]uint64) 269 270 for name, c := range m.counters { 271 snapshot[name] = c.IntervalValue() 272 } 273 274 for name, g := range m.gauges { 275 snapshot[name] = g.Value() 276 } 277 278 return snapshot 279 } 280 281 func (m *Metrics) rotate() { 282 m.mu.Lock() 283 defer m.mu.Unlock() 284 285 for _, c := range m.counters { 286 c.UpdateDelta() 287 } 288 }