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 }