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  }