go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/statsd-to-tsmon/config_test.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 "strings" 19 "testing" 20 21 "go.chromium.org/luci/common/tsmon/field" 22 "go.chromium.org/luci/common/tsmon/metric" 23 "go.chromium.org/luci/common/tsmon/types" 24 25 "go.chromium.org/luci/server/cmd/statsd-to-tsmon/config" 26 27 . "github.com/smartystreets/goconvey/convey" 28 . "go.chromium.org/luci/common/testing/assertions" 29 ) 30 31 func TestConfig(t *testing.T) { 32 t.Parallel() 33 34 Convey("Works", t, func() { 35 cfg, err := loadConfig(&config.Config{ 36 Metrics: []*config.Metric{ 37 { 38 Metric: "m1", 39 Kind: config.Kind_COUNTER, 40 Fields: []string{"f1", "f2"}, 41 Rules: []*config.Rule{ 42 { 43 Pattern: "*.foo.${var}.*.bar.sfx1", 44 Fields: map[string]string{ 45 "f1": "static", 46 "f2": "${var}", 47 }, 48 }, 49 }, 50 }, 51 { 52 Metric: "m2", 53 Kind: config.Kind_COUNTER, 54 Rules: []*config.Rule{ 55 { 56 Pattern: "foo.bar.sfx2", 57 }, 58 }, 59 }, 60 }, 61 }) 62 So(err, ShouldBeNil) 63 64 rule := func(m string) string { 65 chunks := strings.Split(m, ".") 66 bytes := make([][]byte, len(chunks)) 67 for i, c := range chunks { 68 bytes[i] = []byte(c) 69 } 70 if r := cfg.FindMatchingRule(bytes); r != nil { 71 return r.pattern.str 72 } 73 return "" 74 } 75 76 So(rule("xxx.foo.val.xxx.bar.sfx1"), ShouldEqual, "*.foo.${var}.*.bar.sfx1") 77 So(rule("yyy.foo.val.yyy.bar.sfx1"), ShouldEqual, "*.foo.${var}.*.bar.sfx1") 78 So(rule("foo.bar.sfx2"), ShouldEqual, "foo.bar.sfx2") 79 80 // Wrong length. 81 So(rule("foo.val.xxx.bar.sfx1"), ShouldEqual, "") 82 // Wrong static component. 83 So(rule("xxx.foo.val.xxx.baz.sfx1"), ShouldEqual, "") 84 // Unknown suffix. 85 So(rule("foo.bar.sfx3"), ShouldEqual, "") 86 // Empty. 87 So(rule(""), ShouldEqual, "") 88 }) 89 90 Convey("Errors", t, func() { 91 call := func(cfg *config.Config) error { 92 _, err := loadConfig(cfg) 93 return err 94 } 95 96 Convey("Bad pattern", func() { 97 So(call(&config.Config{ 98 Metrics: []*config.Metric{ 99 { 100 Metric: "m3", 101 Kind: config.Kind_COUNTER, 102 Rules: []*config.Rule{ 103 {Pattern: "."}, 104 }, 105 }, 106 }, 107 }), ShouldErrLike, `bad pattern`) 108 }) 109 110 Convey("Not enough fields", func() { 111 So(call(&config.Config{ 112 Metrics: []*config.Metric{ 113 { 114 Metric: "m4", 115 Kind: config.Kind_COUNTER, 116 Fields: []string{"f1", "f2"}, 117 Rules: []*config.Rule{ 118 {Pattern: "foo", Fields: map[string]string{"f1": "value"}}, 119 }, 120 }, 121 }, 122 }), ShouldErrLike, `value of field "f2" is not provided`) 123 }) 124 125 Convey("Extra field", func() { 126 So(call(&config.Config{ 127 Metrics: []*config.Metric{ 128 { 129 Metric: "m5", 130 Kind: config.Kind_COUNTER, 131 Rules: []*config.Rule{ 132 {Pattern: "foo", Fields: map[string]string{"f1": "value"}}, 133 }, 134 }, 135 }, 136 }), ShouldErrLike, `has too many fields`) 137 }) 138 139 Convey("Bad field value", func() { 140 So(call(&config.Config{ 141 Metrics: []*config.Metric{ 142 { 143 Metric: "m6", 144 Kind: config.Kind_COUNTER, 145 Fields: []string{"f1"}, 146 Rules: []*config.Rule{ 147 {Pattern: "foo", Fields: map[string]string{"f1": "foo-${bar}"}}, 148 }, 149 }, 150 }, 151 }), ShouldErrLike, `field "f1" has bad value`) 152 }) 153 154 Convey("Unknown var ref", func() { 155 So(call(&config.Config{ 156 Metrics: []*config.Metric{ 157 { 158 Metric: "m7", 159 Kind: config.Kind_COUNTER, 160 Fields: []string{"f1"}, 161 Rules: []*config.Rule{ 162 {Pattern: "foo", Fields: map[string]string{"f1": "${bar}"}}, 163 }, 164 }, 165 }, 166 }), ShouldErrLike, `field "f1" references undefined var "bar"`) 167 }) 168 169 Convey("Not a static suffix", func() { 170 So(call(&config.Config{ 171 Metrics: []*config.Metric{ 172 { 173 Metric: "m8", 174 Kind: config.Kind_COUNTER, 175 Rules: []*config.Rule{ 176 {Pattern: "foo.*"}, 177 }, 178 }, 179 }, 180 }), ShouldErrLike, `must end with a static suffix`) 181 }) 182 183 Convey("Dup suffix", func() { 184 So(call(&config.Config{ 185 Metrics: []*config.Metric{ 186 { 187 Metric: "m9", 188 Kind: config.Kind_COUNTER, 189 Rules: []*config.Rule{ 190 {Pattern: "foo1.bar"}, 191 }, 192 }, 193 { 194 Metric: "m10", 195 Kind: config.Kind_COUNTER, 196 Rules: []*config.Rule{ 197 {Pattern: "foo2.bar"}, 198 }, 199 }, 200 }, 201 }), ShouldErrLike, `there's already another rule with this suffix`) 202 }) 203 }) 204 } 205 206 func TestLoadMetrics(t *testing.T) { 207 t.Parallel() 208 209 Convey("Works", t, func() { 210 m, err := loadMetrics([]*config.Metric{ 211 { 212 Metric: "gauge", 213 Kind: config.Kind_GAUGE, 214 Units: config.Unit_MILLISECONDS, 215 Fields: []string{"f1", "f2"}, 216 }, 217 { 218 Metric: "counter", 219 Kind: config.Kind_COUNTER, 220 Units: config.Unit_BYTES, 221 Fields: []string{"f3", "f4"}, 222 }, 223 { 224 Metric: "distribution", 225 Kind: config.Kind_CUMULATIVE_DISTRIBUTION, 226 Fields: []string{"f5", "f6"}, 227 }, 228 }) 229 So(err, ShouldBeNil) 230 So(m, ShouldHaveLength, 3) 231 232 g, ok := m["gauge"].(metric.Int) 233 So(ok, ShouldBeTrue) 234 So(g.Metadata().Units, ShouldEqual, types.Milliseconds) 235 So(g.Info().Fields, ShouldResemble, []field.Field{ 236 {Name: "f1", Type: field.StringType}, 237 {Name: "f2", Type: field.StringType}, 238 }) 239 240 c, ok := m["counter"].(metric.Counter) 241 So(ok, ShouldBeTrue) 242 So(c.Metadata().Units, ShouldEqual, types.Bytes) 243 So(c.Info().Fields, ShouldResemble, []field.Field{ 244 {Name: "f3", Type: field.StringType}, 245 {Name: "f4", Type: field.StringType}, 246 }) 247 248 d, ok := m["distribution"].(metric.CumulativeDistribution) 249 So(ok, ShouldBeTrue) 250 So(d.Metadata().Units, ShouldEqual, types.Milliseconds) 251 So(d.Info().Fields, ShouldResemble, []field.Field{ 252 {Name: "f5", Type: field.StringType}, 253 {Name: "f6", Type: field.StringType}, 254 }) 255 }) 256 } 257 258 func TestPattern(t *testing.T) { 259 t.Parallel() 260 261 Convey("Parse success", t, func() { 262 p, err := parsePattern("abc.${foo}.*.${bar}.zzz") 263 So(err, ShouldBeNil) 264 So(p, ShouldResemble, &pattern{ 265 str: "abc.${foo}.*.${bar}.zzz", 266 len: 5, 267 vars: map[string]int{ 268 "foo": 1, 269 "bar": 3, 270 }, 271 static: []staticNameComponent{ 272 {0, "abc"}, 273 {4, "zzz"}, 274 }, 275 suffix: "zzz", 276 }) 277 }) 278 279 Convey("All static", t, func() { 280 p, err := parsePattern("abc.def") 281 So(err, ShouldBeNil) 282 So(p, ShouldResemble, &pattern{ 283 str: "abc.def", 284 len: 2, 285 static: []staticNameComponent{ 286 {0, "abc"}, 287 {1, "def"}, 288 }, 289 suffix: "def", 290 }) 291 }) 292 293 Convey("Empty component", t, func() { 294 _, err := parsePattern("abc..zzz") 295 So(err, ShouldErrLike, "empty name component") 296 }) 297 298 Convey("Bad var", t, func() { 299 _, err := parsePattern("${}") 300 So(err, ShouldErrLike, "var name is required") 301 302 _, err = parsePattern("foo-${bar}") 303 So(err, ShouldErrLike, "is not allowed") 304 }) 305 306 Convey("Dup var", t, func() { 307 _, err := parsePattern("${abc}.${abc}") 308 So(err, ShouldErrLike, "duplicate var") 309 }) 310 311 Convey("Var suffix", t, func() { 312 _, err := parsePattern("${abc}.${def}") 313 So(err, ShouldErrLike, "must end with a static suffix") 314 }) 315 316 Convey("Star suffix", t, func() { 317 _, err := parsePattern("abc.*") 318 So(err, ShouldErrLike, "must end with a static suffix") 319 }) 320 }