aboutsummaryrefslogtreecommitdiffstats
path: root/library/StatsD_Checker.ttcn
blob: 8ace1508f9a97b32065a8dcf28d63f65c579555f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
module StatsD_Checker {

/* Verifies that  StatsD metrics in a test match the expected values
 * Uses StatsD_CodecPort to receive the statsd messages from the DUT
 * and a separate VTY connection to reset and trigger the stats.
 *
 * When using this you should configure your stats reporter to disable
 * interval-based reports and always send all metrics:
 * > stats interval 0
 * > stats reporter statsd
 * >  remote-ip a.b.c.d
 * >  remote-port 8125
 * >  level subscriber
 * >  flush-period 1
 * >  mtu 1024
 * >  enable
 *
 * (C) 2020 by sysmocom s.f.m.c. GmbH <info@sysmocom.de>
 * All rights reserved.
 *
 * Author: Daniel Willmann <dwillmann@sysmocom.de>
 *
 * Released under the terms of GNU General Public License, Version 2 or
 * (at your option) any later version.
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

import from Misc_Helpers all;
import from Socket_API_Definitions all;

import from StatsD_Types all;
import from StatsD_CodecPort all;
import from StatsD_CodecPort_CtrlFunct all;

import from Osmocom_Types all;
import from Osmocom_VTY_Functions all;
import from TELNETasp_PortType all;

modulepar {
	/* Whether to test stats values */
	boolean mp_enable_stats := true;
}

type record StatsDExpect {
	MetricName	name,
	MetricType	mtype,
	MetricValue	min,
	MetricValue	max
};

type set of StatsDExpect StatsDExpects;

type record StatsDExpectPriv {
	StatsDExpect expect,
	integer seen
}

type set of StatsDExpectPriv StatsDExpectPrivs;

type enumerated StatsDResultType {
	e_Matched,
	e_Mismatched,
	e_NotFound
}

type record StatsDExpectResult {
	StatsDResultType kind,
	integer idx
}

type component StatsD_Checker_CT {
	port TELNETasp_PT STATSVTY;
	port STATSD_PROC_PT STATSD_PROC;
	port STATSD_CODEC_PT STATS;
	timer T_statsd := 5.0;
}

type component StatsD_ConnHdlr {
	port STATSD_PROC_PT STATSD_PROC;
}

signature STATSD_reset();
signature STATSD_expect(in StatsDExpects expects) return boolean;

type port STATSD_PROC_PT procedure {
	inout STATSD_reset, STATSD_expect;
} with {extension "internal"};

/* Expect templates and functions */


/* StatsD checker component */
function main(charstring statsd_host, integer statsd_port) runs on StatsD_Checker_CT {
	var StatsD_ConnHdlr vc_conn;
	var StatsDExpects expects;
	var Result res;

	while (not mp_enable_stats) {
		log("StatsD checker disabled by modulepar");
		f_sleep(3600.0);
	}

	map(self:STATS, system:STATS);
	res := StatsD_CodecPort_CtrlFunct.f_IPL4_listen(STATS, statsd_host, statsd_port, { udp := {} }, {});
	if (not ispresent(res.connId)) {
		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
					"Could not bind StatsD socket, check your configuration");
	}

	/* Connect to VTY and reset stats */
	map(self:STATSVTY, system:STATSVTY);
	f_vty_set_prompts(STATSVTY);
	f_vty_transceive(STATSVTY, "enable");

	/* Reset the stats system at start */
	f_vty_transceive(STATSVTY, "stats reset");

	while (true) {
		alt {
		[] STATSD_PROC.getcall(STATSD_reset:{}) -> sender vc_conn {
			f_vty_transceive(STATSVTY, "stats reset");
			STATSD_PROC.reply(STATSD_reset:{}) to vc_conn;
			}
		[] STATSD_PROC.getcall(STATSD_expect:{?}) -> param(expects) sender vc_conn {
			var boolean success := f_statsd_checker_expect(expects);
			STATSD_PROC.reply(STATSD_expect:{expects} value success) to vc_conn;
			}
		}
	}
}


/* Return false if the expectation doesn't match the metric, otherwise return true */
private function f_compare_expect(StatsDMetric metric, StatsDExpect expect) return boolean {
	if ((metric.name == expect.name) and (metric.mtype == expect.mtype)
		and (metric.val >= expect.min) and (metric.val <= expect.max)) {
		return true;
	} else {
		return false;
	}
}

private function f_statsd_checker_metric_expects(StatsDExpectPrivs exp_seen, StatsDMetric metric)
return StatsDExpectResult {
	var StatsDExpectResult result := {
		kind := e_NotFound,
		idx := -1
	};

	for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
		var StatsDExpectPriv exp := exp_seen[i];
		if (exp.expect.name != metric.name) {
			continue;
		}
		if (not f_compare_expect(metric, exp.expect)) {
			log("EXP mismatch: ", metric, " vs ", exp.expect);
			result := {
				kind := e_Mismatched,
				idx := i
			};
			break;
		} else {
			log("EXP match: ", metric, " vs ", exp.expect);
			result := {
				kind := e_Matched,
				idx := i
			};
			break;
		}
	}
	return result;
}

template StatsDExpectPriv t_statsd_expect_priv(template StatsDExpect expect) := {
	expect := expect,
	seen := 0
}

private function f_statsd_checker_expect(StatsDExpects expects) runs on StatsD_Checker_CT return boolean {
	var default t;
	var StatsDMessage msg;
	var StatsDExpectResult res;
	var StatsDExpectPrivs exp_seen := {};

	for (var integer i := 0; i < lengthof(expects); i := i + 1) {
		exp_seen := exp_seen & {valueof(t_statsd_expect_priv(expects[i]))};
	}

	/* Dismiss any messages we might have skipped from the last report */
	STATS.clear;

	f_vty_transceive(STATSVTY, "stats report");

	var boolean seen_all := false;
	T_statsd.start;
	while (not seen_all) {
		var StatsD_RecvFrom rf;
		alt {
		[] STATS.receive(tr_StatsD_RecvFrom(?, ?)) -> value rf {
			msg := rf.msg;
			}
		[] T_statsd.timeout {
			for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
				/* We're still missing some expects, keep looking */
				if (exp_seen[i].seen == 0) {
					log("Timeout waiting for ", exp_seen[i].expect.name, " (min: ", exp_seen[i].expect.min,
						", max: ", exp_seen[i].expect.max, ")");
				}
			}
			setverdict(fail, "Timeout waiting for metrics");
			return false;
		}
		}

		for (var integer i := 0; i < lengthof(msg); i := i + 1) {
			var StatsDMetric metric := msg[i];

			res := f_statsd_checker_metric_expects(exp_seen, metric);
			if (res.kind == e_NotFound) {
				continue;
			}

			if (res.kind == e_Mismatched) {
				log("Metric: ", metric);
				log("Expect: ", exp_seen[res.idx].expect);
				setverdict(fail, "Metric failed expectation ", metric, " vs ", exp_seen[res.idx].expect);
				return false;
			} else if (res.kind == e_Matched) {
				exp_seen[res.idx].seen := exp_seen[res.idx].seen + 1;
			}
		}

		/* Check if all expected metrics were received */
		seen_all := true;
		for (var integer i := 0; i < lengthof(exp_seen); i := i + 1) {
			/* We're still missing some expects, keep looking */
			if (exp_seen[i].seen == 0) {
				seen_all := false;
				break;
			}
		}
	}

	T_statsd.stop;
	return seen_all;
}

function f_init_statsd(charstring id, inout StatsD_Checker_CT vc_STATSD, charstring dst_addr, integer dst_port) {
	id := id & "-STATS";

	vc_STATSD := StatsD_Checker_CT.create(id);
	vc_STATSD.start(StatsD_Checker.main(dst_addr, dst_port));
}


/* StatsD connhdlr */
function f_statsd_reset() runs on StatsD_ConnHdlr {
	if (not mp_enable_stats) {
		return;
	}

	STATSD_PROC.call(STATSD_reset:{}) {
		[] STATSD_PROC.getreply(STATSD_reset:{}) {}
	}
}

function f_statsd_expect(StatsDExpects expects) runs on StatsD_ConnHdlr return boolean {
	var boolean res;

	if (not mp_enable_stats) {
		return true;
	}

	STATSD_PROC.call(STATSD_expect:{expects}) {
		[] STATSD_PROC.getreply(STATSD_expect:{expects}) -> value res;
	}
	return res;
}

}