go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/statsd-to-tsmon/main.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Executable statsd-to-tsmon implements a statsd sink that sends aggregated 16 // metrics to tsmon. 17 // 18 // Supports only integer counters, gauges and timers. Timers are converted into 19 // tsmon histograms. 20 // 21 // See https://github.com/b/statsd_spec for a reference. 22 package main 23 24 import ( 25 "context" 26 "flag" 27 "fmt" 28 "net" 29 "time" 30 31 "go.chromium.org/luci/common/clock" 32 "go.chromium.org/luci/common/errors" 33 "go.chromium.org/luci/common/logging" 34 "go.chromium.org/luci/common/tsmon/field" 35 "go.chromium.org/luci/common/tsmon/metric" 36 37 "go.chromium.org/luci/server" 38 ) 39 40 var ( 41 statsdMetricsProcessed = metric.NewCounter( 42 "luci/statsd/metrics_processed", 43 "How many statsd metrics were processed (per outcome).", 44 nil, 45 field.String("outcome"), // see processStatsdPacket 46 ) 47 ) 48 49 func main() { 50 statsdPort := flag.Int( 51 "statsd-port", 52 8125, 53 "Localhost UDP port to bind to.") 54 configFile := flag.String( 55 "statsd-to-tsmon-config", 56 "/etc/statsd-to-tsmon/config.cfg", 57 "Path to the config file.") 58 59 opts := &server.Options{ 60 HTTPAddr: "-", // not serving any HTTP routes 61 } 62 63 server.Main(opts, nil, func(srv *server.Server) error { 64 if *configFile == "" { 65 return errors.New("-statsd-to-tsmon-config is required") 66 } 67 cfg, err := LoadConfig(*configFile) 68 if err != nil { 69 return errors.Annotate(err, "failed to load the config file").Err() 70 } 71 72 // Statsd metrics are sent to an UDP port. 73 pc, err := net.ListenPacket("udp", fmt.Sprintf("localhost:%d", *statsdPort)) 74 if err != nil { 75 return errors.Annotate(err, "failed to bind the UDP socket").Err() 76 } 77 78 // Spin in a loop, reading and processing incoming UDP packets. 79 srv.RunInBackground("statsd", func(ctx context.Context) { mainLoop(ctx, pc, cfg, nil) }) 80 return nil 81 }) 82 } 83 84 func mainLoop(ctx context.Context, pc net.PacketConn, cfg *Config, tick chan struct{}) { 85 go func() { 86 <-ctx.Done() 87 pc.Close() 88 }() 89 90 // Buffer to store incoming UDP packets in. 91 buf := make([]byte, 32*1024) 92 // Buffer to store parsed statds metrics. 93 m := StatsdMetric{Name: make([][]byte, 0, 8)} 94 // Number of consecutive UDP receive errors. 95 errs := 0 96 97 for ctx.Err() == nil { 98 n, _, err := pc.ReadFrom(buf) 99 if err != nil { 100 if ctx.Err() != nil { 101 break 102 } 103 errs += 1 104 if errs > 1 { 105 logging.Errorf(ctx, "%d consecutive errors in ReadFrom: %s\n", errs, err) 106 clock.Sleep(ctx, time.Second) // do not spinlock on persistent errors 107 } else { 108 logging.Errorf(ctx, "Error in ReadFrom: %s\n", err) 109 } 110 } else { 111 errs = 0 112 processStatsdPacket(ctx, cfg, buf[:n], &m) 113 } 114 115 // Used for synchronization in tests. 116 if tick != nil { 117 select { 118 case tick <- struct{}{}: 119 case <-ctx.Done(): 120 } 121 } 122 } 123 } 124 125 func processStatsdPacket(ctx context.Context, cfg *Config, buf []byte, m *StatsdMetric) { 126 // Counters to flush to statsdMetricsProcessed. 127 var ( 128 countOK int64 129 countMalformed int64 130 countUnsupported int64 131 countUnexpected int64 132 countSkipped int64 133 countUnknown int64 134 ) 135 136 for len(buf) != 0 { 137 read, err := ParseStatsdMetric(buf, m) 138 if err == nil { 139 err = ConvertMetric(ctx, cfg, m) 140 } 141 switch err { 142 case nil: 143 countOK++ 144 case ErrMalformedStatsdLine: 145 logging.Warningf(ctx, "Bad statsd line: %q", string(buf[:read])) 146 countMalformed++ 147 case ErrUnsupportedType: 148 logging.Warningf(ctx, "Unsupported metric type: %q", string(buf[:read])) 149 countUnsupported++ 150 case ErrUnexpectedType: 151 logging.Warningf(ctx, "Unexpected metric type: %q", string(buf[:read])) 152 countUnexpected++ 153 case ErrSkipped: // this is expected, do not log 154 countSkipped++ 155 default: 156 logging.Warningf(ctx, "Error when processing %q: %s", string(buf[:read]), err) 157 countUnknown++ 158 } 159 buf = buf[read:] 160 } 161 162 if countOK != 0 { 163 statsdMetricsProcessed.Add(ctx, countOK, "OK") 164 } 165 if countMalformed != 0 { 166 statsdMetricsProcessed.Add(ctx, countMalformed, "MALFORMED") 167 } 168 if countUnsupported != 0 { 169 statsdMetricsProcessed.Add(ctx, countUnsupported, "UNSUPPORTED") 170 } 171 if countUnexpected != 0 { 172 statsdMetricsProcessed.Add(ctx, countUnexpected, "UNEXPECTED") 173 } 174 if countSkipped != 0 { 175 statsdMetricsProcessed.Add(ctx, countSkipped, "SKIPPED") 176 } 177 if countUnknown != 0 { 178 statsdMetricsProcessed.Add(ctx, countUnknown, "UNKNOWN") 179 } 180 }