go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/statsd-to-tsmon/config.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 "os" 19 "strings" 20 21 "github.com/golang/protobuf/proto" 22 23 "go.chromium.org/luci/common/data/stringset" 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/tsmon/distribution" 26 "go.chromium.org/luci/common/tsmon/field" 27 "go.chromium.org/luci/common/tsmon/metric" 28 "go.chromium.org/luci/common/tsmon/types" 29 30 "go.chromium.org/luci/server/cmd/statsd-to-tsmon/config" 31 ) 32 33 // NameComponentIndex is an index of a statsd metric name components. 34 // 35 // E.g. in a metric "envoy.clusters.upstream.membership_healthy" the component 36 // "clusters" has index 1. 37 type NameComponentIndex int 38 39 // Config holds rules for converting statsd metrics into tsmon metrics. 40 // 41 // Each rule tells how to transform statsd metric name into a tsmon metric 42 // and its fields. 43 type Config struct { 44 metrics map[string]types.Metric // registered tsmon metrics 45 perSuffix map[string]*Rule // statsd metric suffix -> its conversion rule 46 } 47 48 // Rule describes how to send a tsmon metric given a matching statsd metric. 49 type Rule struct { 50 // Metric is some concrete tsmon metric. 51 // 52 // Its set of fields matches `Fields`. 53 Metric types.Metric 54 55 // Fields describes how to assemble metric fields. 56 // 57 // Each item either a string for a preset field value or a NameComponentIndex 58 // to grab field's value from parsed statsd metric name. 59 Fields []any 60 61 // Statsd metric name pattern, as taken from the config. 62 pattern *pattern 63 } 64 65 // pattern is a parsed conversion rule pattern. 66 // 67 // Parsed pattern "*.cluster.${upstream}.membership_healthy" results in 68 // 69 // pattern{ 70 // str: "*.cluster.${upstream}.membership_healthy", 71 // len: 4, 72 // vars: {"upstream": 2}, 73 // static: [{1, "cluster"}, {3, "membership_healthy"}] 74 // suffix: "membership_healthy", 75 // } 76 type pattern struct { 77 str string 78 len int 79 vars map[string]int 80 static []staticNameComponent 81 suffix string 82 } 83 84 // staticNameComponent is static component of a pattern. 85 type staticNameComponent struct { 86 index int 87 value string 88 } 89 90 // LoadConfig parses and interprets the configuration file. 91 // 92 // It should be a config.Config proto encoded using jsonpb. 93 func LoadConfig(path string) (*Config, error) { 94 blob, err := os.ReadFile(path) 95 if err != nil { 96 return nil, err 97 } 98 msg, err := parseConfig(blob) 99 if err != nil { 100 return nil, err 101 } 102 return loadConfig(msg) 103 } 104 105 // FindMatchingRule finds a conversion rule that matches the statsd metric name. 106 // 107 // The metric name is given as a list of its components, e.g. "a.b.c" is 108 // represented by [][]byte{{'a'}, {'b'}, {'c'}}. 109 func (cfg *Config) FindMatchingRule(name [][]byte) *Rule { 110 // Find the rule matching the suffix. 111 if len(name) == 0 { 112 return nil 113 } 114 rule := cfg.perSuffix[string(name[len(name)-1])] 115 if rule == nil { 116 return nil 117 } 118 119 // Skip if `name` doesn't match the rest of the pattern. 120 if len(name) != rule.pattern.len { 121 return nil 122 } 123 for _, s := range rule.pattern.static { 124 if string(name[s.index]) != s.value { 125 return nil 126 } 127 } 128 129 return rule 130 } 131 132 // parseConfig converts config jsonpb into a proto. 133 func parseConfig(blob []byte) (*config.Config, error) { 134 cfg := &config.Config{} 135 if err := proto.UnmarshalText(string(blob), cfg); err != nil { 136 return nil, errors.Annotate(err, "bad config format").Err() 137 } 138 return cfg, nil 139 } 140 141 // loadConfig interprets the config proto. 142 func loadConfig(cfg *config.Config) (*Config, error) { 143 metrics, err := loadMetrics(cfg.Metrics) 144 if err != nil { 145 return nil, err 146 } 147 148 perSuffix := map[string]*Rule{} 149 150 for _, metricSpec := range cfg.Metrics { 151 metric := metrics[metricSpec.Metric] 152 for idx, ruleSpec := range metricSpec.Rules { 153 if ruleSpec.Pattern == "" { 154 return nil, errors.Reason("metric %q: rule #%d: a pattern is required", metricSpec.Metric, idx+1).Err() 155 } 156 157 rule, err := loadRule(metric, ruleSpec) 158 if err != nil { 159 return nil, errors.Annotate(err, "metric %q: rule %q", metricSpec.Metric, ruleSpec.Pattern).Err() 160 } 161 162 if perSuffix[rule.pattern.suffix] != nil { 163 return nil, errors.Reason("metric %q: rule %q: there's already another rule with this suffix", metricSpec.Metric, ruleSpec.Pattern).Err() 164 } 165 perSuffix[rule.pattern.suffix] = rule 166 } 167 } 168 169 return &Config{ 170 metrics: metrics, 171 perSuffix: perSuffix, 172 }, nil 173 } 174 175 // loadMetrics instantiates tsmon metrics based on the configuration. 176 func loadMetrics(cfg []*config.Metric) (map[string]types.Metric, error) { 177 metrics := make(map[string]types.Metric, len(cfg)) 178 179 for idx, spec := range cfg { 180 name := spec.Metric 181 if name == "" { 182 return nil, errors.Reason("metric #%d: a name is required", idx+1).Err() 183 } 184 if metrics[name] != nil { 185 return nil, errors.Reason("duplicate metric %q", name).Err() 186 } 187 188 if len(spec.Fields) != stringset.NewFromSlice(spec.Fields...).Len() { 189 return nil, errors.Reason("metric %q: has duplicate fields", name).Err() 190 } 191 tsmonFields := make([]field.Field, len(spec.Fields)) 192 for idx, fieldName := range spec.Fields { 193 tsmonFields[idx] = field.String(fieldName) 194 } 195 196 var metadata types.MetricMetadata 197 switch spec.Units { 198 case config.Unit_MILLISECONDS: 199 metadata.Units = types.Milliseconds 200 case config.Unit_BYTES: 201 metadata.Units = types.Bytes 202 case config.Unit_UNIT_UNSPECIFIED: 203 // no units, this is fine 204 default: 205 return nil, errors.Reason("metric %q: unrecognized units %s", name, spec.Units).Err() 206 } 207 208 var m types.Metric 209 switch spec.Kind { 210 case config.Kind_GAUGE: 211 m = metric.NewInt(name, spec.Desc, &metadata, tsmonFields...) 212 case config.Kind_COUNTER: 213 m = metric.NewCounter(name, spec.Desc, &metadata, tsmonFields...) 214 case config.Kind_CUMULATIVE_DISTRIBUTION: 215 // Distributions are used for StatsdMetricTimer metrics, they are always 216 // in milliseconds per statsd protocol. 217 m = metric.NewCumulativeDistribution( 218 name, 219 spec.Desc, 220 &types.MetricMetadata{Units: types.Milliseconds}, 221 distribution.DefaultBucketer, 222 tsmonFields...) 223 default: 224 return nil, errors.Reason("metric %q: unrecognized type %s", name, spec.Kind).Err() 225 } 226 227 metrics[name] = m 228 } 229 230 return metrics, nil 231 } 232 233 // loadRule interprets single rule{...} config stanza. 234 func loadRule(metric types.Metric, spec *config.Rule) (*Rule, error) { 235 pat, err := parsePattern(spec.Pattern) 236 if err != nil { 237 return nil, errors.Annotate(err, "bad pattern").Err() 238 } 239 240 // Make sure the rule specifies all required fields and only them. 241 tsmonFields := metric.Info().Fields 242 fields := make([]any, len(tsmonFields)) 243 for idx, f := range tsmonFields { 244 val, ok := spec.Fields[f.Name] 245 if !ok { 246 return nil, errors.Reason("value of field %q is not provided", f.Name).Err() 247 } 248 // Here `val` may be a variable (e.g. "${var}"), referring to a position 249 // in the parsed pattern, or just some static string. 250 vr, err := parseVar(val) 251 if err != nil { 252 return nil, errors.Annotate(err, "field %q has bad value %q", f.Name, val).Err() 253 } 254 switch { 255 case vr != "": 256 componentIdx, ok := pat.vars[vr] 257 if !ok { 258 return nil, errors.Reason("field %q references undefined var %q", f.Name, vr).Err() 259 } 260 fields[idx] = NameComponentIndex(componentIdx) 261 case val != "": 262 fields[idx] = val // just a static string 263 default: 264 return nil, errors.Reason("field %q has empty value, this is not allowed", f.Name).Err() 265 } 266 } 267 268 // We checked metricsDesc.fields is a subset of spec.Fields. Now check there 269 // are in fact equal. 270 if len(spec.Fields) != len(tsmonFields) { 271 return nil, errors.Reason("has too many fields").Err() 272 } 273 274 return &Rule{ 275 Metric: metric, 276 Fields: fields, 277 pattern: pat, 278 }, nil 279 } 280 281 // parsePattern parses a string like "*.cluster.${upstream}.membership_healthy" 282 // into its components. 283 // 284 // Each var appearance must be unique and the pattern should end with a static 285 // suffix (i.e. not ".*" and not ".${var}"). 286 func parsePattern(pat string) (*pattern, error) { 287 chunks := strings.Split(pat, ".") 288 p := &pattern{ 289 str: pat, 290 len: len(chunks), 291 } 292 for idx, chunk := range chunks { 293 if chunk == "" { 294 return nil, errors.Reason("empty name component").Err() 295 } 296 if chunk == "*" { 297 continue // an index not otherwise mentioned in *pattern is a wildcard 298 } 299 switch vr, err := parseVar(chunk); { 300 case err != nil: 301 return nil, errors.Annotate(err, "in name component %q", chunk).Err() 302 case vr != "": 303 if _, hasIt := p.vars[vr]; hasIt { 304 return nil, errors.Reason("duplicate var %q", vr).Err() 305 } 306 if p.vars == nil { 307 p.vars = make(map[string]int, 1) 308 } 309 p.vars[vr] = idx 310 default: 311 p.static = append(p.static, staticNameComponent{ 312 index: idx, 313 value: chunk, 314 }) 315 } 316 } 317 318 // We require suffixes to be static to simplify FindMatchingRule. 319 if len(p.static) != 0 { 320 if last := p.static[len(p.static)-1]; last.index == p.len-1 { 321 p.suffix = last.value 322 } 323 } 324 if p.suffix == "" { 325 return nil, errors.Reason("must end with a static suffix").Err() 326 } 327 328 return p, nil 329 } 330 331 // parseVar takes "${<something>}" and returns "<something>". 332 // 333 // If the input doesn't look like "${...}" and doesn't have "${" in it at all, 334 // returns an empty string and no error. 335 // 336 // If the input doesn't look like "${...}" but has "${" somewhere in it, returns 337 // an error: var usage such as "foo-${bar}" is not supported. 338 func parseVar(p string) (string, error) { 339 if strings.HasPrefix(p, "${") && strings.HasSuffix(p, "}") { 340 if len(p) == 3 { 341 return "", errors.Reason("var name is required").Err() 342 } 343 return p[2 : len(p)-1], nil 344 } 345 if strings.Contains(p, "${") { 346 return "", errors.Reason("var usage such as `foo-${bar}` is not allowed").Err() 347 } 348 return "", nil 349 }