Add parsing and checking of StatsD metrics
Change-Id: Icd1317b5f192d98e6cdc6635788d450501992bf1 Related: SYS#4877
This commit is contained in:
parent
62d5da3949
commit
742372880b
|
@ -0,0 +1,272 @@
|
|||
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 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 := false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
while (not mp_enable_stats) {
|
||||
log("StatsD checker disabled by modulepar");
|
||||
f_sleep(3600.0);
|
||||
}
|
||||
|
||||
map(self:STATS, system:STATS);
|
||||
StatsD_CodecPort_CtrlFunct.f_IPL4_listen(STATS, statsd_host, statsd_port, { udp := {} }, {});
|
||||
|
||||
/* 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, exp.expect);
|
||||
result := {
|
||||
kind := e_Mismatched,
|
||||
idx := i
|
||||
};
|
||||
break;
|
||||
} else {
|
||||
log("EXP match: ", metric, 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
module StatsD_CodecPort {
|
||||
|
||||
import from StatsD_Types all;
|
||||
import from IPL4asp_PortType all;
|
||||
import from IPL4asp_Types all;
|
||||
|
||||
type record StatsD_RecvFrom {
|
||||
ConnectionId connId,
|
||||
HostName remName,
|
||||
PortNumber remPort,
|
||||
HostName locName,
|
||||
PortNumber locPort,
|
||||
StatsDMessage msg
|
||||
}
|
||||
|
||||
template StatsD_RecvFrom tr_StatsD_RecvFrom(template ConnectionId cid, template StatsDMessage msg) := {
|
||||
connId := cid,
|
||||
remName := ?,
|
||||
remPort := ?,
|
||||
locName := ?,
|
||||
locPort := ?,
|
||||
msg := msg
|
||||
}
|
||||
|
||||
type record StatsD_Send {
|
||||
ConnectionId connId,
|
||||
StatsDMessage msg
|
||||
}
|
||||
|
||||
private function IPL4_to_StatsD_RecvFrom(in ASP_RecvFrom pin, out StatsD_RecvFrom pout) {
|
||||
pout.connId := pin.connId;
|
||||
pout.remName := pin.remName;
|
||||
pout.remPort := pin.remPort;
|
||||
pout.locName := pin.locName;
|
||||
pout.locPort := pin.locPort;
|
||||
pout.msg := dec_StatsDMessage(oct2char(pin.msg));
|
||||
} with { extension "prototype(fast)" };
|
||||
|
||||
private function StatsD_to_IPL4_Send(in StatsD_Send pin, out ASP_Send pout) {
|
||||
pout.connId := pin.connId;
|
||||
pout.proto := { udp := {} };
|
||||
pout.msg := char2oct(enc_StatsDMessage(pin.msg));
|
||||
} with { extension "prototype(fast)" };
|
||||
|
||||
type port STATSD_CODEC_PT message {
|
||||
out StatsD_Send;
|
||||
in StatsD_RecvFrom,
|
||||
ASP_ConnId_ReadyToRelease,
|
||||
ASP_Event;
|
||||
} with { extension "user IPL4asp_PT
|
||||
out(StatsD_Send -> ASP_Send: function(StatsD_to_IPL4_Send))
|
||||
in(ASP_RecvFrom -> StatsD_RecvFrom: function(IPL4_to_StatsD_RecvFrom);
|
||||
ASP_ConnId_ReadyToRelease -> ASP_ConnId_ReadyToRelease: simple;
|
||||
ASP_Event -> ASP_Event: simple)"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
module StatsD_CodecPort_CtrlFunct {
|
||||
|
||||
import from StatsD_CodecPort all;
|
||||
import from IPL4asp_Types all;
|
||||
|
||||
external function f_IPL4_listen(
|
||||
inout STATSD_CODEC_PT portRef,
|
||||
in HostName locName,
|
||||
in PortNumber locPort,
|
||||
in ProtoTuple proto,
|
||||
in OptionList options := {}
|
||||
) return Result;
|
||||
|
||||
external function f_IPL4_connect(
|
||||
inout STATSD_CODEC_PT portRef,
|
||||
in HostName remName,
|
||||
in PortNumber remPort,
|
||||
in HostName locName,
|
||||
in PortNumber locPort,
|
||||
in ConnectionId connId,
|
||||
in ProtoTuple proto,
|
||||
in OptionList options := {}
|
||||
) return Result;
|
||||
|
||||
external function f_IPL4_close(
|
||||
inout STATSD_CODEC_PT portRef,
|
||||
in ConnectionId id,
|
||||
in ProtoTuple proto := { unspecified := {} }
|
||||
) return Result;
|
||||
|
||||
external function f_IPL4_setUserData(
|
||||
inout STATSD_CODEC_PT portRef,
|
||||
in ConnectionId id,
|
||||
in UserData userData
|
||||
) return Result;
|
||||
|
||||
external function f_IPL4_getUserData(
|
||||
inout STATSD_CODEC_PT portRef,
|
||||
in ConnectionId id,
|
||||
out UserData userData
|
||||
) return Result;
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
#include "IPL4asp_PortType.hh"
|
||||
#include "IPL4asp_PT.hh"
|
||||
#include "StatsD_CodecPort.hh"
|
||||
|
||||
namespace StatsD__CodecPort__CtrlFunct {
|
||||
|
||||
IPL4asp__Types::Result f__IPL4__listen(
|
||||
StatsD__CodecPort::STATSD__CODEC__PT& portRef,
|
||||
const IPL4asp__Types::HostName& locName,
|
||||
const IPL4asp__Types::PortNumber& locPort,
|
||||
const IPL4asp__Types::ProtoTuple& proto,
|
||||
const IPL4asp__Types::OptionList& options)
|
||||
{
|
||||
return f__IPL4__PROVIDER__listen(portRef, locName, locPort, proto, options);
|
||||
}
|
||||
|
||||
IPL4asp__Types::Result f__IPL4__connect(
|
||||
StatsD__CodecPort::STATSD__CODEC__PT& portRef,
|
||||
const IPL4asp__Types::HostName& remName,
|
||||
const IPL4asp__Types::PortNumber& remPort,
|
||||
const IPL4asp__Types::HostName& locName,
|
||||
const IPL4asp__Types::PortNumber& locPort,
|
||||
const IPL4asp__Types::ConnectionId& connId,
|
||||
const IPL4asp__Types::ProtoTuple& proto,
|
||||
const IPL4asp__Types::OptionList& options)
|
||||
{
|
||||
return f__IPL4__PROVIDER__connect(portRef, remName, remPort,
|
||||
locName, locPort, connId, proto, options);
|
||||
}
|
||||
|
||||
IPL4asp__Types::Result f__IPL4__close(
|
||||
StatsD__CodecPort::STATSD__CODEC__PT& portRef,
|
||||
const IPL4asp__Types::ConnectionId& connId,
|
||||
const IPL4asp__Types::ProtoTuple& proto)
|
||||
{
|
||||
return f__IPL4__PROVIDER__close(portRef, connId, proto);
|
||||
}
|
||||
|
||||
IPL4asp__Types::Result f__IPL4__setUserData(
|
||||
StatsD__CodecPort::STATSD__CODEC__PT& portRef,
|
||||
const IPL4asp__Types::ConnectionId& connId,
|
||||
const IPL4asp__Types::UserData& userData)
|
||||
{
|
||||
return f__IPL4__PROVIDER__setUserData(portRef, connId, userData);
|
||||
}
|
||||
|
||||
IPL4asp__Types::Result f__IPL4__getUserData(
|
||||
StatsD__CodecPort::STATSD__CODEC__PT& portRef,
|
||||
const IPL4asp__Types::ConnectionId& connId,
|
||||
IPL4asp__Types::UserData& userData)
|
||||
{
|
||||
return f__IPL4__PROVIDER__getUserData(portRef, connId, userData);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
module StatsD_Types {
|
||||
|
||||
/* Definition of abstract types for the StatsD protocol. USes the TITAN "TEXT"
|
||||
* codec to auto-generate encoder/decoder functions
|
||||
*
|
||||
* (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
|
||||
*/
|
||||
|
||||
type charstring MetricName with {
|
||||
variant "END(':')";
|
||||
};
|
||||
|
||||
type integer MetricValue with {
|
||||
variant "END('|', '\|#(1)')";
|
||||
};
|
||||
|
||||
type charstring MetricType (pattern "(g|c|ms|h|m)");
|
||||
|
||||
type charstring MetricSampleRate (pattern "\d.\d+") with {
|
||||
variant "BEGIN('|@')"
|
||||
};
|
||||
|
||||
type record StatsDMetric {
|
||||
MetricName name,
|
||||
MetricValue val,
|
||||
MetricType mtype,
|
||||
MetricSampleRate srate optional
|
||||
};
|
||||
|
||||
type record of StatsDMetric StatsDMessage with {
|
||||
variant "SEPARATOR('\n')";
|
||||
};
|
||||
|
||||
external function enc_StatsDMessage(in StatsDMessage id) return charstring
|
||||
with { extension "prototype(convert) encode(TEXT)"};
|
||||
|
||||
external function dec_StatsDMessage(in charstring id) return StatsDMessage
|
||||
with { extension "prototype(convert) decode(TEXT)"};
|
||||
|
||||
template StatsDMessage tr_StatsDMsg1(template StatsDMetric metric) := {
|
||||
[0] := metric
|
||||
}
|
||||
|
||||
template StatsDMetric tr_StatsDMetric(template MetricName name, template MetricValue val := ?, template MetricType mtype) := {
|
||||
name := name,
|
||||
val := val,
|
||||
mtype := mtype
|
||||
}
|
||||
|
||||
template StatsDMetric tr_StatsDMetricCounter(template MetricName name, template MetricValue val := ?) := {
|
||||
name := name,
|
||||
val := val,
|
||||
mtype := "c"
|
||||
}
|
||||
|
||||
template StatsDMetric tr_StatsDMetricGauge(template MetricName name, template MetricValue val := ?) := {
|
||||
name := name,
|
||||
val := val,
|
||||
mtype := "g"
|
||||
}
|
||||
|
||||
} with { encode "TEXT" }
|
Loading…
Reference in New Issue