go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/statsd-to-tsmon/statsd.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  package main
    16  
    17  import (
    18  	"go.chromium.org/luci/common/errors"
    19  )
    20  
    21  var (
    22  	// ErrMalformedStatsdLine is returned by ParseStatsdMetric if it receives
    23  	// an input that doesn't look like a statsd metric line.
    24  	ErrMalformedStatsdLine = errors.New("statsd line has wrong format")
    25  
    26  	// ErrUnsupportedType is returned by ParseStatsdMetric if the statsd metric
    27  	// has a type we don't support.
    28  	ErrUnsupportedType = errors.New("unsupported metric type")
    29  )
    30  
    31  // StatsdMetricType enumerates supports statsd metric types.
    32  type StatsdMetricType int
    33  
    34  const (
    35  	StatsdMetricUnknown StatsdMetricType = 0
    36  	StatsdMetricGauge   StatsdMetricType = 1 // 'g'
    37  	StatsdMetricCounter StatsdMetricType = 2 // 'c'
    38  	StatsdMetricTimer   StatsdMetricType = 3 // 'ms'
    39  )
    40  
    41  // StatsdMetric holds some parsed statsd metric.
    42  //
    43  // It retains pointers to the buffer it was parsed from.
    44  //
    45  // A metric line "a.b.c:1234|c" is represented by
    46  //
    47  //	m := StatsdMetric{
    48  //	  Name: [][]byte{
    49  //	    {'a'},
    50  //	    {'b'},
    51  //	    {'c'},
    52  //	  },
    53  //	  Type: StatsdMetricCounter,
    54  //	  Value: []byte("1234"),
    55  //	}
    56  type StatsdMetric struct {
    57  	Name  [][]byte
    58  	Type  StatsdMetricType
    59  	Value []byte
    60  }
    61  
    62  // ParseStatsdMetric parses one statsd metric line from the buffer.
    63  //
    64  // Returns the number of bytes read and the parsed metric. The parsed metric
    65  // retains pointers to the `buf`. Make sure `buf` is not modified as long as
    66  // there are StatsdMetric that point to it.
    67  func ParseStatsdMetric(buf []byte, metric *StatsdMetric) (read int, err error) {
    68  	// Reset the state (but retain Name buffer).
    69  	metric.Name = metric.Name[:0]
    70  	metric.Type = StatsdMetricUnknown
    71  	metric.Value = nil
    72  
    73  	// Buf contains one or more lines like:
    74  	//
    75  	// xxx.yyy.zzz:12345|c
    76  	// xxx.yyy.zzz:12345|c|<ignore>
    77  	// xxx.yyy.zzz:12345|c#ignore
    78  	//
    79  	// We parse only the first line using a simple state machine. That way we can
    80  	// avoid unnecessary memory allocations on a hot code path.
    81  	const (
    82  		S_NAME        = iota // in 'xxx'
    83  		S_VALUE              // in '1234'
    84  		S_TYPE               // in 'c'
    85  		S_SKIP               // waiting for \n or EOF
    86  		S_SKIP_BROKEN        // waiting for \n or EOF
    87  	)
    88  	state := S_NAME
    89  
    90  	nameIdx := 0       // index where the name component started
    91  	valueIdx := 0      // index where the value portion started
    92  	typIdx := 0        // index where the type portion started
    93  	typ := []byte(nil) // the type portion
    94  
    95  	idx := 0       // the current scanning pointer
    96  	chr := byte(0) // the current character
    97  
    98  	for idx, chr = range buf {
    99  		switch {
   100  		case state == S_NAME && (chr == '.' || chr == ':'):
   101  			// This happens when parsing e.g. "abc..def".
   102  			if nameIdx == idx {
   103  				state = S_SKIP_BROKEN
   104  				continue
   105  			}
   106  			metric.Name = append(metric.Name, buf[nameIdx:idx])
   107  			if chr == '.' {
   108  				nameIdx = idx + 1
   109  				state = S_NAME // keep reading the name
   110  			} else { // ':'
   111  				valueIdx = idx + 1
   112  				state = S_VALUE // reading the value now
   113  			}
   114  
   115  		case state == S_VALUE && chr == '|':
   116  			metric.Value = buf[valueIdx:idx]
   117  			typIdx = idx + 1
   118  			state = S_TYPE
   119  
   120  		case state == S_TYPE && (chr == '|' || chr == '#' || chr == '\n'):
   121  			typ = buf[typIdx:idx]
   122  			state = S_SKIP
   123  		}
   124  
   125  		if chr == '\n' {
   126  			break
   127  		}
   128  	}
   129  	read = idx + 1
   130  
   131  	// S_TYPE transitions into S_SKIP on EOF/EOL.
   132  	if state == S_TYPE {
   133  		typ = buf[typIdx : idx+1]
   134  		state = S_SKIP
   135  	}
   136  
   137  	// S_SKIP is the only valid state on EOF/EOL.
   138  	if state != S_SKIP {
   139  		err = ErrMalformedStatsdLine
   140  	} else {
   141  		// Parse `typ`.
   142  		switch {
   143  		case len(typ) == 1 && typ[0] == 'g':
   144  			metric.Type = StatsdMetricGauge
   145  		case len(typ) == 1 && typ[0] == 'c':
   146  			metric.Type = StatsdMetricCounter
   147  		case len(typ) == 2 && typ[0] == 'm' && typ[1] == 's':
   148  			metric.Type = StatsdMetricTimer
   149  		default:
   150  			err = ErrUnsupportedType
   151  		}
   152  	}
   153  
   154  	return
   155  }