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  }