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 }