blob: d287b744356678f756ae7202fac308b606551859 [file] [log] [blame]
Daniel Willmann74237282020-08-12 12:44:04 +02001module StatsD_Checker {
2
3/* Verifies that StatsD metrics in a test match the expected values
4 * Uses StatsD_CodecPort to receive the statsd messages from the DUT
5 * and a separate VTY connection to reset and trigger the stats.
6 *
7 * When using this you should configure your stats reporter to disable
8 * interval-based reports and always send all metrics:
9 * > stats interval 0
10 * > stats reporter statsd
11 * > remote-ip a.b.c.d
12 * > remote-port 8125
13 * > level subscriber
14 * > flush-period 1
15 * > mtu 1024
16 * > enable
17 *
18 * (C) 2020 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
19 * All rights reserved.
20 *
21 * Author: Daniel Willmann <dwillmann@sysmocom.de>
22 *
23 * Released under the terms of GNU General Public License, Version 2 or
24 * (at your option) any later version.
25 * SPDX-License-Identifier: GPL-2.0-or-later
26 */
27
28import from StatsD_Types all;
29import from StatsD_CodecPort all;
30import from StatsD_CodecPort_CtrlFunct all;
31
32import from Osmocom_Types all;
33import from Osmocom_VTY_Functions all;
34import from TELNETasp_PortType all;
35
36modulepar {
37 /* Whether to test stats values */
38 boolean mp_enable_stats := false;
39}
40
41type record StatsDExpect {
42 MetricName name,
43 MetricType mtype,
44 MetricValue min,
45 MetricValue max
46};
47
48type set of StatsDExpect StatsDExpects;
49
50type record StatsDExpectPriv {
51 StatsDExpect expect,
52 integer seen
53}
54
55type set of StatsDExpectPriv StatsDExpectPrivs;
56
57type enumerated StatsDResultType {
58 e_Matched,
59 e_Mismatched,
60 e_NotFound
61}
62
63type record StatsDExpectResult {
64 StatsDResultType kind,
65 integer idx
66}
67
68type component StatsD_Checker_CT {
69 port TELNETasp_PT STATSVTY;
70 port STATSD_PROC_PT STATSD_PROC;
71 port STATSD_CODEC_PT STATS;
72 timer T_statsd := 5.0;
73}
74
75type component StatsD_ConnHdlr {
76 port STATSD_PROC_PT STATSD_PROC;
77}
78
79signature STATSD_reset();
80signature STATSD_expect(in StatsDExpects expects) return boolean;
81
82type port STATSD_PROC_PT procedure {
83 inout STATSD_reset, STATSD_expect;
84} with {extension "internal"};
85
86/* Expect templates and functions */
87
88
89/* StatsD checker component */
90function main(charstring statsd_host, integer statsd_port) runs on StatsD_Checker_CT {
91 var StatsD_ConnHdlr vc_conn;
92 var StatsDExpects expects;
93
94 while (not mp_enable_stats) {
95 log("StatsD checker disabled by modulepar");
96 f_sleep(3600.0);
97 }
98
99 map(self:STATS, system:STATS);
100 StatsD_CodecPort_CtrlFunct.f_IPL4_listen(STATS, statsd_host, statsd_port, { udp := {} }, {});
101
102 /* Connect to VTY and reset stats */
103 map(self:STATSVTY, system:STATSVTY);
104 f_vty_set_prompts(STATSVTY);
105 f_vty_transceive(STATSVTY, "enable");
106
107 /* Reset the stats system at start */
108 f_vty_transceive(STATSVTY, "stats reset");
109
110 while (true) {
111 alt {
112 [] STATSD_PROC.getcall(STATSD_reset:{}) -> sender vc_conn {
113 f_vty_transceive(STATSVTY, "stats reset");
114 STATSD_PROC.reply(STATSD_reset:{}) to vc_conn;
115 }
116 [] STATSD_PROC.getcall(STATSD_expect:{?}) -> param(expects) sender vc_conn {
117 var boolean success := f_statsd_checker_expect(expects);
118 STATSD_PROC.reply(STATSD_expect:{expects} value success) to vc_conn;
119 }
120 }
121 }
122}
123
124
125/* Return false if the expectation doesn't match the metric, otherwise return true */
126private function f_compare_expect(StatsDMetric metric, StatsDExpect expect) return boolean {
127 if ((metric.name == expect.name) and (metric.mtype == expect.mtype)
128 and (metric.val >= expect.min) and (metric.val <= expect.max)) {
129 return true;
130 } else {
131 return false;
132 }
133}
134
135private function f_statsd_checker_metric_expects(StatsDExpectPrivs exp_seen, StatsDMetric metric)
136return StatsDExpectResult {
137 var StatsDExpectResult result := {
138 kind := e_NotFound,
139 idx := -1
140 };
141
142 for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
143 var StatsDExpectPriv exp := exp_seen[i];
144 if (exp.expect.name != metric.name) {
145 continue;
146 }
147 if (not f_compare_expect(metric, exp.expect)) {
148 log("EXP mismatch: ", metric, exp.expect);
149 result := {
150 kind := e_Mismatched,
151 idx := i
152 };
153 break;
154 } else {
155 log("EXP match: ", metric, exp.expect);
156 result := {
157 kind := e_Matched,
158 idx := i
159 };
160 break;
161 }
162 }
163 return result;
164}
165
166template StatsDExpectPriv t_statsd_expect_priv(template StatsDExpect expect) := {
167 expect := expect,
168 seen := 0
169}
170
171private function f_statsd_checker_expect(StatsDExpects expects) runs on StatsD_Checker_CT return boolean {
172 var default t;
173 var StatsDMessage msg;
174 var StatsDExpectResult res;
175 var StatsDExpectPrivs exp_seen := {};
176
177 for (var integer i := 0; i < lengthof(expects); i := i + 1) {
178 exp_seen := exp_seen & {valueof(t_statsd_expect_priv(expects[i]))};
179 }
180
181 /* Dismiss any messages we might have skipped from the last report */
182 STATS.clear;
183
184 f_vty_transceive(STATSVTY, "stats report");
185
186 var boolean seen_all := false;
187 T_statsd.start;
188 while (not seen_all) {
189 var StatsD_RecvFrom rf;
190 alt {
191 [] STATS.receive(tr_StatsD_RecvFrom(?, ?)) -> value rf {
192 msg := rf.msg;
193 }
194 [] T_statsd.timeout {
195 for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
196 /* We're still missing some expects, keep looking */
197 if (exp_seen[i].seen == 0) {
198 log("Timeout waiting for ", exp_seen[i].expect.name, " (min: ", exp_seen[i].expect.min,
199 ", max: ", exp_seen[i].expect.max, ")");
200 }
201 }
202 setverdict(fail, "Timeout waiting for metrics");
203 return false;
204 }
205 }
206
207 for (var integer i := 0; i < lengthof(msg); i := i + 1) {
208 var StatsDMetric metric := msg[i];
209
210 res := f_statsd_checker_metric_expects(exp_seen, metric);
211 if (res.kind == e_NotFound) {
212 continue;
213 }
214
215 if (res.kind == e_Mismatched) {
216 log("Metric: ", metric);
217 log("Expect: ", exp_seen[res.idx].expect);
218 setverdict(fail, "Metric failed expectation ", metric, " vs ", exp_seen[res.idx].expect);
219 return false;
220 } else if (res.kind == e_Matched) {
221 exp_seen[res.idx].seen := exp_seen[res.idx].seen + 1;
222 }
223 }
224
225 /* Check if all expected metrics were received */
226 seen_all := true;
227 for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
228 /* We're still missing some expects, keep looking */
229 if (exp_seen[i].seen == 0) {
230 seen_all := false;
231 break;
232 }
233 }
234 }
235
236 T_statsd.stop;
237 return seen_all;
238}
239
240function f_init_statsd(charstring id, inout StatsD_Checker_CT vc_STATSD, charstring dst_addr, integer dst_port) {
241 id := id & "-STATS";
242
243 vc_STATSD := StatsD_Checker_CT.create(id);
244 vc_STATSD.start(StatsD_Checker.main(dst_addr, dst_port));
245}
246
247
248/* StatsD connhdlr */
249function f_statsd_reset() runs on StatsD_ConnHdlr {
250 if (not mp_enable_stats) {
251 return;
252 }
253
254 STATSD_PROC.call(STATSD_reset:{}) {
255 [] STATSD_PROC.getreply(STATSD_reset:{}) {}
256 }
257}
258
259function f_statsd_expect(StatsDExpects expects) runs on StatsD_ConnHdlr return boolean {
260 var boolean res;
261
262 if (not mp_enable_stats) {
263 return true;
264 }
265
266 STATSD_PROC.call(STATSD_expect:{expects}) {
267 [] STATSD_PROC.getreply(STATSD_expect:{expects}) -> value res;
268 }
269 return res;
270}
271
272}