github.com/google/cloudprober@v0.11.3/metrics/payload/payload.go (about)

     1  // Copyright 2017-2020 The Cloudprober 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 payload provides utilities to work with the metrics in payload.
    16  package payload
    17  
    18  import (
    19  	"errors"
    20  	"fmt"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/google/cloudprober/logger"
    26  	"github.com/google/cloudprober/metrics"
    27  	configpb "github.com/google/cloudprober/metrics/payload/proto"
    28  )
    29  
    30  // Parser encapsulates the config for parsing payloads to metrics.
    31  type Parser struct {
    32  	baseEM      *metrics.EventMetrics
    33  	distMetrics map[string]*metrics.Distribution
    34  	aggregate   bool
    35  	l           *logger.Logger
    36  }
    37  
    38  // NewParser returns a new payload parser, based on the config provided.
    39  func NewParser(opts *configpb.OutputMetricsOptions, ptype, probeName string, defaultKind metrics.Kind, l *logger.Logger) (*Parser, error) {
    40  	parser := &Parser{
    41  		aggregate:   opts.GetAggregateInCloudprober(),
    42  		distMetrics: make(map[string]*metrics.Distribution),
    43  		l:           l,
    44  	}
    45  
    46  	// If there are any distribution metrics, build them now itself.
    47  	for name, distMetric := range opts.GetDistMetric() {
    48  		d, err := metrics.NewDistributionFromProto(distMetric)
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  		parser.distMetrics[name] = d
    53  	}
    54  
    55  	em := metrics.NewEventMetrics(time.Now()).
    56  		AddLabel("ptype", ptype).
    57  		AddLabel("probe", probeName)
    58  
    59  	switch opts.GetMetricsKind() {
    60  	case configpb.OutputMetricsOptions_CUMULATIVE:
    61  		em.Kind = metrics.CUMULATIVE
    62  	case configpb.OutputMetricsOptions_GAUGE:
    63  		if opts.GetAggregateInCloudprober() {
    64  			return nil, errors.New("payload.NewParser: invalid config, GAUGE metrics should not have aggregate_in_cloudprober enabled")
    65  		}
    66  		em.Kind = metrics.GAUGE
    67  	case configpb.OutputMetricsOptions_UNDEFINED:
    68  		em.Kind = defaultKind
    69  	}
    70  
    71  	// Labels are specified in the probe config.
    72  	if opts.GetAdditionalLabels() != "" {
    73  		for _, label := range strings.Split(opts.GetAdditionalLabels(), ",") {
    74  			labelKV := strings.Split(label, "=")
    75  			if len(labelKV) != 2 {
    76  				return nil, fmt.Errorf("payload.NewParser: invlaid config, wrong label format: %v", labelKV)
    77  			}
    78  			em.AddLabel(labelKV[0], labelKV[1])
    79  		}
    80  	}
    81  
    82  	parser.baseEM = em
    83  
    84  	return parser, nil
    85  }
    86  
    87  func updateMetricValue(mv metrics.Value, val string) error {
    88  	// If a distribution, process it through processDistValue.
    89  	if mVal, ok := mv.(*metrics.Distribution); ok {
    90  		if err := processDistValue(mVal, val); err != nil {
    91  			return fmt.Errorf("error parsing distribution value (%s): %v", val, err)
    92  		}
    93  		return nil
    94  	}
    95  
    96  	v, err := metrics.ParseValueFromString(val)
    97  	if err != nil {
    98  		return fmt.Errorf("error parsing value (%s): %v", val, err)
    99  	}
   100  
   101  	return mv.Add(v)
   102  }
   103  
   104  func parseLabels(labelStr string) [][2]string {
   105  	var labels [][2]string
   106  	for _, l := range strings.Split(labelStr, ",") {
   107  		parts := strings.SplitN(strings.TrimSpace(l), "=", 2)
   108  		if len(parts) != 2 {
   109  			continue
   110  		}
   111  		key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
   112  		// Unquote val if it is a quoted string. strconv returns an error if string
   113  		// is not quoted at all or is unproperly quoted. We use raw string in that
   114  		// case.
   115  		uval, err := strconv.Unquote(val)
   116  		if err == nil {
   117  			val = uval
   118  		}
   119  		labels = append(labels, [2]string{key, val})
   120  	}
   121  	return labels
   122  }
   123  
   124  func parseLine(line string) (string, string, string, error) {
   125  	ob := strings.Index(line, "{")
   126  	// If "{" was not found or was the last element, assume label-less metric.
   127  
   128  	if ob == -1 || ob == len(line)-1 {
   129  		// Parse line as metric has no labels.
   130  		varKV := strings.SplitN(line, " ", 2)
   131  		if len(varKV) < 2 {
   132  			return "", "", "", fmt.Errorf("wrong var key-value format: %s", line)
   133  		}
   134  		return varKV[0], "", strings.TrimSpace(varKV[1]), nil
   135  	}
   136  
   137  	// Capture metric name and move line-beginning forward.
   138  	metricName := line[:ob]
   139  	line = line[ob+1:]
   140  
   141  	eb := strings.Index(line, "}")
   142  	// If "}" was not found or was the last element, invalid line.
   143  	if eb == -1 || eb == len(line)-1 {
   144  		return "", "", "", fmt.Errorf("invalid line (%s), only opening brace found", line)
   145  	}
   146  
   147  	// Capture label string and move line-beginning forward.
   148  	labelStr := line[:eb]
   149  	line = line[eb+1:]
   150  
   151  	return metricName, labelStr, strings.TrimSpace(line), nil
   152  }
   153  
   154  func (p *Parser) metricValueLabels(line string) (metricName, val string, labels [][2]string) {
   155  	line = strings.TrimSpace(line)
   156  	if len(line) == 0 {
   157  		return
   158  	}
   159  
   160  	metricName, labelStr, value, err := parseLine(line)
   161  	if err != nil {
   162  		p.l.Warningf("Error while parsing line (%s): %v", line, err)
   163  		return
   164  	}
   165  
   166  	if p.aggregate && labelStr != "" {
   167  		p.l.Warning("Payload labels are not supported in aggregate_in_cloudprober mode, bad line: ", line)
   168  		return
   169  	}
   170  
   171  	return metricName, value, parseLabels(labelStr)
   172  }
   173  
   174  func addNewMetric(em *metrics.EventMetrics, metricName, val string) error {
   175  	// New metric name, make sure it's not disallowed.
   176  	switch metricName {
   177  	case "success", "total", "latency":
   178  		return fmt.Errorf("metric name (%s) in the payload conflicts with standard metrics: (success,total,latency), ignoring", metricName)
   179  	}
   180  
   181  	v, err := metrics.ParseValueFromString(val)
   182  	if err != nil {
   183  		return fmt.Errorf("could not parse value (%s) for the new metric name (%s): %v", val, metricName, err)
   184  	}
   185  
   186  	em.AddMetric(metricName, v)
   187  	return nil
   188  }
   189  
   190  // PayloadMetrics parses the given payload and creates one EventMetrics per
   191  // line. Each metric line can have its own labels, e.g. num_rows{db=dbA}.
   192  func (p *Parser) PayloadMetrics(payload, target string) []*metrics.EventMetrics {
   193  	// Timestamp for all EventMetrics generated from this payload.
   194  	payloadTS := time.Now()
   195  	var results []*metrics.EventMetrics
   196  
   197  	for _, line := range strings.Split(payload, "\n") {
   198  		metricName, val, labels := p.metricValueLabels(line)
   199  		if metricName == "" {
   200  			continue
   201  		}
   202  
   203  		em := p.baseEM.Clone().AddLabel("dst", target)
   204  		em.Timestamp = payloadTS
   205  		for _, kv := range labels {
   206  			em.AddLabel(kv[0], kv[1])
   207  		}
   208  
   209  		// If pre-configured, distribution metric.
   210  		if dv, ok := p.distMetrics[metricName]; ok {
   211  			d := dv.Clone().(*metrics.Distribution)
   212  			processDistValue(d, val)
   213  			em.AddMetric(metricName, d)
   214  			results = append(results, em)
   215  			continue
   216  		}
   217  
   218  		if err := addNewMetric(em, metricName, val); err != nil {
   219  			p.l.Warning(err.Error())
   220  			continue
   221  		}
   222  		results = append(results, em)
   223  	}
   224  
   225  	return results
   226  }
   227  
   228  // AggregatedPayloadMetrics parses the given payload and updates the provided
   229  // metrics. If provided payload metrics is nil, we initialize a new one using
   230  // the default values configured at the time of parser creation.
   231  func (p *Parser) AggregatedPayloadMetrics(em *metrics.EventMetrics, payload, target string) *metrics.EventMetrics {
   232  	// If not initialized yet, initialize metrics from the default metrics.
   233  	if em == nil {
   234  		em = p.baseEM.Clone().AddLabel("dst", target)
   235  		for m, v := range p.distMetrics {
   236  			em.AddMetric(m, v)
   237  		}
   238  	}
   239  
   240  	em.Timestamp = time.Now()
   241  
   242  	for _, line := range strings.Split(payload, "\n") {
   243  		metricName, val, _ := p.metricValueLabels(line)
   244  		if metricName == "" {
   245  			continue
   246  		}
   247  
   248  		// If a metric already exists in the EventMetric, we simply add the new
   249  		// value (after parsing) to it.
   250  		if mv := em.Metric(metricName); mv != nil {
   251  			if err := updateMetricValue(mv, val); err != nil {
   252  				p.l.Warningf("Error updating metric %s with val %s: %v", metricName, val, err)
   253  			}
   254  			continue
   255  		}
   256  
   257  		if err := addNewMetric(em, metricName, val); err != nil {
   258  			p.l.Warning(err.Error())
   259  			continue
   260  		}
   261  	}
   262  
   263  	return em
   264  }
   265  
   266  // processDistValue processes a distribution value. It works with distribution
   267  // values in 2 formats:
   268  // a) a full distribution string, capturing all the details, e.g.
   269  //    "dist:sum:899|count:221|lb:-Inf,0.5,2,7.5|bc:34,54,121,12"
   270  // a) a comma-separated list of floats, where distribution details have been
   271  //    provided at the time of config, e.g.
   272  //    "12,13,10.1,9.875,11.1"
   273  func processDistValue(mVal *metrics.Distribution, val string) error {
   274  	if val[0] == 'd' {
   275  		distVal, err := metrics.ParseDistFromString(val)
   276  		if err != nil {
   277  			return err
   278  		}
   279  		return mVal.Add(distVal)
   280  	}
   281  
   282  	// It's a pre-defined distribution metric
   283  	for _, s := range strings.Split(val, ",") {
   284  		f, err := strconv.ParseFloat(s, 64)
   285  		if err != nil {
   286  			return fmt.Errorf("unsupported value for distribution metric (expected comma separated list of float64s): %s", val)
   287  		}
   288  		mVal.AddFloat64(f)
   289  	}
   290  	return nil
   291  }