go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/statsd-to-tsmon/main_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 "context" 19 "net" 20 "strings" 21 "testing" 22 "time" 23 24 "go.chromium.org/luci/common/tsmon" 25 "go.chromium.org/luci/common/tsmon/distribution" 26 27 "go.chromium.org/luci/server/cmd/statsd-to-tsmon/config" 28 29 . "github.com/smartystreets/goconvey/convey" 30 ) 31 32 func TestEndToEnd(t *testing.T) { 33 t.Parallel() 34 35 Convey("Works", t, func() { 36 cfg, err := loadConfig(&config.Config{ 37 Metrics: []*config.Metric{ 38 { 39 Metric: "e2e/counter", 40 Kind: config.Kind_COUNTER, 41 Fields: []string{"f1", "f2"}, 42 Rules: []*config.Rule{ 43 { 44 Pattern: "statsd.${f}.counter", 45 Fields: map[string]string{"f1": "static", "f2": "${f}"}, 46 }, 47 }, 48 }, 49 { 50 Metric: "e2e/gauge", 51 Kind: config.Kind_GAUGE, 52 Fields: []string{"f1", "f2"}, 53 Rules: []*config.Rule{ 54 { 55 Pattern: "statsd.${f}.gauge", 56 Fields: map[string]string{"f1": "static", "f2": "${f}"}, 57 }, 58 }, 59 }, 60 { 61 Metric: "e2e/timer", 62 Kind: config.Kind_CUMULATIVE_DISTRIBUTION, 63 Fields: []string{"f1", "f2"}, 64 Rules: []*config.Rule{ 65 { 66 Pattern: "statsd.${f}.timer", 67 Fields: map[string]string{"f1": "static", "f2": "${f}"}, 68 }, 69 }, 70 }, 71 }, 72 }) 73 So(err, ShouldBeNil) 74 75 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 76 ctx, _ = tsmon.WithDummyInMemory(ctx) 77 store := tsmon.Store(ctx) 78 79 // The listening socket. 80 pc, err := net.ListenPacket("udp", "localhost:0") 81 So(err, ShouldBeNil) 82 defer pc.Close() 83 84 // The socket used by the test to send packets. 85 con, err := net.Dial("udp", pc.LocalAddr().String()) 86 So(err, ShouldBeNil) 87 defer con.Close() 88 89 // Tick is signaled after each processed UDP packet. 90 tick := make(chan struct{}) 91 92 // Run mainLoop in background, make sure it is done before we exit. 93 done := make(chan struct{}) 94 go func() { 95 defer close(done) 96 mainLoop(ctx, pc, cfg, tick) 97 }() 98 defer func() { <-done }() 99 100 // This must be the last defer, so it is called first to trigger 101 // the shutdown of everything else. 102 defer cancel() 103 104 // Sends a statsd UDP packet and waits until it is processed. 105 send := func(packet string) { 106 _, err := con.Write([]byte(packet)) 107 So(err, ShouldBeNil) 108 select { 109 case <-tick: 110 case <-time.After(5 * time.Second): 111 panic("timeout") 112 } 113 } 114 115 // Send a bunch of metrics. 116 send("statsd.a.counter:1|c") 117 send("statsd.a.counter:1|c") 118 send("statsd.b.counter:1|c") 119 send("statsd.a.gauge:123|g") 120 send("statsd.a.timer:123|ms") 121 122 // Parsed successfully. 123 val := store.Get(ctx, cfg.metrics["e2e/counter"], time.Time{}, []any{"static", "a"}) 124 So(val, ShouldEqual, 2) 125 val = store.Get(ctx, cfg.metrics["e2e/counter"], time.Time{}, []any{"static", "b"}) 126 So(val, ShouldEqual, 1) 127 val = store.Get(ctx, cfg.metrics["e2e/gauge"], time.Time{}, []any{"static", "a"}) 128 So(val, ShouldEqual, 123) 129 val = store.Get(ctx, cfg.metrics["e2e/timer"], time.Time{}, []any{"static", "a"}) 130 So(val.(*distribution.Distribution).Sum(), ShouldEqual, 123) 131 132 // Updated its own internal metric. 133 So(getStatsdMetricsProcessed(ctx), ShouldResemble, map[string]int64{ 134 "OK": 5, 135 }) 136 137 // Send a bunch of metrics in a single packet. Intermix some broken metrics. 138 send(strings.Join([]string{ 139 "statsd.a.counter:1|c", 140 "broken", 141 "stats.unsupported:1|h", 142 "statsd.a.counter:1|g", // wrong type 143 "statsd.skipped:1|c", // skipped 144 "statsd.b.counter:1|c", 145 }, "\n")) 146 147 // Tsmon metrics are updated now. 148 val = store.Get(ctx, cfg.metrics["e2e/counter"], time.Time{}, []any{"static", "a"}) 149 So(val, ShouldEqual, 3) 150 val = store.Get(ctx, cfg.metrics["e2e/counter"], time.Time{}, []any{"static", "b"}) 151 So(val, ShouldEqual, 2) 152 153 // Updated its own internal metric. 154 So(getStatsdMetricsProcessed(ctx), ShouldResemble, map[string]int64{ 155 "OK": 7, 156 "MALFORMED": 1, 157 "UNSUPPORTED": 1, 158 "UNEXPECTED": 1, 159 "SKIPPED": 1, 160 }) 161 }) 162 } 163 164 func getStatsdMetricsProcessed(ctx context.Context) map[string]int64 { 165 out := map[string]int64{} 166 store := tsmon.Store(ctx) 167 for _, f := range []string{ 168 "OK", 169 "MALFORMED", 170 "UNSUPPORTED", 171 "UNEXPECTED", 172 "SKIPPED", 173 "UNKNOWN", 174 } { 175 val := store.Get(ctx, statsdMetricsProcessed, time.Time{}, []any{f}) 176 if val != nil { 177 out[f] = val.(int64) 178 } 179 } 180 return out 181 }