github.com/anycable/anycable-go@v1.5.1/metrics/statsd_writer.go (about)

     1  package metrics
     2  
     3  import (
     4  	"fmt"
     5  	"log/slog"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/smira/go-statsd"
    10  )
    11  
    12  type StatsdConfig struct {
    13  	Host          string
    14  	Prefix        string
    15  	TagFormat     string
    16  	MaxPacketSize int
    17  }
    18  
    19  type StatsdLogger struct {
    20  	log *slog.Logger
    21  }
    22  
    23  func (lg *StatsdLogger) Printf(msg string, args ...interface{}) {
    24  	msg = strings.TrimPrefix(msg, "[STATSD] ")
    25  	// Statsd only prints errors and warnings
    26  	if strings.Contains(msg, "Error") {
    27  		lg.log.Error(fmt.Sprintf(msg, args...))
    28  	} else {
    29  		lg.log.Warn(fmt.Sprintf(msg, args...))
    30  	}
    31  }
    32  
    33  func NewStatsdConfig() StatsdConfig {
    34  	return StatsdConfig{Prefix: "anycable_go.", MaxPacketSize: 1400, TagFormat: "datadog"}
    35  }
    36  
    37  func (c StatsdConfig) Enabled() bool {
    38  	return c.Host != ""
    39  }
    40  
    41  type StatsdWriter struct {
    42  	client *statsd.Client
    43  	config StatsdConfig
    44  	tags   map[string]string
    45  
    46  	log *slog.Logger
    47  	mu  sync.Mutex
    48  }
    49  
    50  var _ IntervalWriter = (*StatsdWriter)(nil)
    51  
    52  func NewStatsdWriter(c StatsdConfig, tags map[string]string, l *slog.Logger) *StatsdWriter {
    53  	return &StatsdWriter{config: c, tags: tags, log: l}
    54  }
    55  
    56  func (sw *StatsdWriter) Run(interval int) error {
    57  	sl := StatsdLogger{sw.log.With("service", "statsd")}
    58  	opts := []statsd.Option{
    59  		statsd.MaxPacketSize(sw.config.MaxPacketSize),
    60  		statsd.MetricPrefix(sw.config.Prefix),
    61  		statsd.Logger(&sl),
    62  	}
    63  
    64  	var tagsInfo string
    65  
    66  	if sw.tags != nil {
    67  		tagsStyle, err := resolveTagsStyle(sw.config.TagFormat)
    68  		if err != nil {
    69  			return err
    70  		}
    71  
    72  		tags := convertTags(sw.tags)
    73  		opts = append(opts,
    74  			statsd.TagStyle(tagsStyle),
    75  			statsd.DefaultTags(tags...),
    76  		)
    77  
    78  		tagsInfo = fmt.Sprintf(", tags=%v, style=%s", sw.tags, sw.config.TagFormat)
    79  	}
    80  
    81  	sw.client = statsd.NewClient(
    82  		sw.config.Host,
    83  		opts...,
    84  	)
    85  
    86  	sw.log.Info(
    87  		fmt.Sprintf(
    88  			"Send statsd metrics to %s with every %vs (prefix=%s%s)",
    89  			sw.config.Host, interval, sw.config.Prefix, tagsInfo,
    90  		),
    91  	)
    92  
    93  	return nil
    94  }
    95  
    96  func (sw *StatsdWriter) Stop() {
    97  	sw.mu.Lock()
    98  	defer sw.mu.Unlock()
    99  
   100  	sw.client.Close()
   101  	sw.client = nil
   102  }
   103  
   104  func (sw *StatsdWriter) Write(m *Metrics) error {
   105  	sw.mu.Lock()
   106  	defer sw.mu.Unlock()
   107  
   108  	if sw.client == nil {
   109  		return nil
   110  	}
   111  
   112  	m.EachCounter(func(counter *Counter) {
   113  		sw.client.Incr(counter.Name(), int64(counter.IntervalValue()))
   114  	})
   115  
   116  	m.EachGauge(func(gauge *Gauge) {
   117  		sw.client.Gauge(gauge.Name(), int64(gauge.Value()))
   118  	})
   119  
   120  	return nil
   121  }
   122  
   123  func resolveTagsStyle(name string) (*statsd.TagFormat, error) {
   124  	switch name {
   125  	case "datadog":
   126  		return statsd.TagFormatDatadog, nil
   127  	case "influxdb":
   128  		return statsd.TagFormatInfluxDB, nil
   129  	case "graphite":
   130  		return statsd.TagFormatGraphite, nil
   131  	}
   132  
   133  	return nil, fmt.Errorf("unknown StatsD tags format: %s", name)
   134  }
   135  
   136  func convertTags(tags map[string]string) []statsd.Tag {
   137  	buf := make([]statsd.Tag, len(tags))
   138  	i := 0
   139  
   140  	for k, v := range tags {
   141  		buf[i] = statsd.StringTag(k, v)
   142  		i++
   143  	}
   144  
   145  	return buf
   146  }