@vpalmisano/webrtcperf 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +296 -0
- package/app.min.js +2 -0
- package/build/src/app.d.ts +6 -0
- package/build/src/app.js +207 -0
- package/build/src/app.js.map +1 -0
- package/build/src/config.d.ts +104 -0
- package/build/src/config.js +880 -0
- package/build/src/config.js.map +1 -0
- package/build/src/generate-config-docs.d.ts +1 -0
- package/build/src/generate-config-docs.js +41 -0
- package/build/src/generate-config-docs.js.map +1 -0
- package/build/src/index.d.ts +9 -0
- package/build/src/index.js +26 -0
- package/build/src/index.js.map +1 -0
- package/build/src/media.d.ts +33 -0
- package/build/src/media.js +113 -0
- package/build/src/media.js.map +1 -0
- package/build/src/rtcstats.d.ts +302 -0
- package/build/src/rtcstats.js +418 -0
- package/build/src/rtcstats.js.map +1 -0
- package/build/src/server.d.ts +173 -0
- package/build/src/server.js +639 -0
- package/build/src/server.js.map +1 -0
- package/build/src/session.d.ts +277 -0
- package/build/src/session.js +1552 -0
- package/build/src/session.js.map +1 -0
- package/build/src/stats.d.ts +243 -0
- package/build/src/stats.js +1383 -0
- package/build/src/stats.js.map +1 -0
- package/build/src/utils.d.ts +249 -0
- package/build/src/utils.js +1220 -0
- package/build/src/utils.js.map +1 -0
- package/build/src/visqol.d.ts +6 -0
- package/build/src/visqol.js +61 -0
- package/build/src/visqol.js.map +1 -0
- package/build/src/vmaf.d.ts +83 -0
- package/build/src/vmaf.js +624 -0
- package/build/src/vmaf.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +129 -0
- package/src/app.ts +241 -0
- package/src/config.ts +852 -0
- package/src/generate-config-docs.ts +47 -0
- package/src/index.ts +9 -0
- package/src/media.ts +151 -0
- package/src/rtcstats.ts +507 -0
- package/src/server.ts +645 -0
- package/src/session.ts +1908 -0
- package/src/stats.ts +1668 -0
- package/src/utils.ts +1295 -0
- package/src/visqol.ts +62 -0
- package/src/vmaf.ts +771 -0
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Stats = exports.FastStats = void 0;
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const events = __importStar(require("events"));
|
|
43
|
+
const fast_stats_1 = require("fast-stats");
|
|
44
|
+
Object.defineProperty(exports, "FastStats", { enumerable: true, get: function () { return fast_stats_1.Stats; } });
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const http = __importStar(require("http"));
|
|
47
|
+
const https = __importStar(require("https"));
|
|
48
|
+
const json5_1 = __importDefault(require("json5"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const promClient = __importStar(require("prom-client"));
|
|
51
|
+
const sprintf_js_1 = require("sprintf-js");
|
|
52
|
+
const zlib = __importStar(require("zlib"));
|
|
53
|
+
const rtcstats_1 = require("./rtcstats");
|
|
54
|
+
const utils_1 = require("./utils");
|
|
55
|
+
const log = (0, utils_1.logger)('webrtcperf:stats');
|
|
56
|
+
function calculateFailAmountPercentile(stat, percentile = 95) {
|
|
57
|
+
return Math.round(stat.percentile(percentile));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* StatsWriter
|
|
61
|
+
*/
|
|
62
|
+
class StatsWriter {
|
|
63
|
+
fname;
|
|
64
|
+
columns;
|
|
65
|
+
_header_written = false;
|
|
66
|
+
constructor(fname = 'stats.log', columns) {
|
|
67
|
+
this.fname = fname;
|
|
68
|
+
this.columns = columns;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* push
|
|
72
|
+
* @param dataColumns
|
|
73
|
+
*/
|
|
74
|
+
async push(dataColumns) {
|
|
75
|
+
if (!this._header_written) {
|
|
76
|
+
const data = ['datetime', ...this.columns].join(',') + '\n';
|
|
77
|
+
await fs.promises.mkdir(path.dirname(this.fname), { recursive: true });
|
|
78
|
+
await fs.promises.writeFile(this.fname, data);
|
|
79
|
+
this._header_written = true;
|
|
80
|
+
}
|
|
81
|
+
//
|
|
82
|
+
const data = [Date.now(), ...dataColumns].join(',') + '\n';
|
|
83
|
+
return fs.promises.appendFile(this.fname, data);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* formatStatsColumns
|
|
88
|
+
* @param column
|
|
89
|
+
*/
|
|
90
|
+
function formatStatsColumns(column) {
|
|
91
|
+
return [
|
|
92
|
+
`${column}_length`,
|
|
93
|
+
`${column}_sum`,
|
|
94
|
+
`${column}_mean`,
|
|
95
|
+
`${column}_stdev`,
|
|
96
|
+
`${column}_5p`,
|
|
97
|
+
`${column}_95p`,
|
|
98
|
+
`${column}_min`,
|
|
99
|
+
`${column}_max`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Formats the stats for console or for file output.
|
|
104
|
+
* @param s The stats object.
|
|
105
|
+
* @param forWriter If true, format the stats to be written on file.
|
|
106
|
+
*/
|
|
107
|
+
function formatStats(s, forWriter = false) {
|
|
108
|
+
if (forWriter) {
|
|
109
|
+
return [
|
|
110
|
+
(0, utils_1.toPrecision)(s.length || 0, 0),
|
|
111
|
+
(0, utils_1.toPrecision)(s.sum || 0),
|
|
112
|
+
(0, utils_1.toPrecision)(s.amean() || 0),
|
|
113
|
+
(0, utils_1.toPrecision)(s.stddev() || 0),
|
|
114
|
+
(0, utils_1.toPrecision)(s.percentile(5) || 0),
|
|
115
|
+
(0, utils_1.toPrecision)(s.percentile(95) || 0),
|
|
116
|
+
(0, utils_1.toPrecision)(s.min || 0),
|
|
117
|
+
(0, utils_1.toPrecision)(s.max || 0),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
length: s.length || 0,
|
|
122
|
+
sum: s.sum || 0,
|
|
123
|
+
mean: s.amean() || 0,
|
|
124
|
+
stddev: s.stddev() || 0,
|
|
125
|
+
p5: s.percentile(5) || 0,
|
|
126
|
+
p95: s.percentile(95) || 0,
|
|
127
|
+
min: s.min || 0,
|
|
128
|
+
max: s.max || 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Formats the console stats title.
|
|
133
|
+
* @param name
|
|
134
|
+
*/
|
|
135
|
+
function sprintfStatsTitle(name) {
|
|
136
|
+
return (0, sprintf_js_1.sprintf)((0, chalk_1.default) `-- {bold %(name)s} %(fill)s\n`, {
|
|
137
|
+
name,
|
|
138
|
+
fill: '-'.repeat(100 - name.length - 4),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Formats the console stats header.
|
|
143
|
+
*/
|
|
144
|
+
function sprintfStatsHeader() {
|
|
145
|
+
return (sprintfStatsTitle(new Date().toUTCString()) +
|
|
146
|
+
(0, sprintf_js_1.sprintf)((0, chalk_1.default) `{bold %(name)\' 30s} {bold %(length)\' 8s} {bold %(sum)\' 8s} {bold %(mean)\' 8s} {bold %(stddev)\' 8s} {bold %(p5)\' 8s} {bold %(p95)\' 8s} {bold %(min)\' 8s} {bold %(max)\' 8s}\n`, {
|
|
147
|
+
name: 'name',
|
|
148
|
+
length: 'count',
|
|
149
|
+
sum: 'sum',
|
|
150
|
+
mean: 'mean',
|
|
151
|
+
stddev: 'stddev',
|
|
152
|
+
p5: '5p',
|
|
153
|
+
p95: '95p',
|
|
154
|
+
min: 'min',
|
|
155
|
+
max: 'max',
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Format the stats for console output.
|
|
160
|
+
*/
|
|
161
|
+
function sprintfStats(name, stats, format = '.2f', unit = '', scale = 1, hideSum = false) {
|
|
162
|
+
if (!stats?.all.length) {
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
if (!scale) {
|
|
166
|
+
scale = 1;
|
|
167
|
+
}
|
|
168
|
+
const statsData = formatStats(stats.all);
|
|
169
|
+
return (0, sprintf_js_1.sprintf)((0, chalk_1.default) `{red {bold %(name)\' 30s}}` +
|
|
170
|
+
(0, chalk_1.default) ` {bold %(length)\' 8d}` +
|
|
171
|
+
(hideSum ? ' ' : (0, chalk_1.default) ` {bold %(sum)\' 8${format}}`) +
|
|
172
|
+
(0, chalk_1.default) ` {bold %(mean)\' 8${format}}` +
|
|
173
|
+
(0, chalk_1.default) ` {bold %(stddev)\' 8${format}}` +
|
|
174
|
+
(0, chalk_1.default) ` {bold %(p5)\' 8${format}}` +
|
|
175
|
+
(0, chalk_1.default) ` {bold %(p95)\' 8${format}}` +
|
|
176
|
+
(0, chalk_1.default) ` {bold %(min)\' 8${format}}` +
|
|
177
|
+
(0, chalk_1.default) ` {bold %(max)\' 8${format}}%(unit)s\n`, {
|
|
178
|
+
name,
|
|
179
|
+
length: statsData.length,
|
|
180
|
+
sum: statsData.sum * scale,
|
|
181
|
+
mean: statsData.mean * scale,
|
|
182
|
+
stddev: statsData.stddev * scale,
|
|
183
|
+
p5: statsData.p5 * scale,
|
|
184
|
+
p95: statsData.p95 * scale,
|
|
185
|
+
min: statsData.min * scale,
|
|
186
|
+
max: statsData.max * scale,
|
|
187
|
+
unit: unit ? (0, chalk_1.default) ` {red {bold ${unit}}}` : '',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const promPrefix = 'wst_';
|
|
191
|
+
const promCreateGauge = (register, name, suffix = '', labelNames = [], collect) => {
|
|
192
|
+
return new promClient.Gauge({
|
|
193
|
+
name: `${promPrefix}${name}${suffix && '_' + suffix}`,
|
|
194
|
+
help: `${name} ${suffix}`,
|
|
195
|
+
labelNames,
|
|
196
|
+
registers: [register],
|
|
197
|
+
collect,
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
const calculateFailAmount = (checkValue, ruleValue) => {
|
|
201
|
+
if (ruleValue) {
|
|
202
|
+
return 100 * Math.min(1, Math.abs(checkValue - ruleValue) / ruleValue);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
return 100 * Math.min(1, Math.abs(checkValue));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* The Stats collector class.
|
|
210
|
+
*/
|
|
211
|
+
class Stats extends events.EventEmitter {
|
|
212
|
+
statsPath;
|
|
213
|
+
detailedStatsPath;
|
|
214
|
+
prometheusPushgateway;
|
|
215
|
+
prometheusPushgatewayJobName;
|
|
216
|
+
prometheusPushgatewayAuth;
|
|
217
|
+
prometheusPushgatewayGzip;
|
|
218
|
+
showStats;
|
|
219
|
+
showPageLog;
|
|
220
|
+
statsInterval;
|
|
221
|
+
rtcStatsTimeout;
|
|
222
|
+
customMetrics = {};
|
|
223
|
+
startTimestamp;
|
|
224
|
+
enableDetailedStats;
|
|
225
|
+
startTimestampString;
|
|
226
|
+
sessions = new Map();
|
|
227
|
+
nextSessionId;
|
|
228
|
+
statsWriter;
|
|
229
|
+
detailedStatsWriter;
|
|
230
|
+
scheduler;
|
|
231
|
+
alertRules = null;
|
|
232
|
+
alertRulesFilename;
|
|
233
|
+
alertRulesFailPercentile;
|
|
234
|
+
pushStatsUrl;
|
|
235
|
+
pushStatsId;
|
|
236
|
+
serverSecret;
|
|
237
|
+
alertRulesReport = new Map();
|
|
238
|
+
gateway = null;
|
|
239
|
+
/* metricConfigGauge: promClient.Gauge<string> | null = null */
|
|
240
|
+
elapsedTimeMetric = null;
|
|
241
|
+
metrics = {};
|
|
242
|
+
alertTagsMetrics;
|
|
243
|
+
customMetricsLabels;
|
|
244
|
+
collectedStats;
|
|
245
|
+
collectedStatsConfig = {
|
|
246
|
+
url: '',
|
|
247
|
+
pages: 0,
|
|
248
|
+
startTime: 0,
|
|
249
|
+
};
|
|
250
|
+
externalCollectedStats = new Map();
|
|
251
|
+
pushStatsInstance = null;
|
|
252
|
+
running = false;
|
|
253
|
+
/**
|
|
254
|
+
* Stats aggregator class.
|
|
255
|
+
*/
|
|
256
|
+
constructor({ statsPath, detailedStatsPath, prometheusPushgateway, prometheusPushgatewayJobName, prometheusPushgatewayAuth, prometheusPushgatewayGzip, showStats, showPageLog, statsInterval, rtcStatsTimeout, customMetrics, alertRules, alertRulesFilename, alertRulesFailPercentile, pushStatsUrl, pushStatsId, serverSecret, startSessionId, startTimestamp, enableDetailedStats, customMetricsLabels, }) {
|
|
257
|
+
super();
|
|
258
|
+
this.statsPath = statsPath;
|
|
259
|
+
this.detailedStatsPath = detailedStatsPath;
|
|
260
|
+
this.prometheusPushgateway = prometheusPushgateway;
|
|
261
|
+
this.prometheusPushgatewayJobName = prometheusPushgatewayJobName || 'default';
|
|
262
|
+
this.prometheusPushgatewayAuth = prometheusPushgatewayAuth || undefined;
|
|
263
|
+
this.prometheusPushgatewayGzip = prometheusPushgatewayGzip;
|
|
264
|
+
this.showStats = showStats !== undefined ? showStats : true;
|
|
265
|
+
this.showPageLog = !!showPageLog;
|
|
266
|
+
this.statsInterval = statsInterval || 10;
|
|
267
|
+
this.rtcStatsTimeout = Math.max(rtcStatsTimeout, this.statsInterval);
|
|
268
|
+
if (customMetrics.trim()) {
|
|
269
|
+
this.customMetrics = json5_1.default.parse(customMetrics);
|
|
270
|
+
log.debug(`using customMetrics: ${JSON.stringify(this.customMetrics, undefined, 2)}`);
|
|
271
|
+
}
|
|
272
|
+
this.collectedStats = this.initCollectedStats();
|
|
273
|
+
this.sessions = new Map();
|
|
274
|
+
this.nextSessionId = startSessionId;
|
|
275
|
+
this.startTimestamp = startTimestamp || Date.now();
|
|
276
|
+
this.startTimestampString = new Date(this.startTimestamp).toISOString();
|
|
277
|
+
this.enableDetailedStats = enableDetailedStats;
|
|
278
|
+
this.customMetricsLabels = customMetricsLabels
|
|
279
|
+
? customMetricsLabels.split(',').reduce((p, label) => {
|
|
280
|
+
label = label.trim();
|
|
281
|
+
if (label) {
|
|
282
|
+
p[label] = undefined;
|
|
283
|
+
}
|
|
284
|
+
return p;
|
|
285
|
+
}, {})
|
|
286
|
+
: {};
|
|
287
|
+
this.statsWriter = null;
|
|
288
|
+
this.detailedStatsWriter = null;
|
|
289
|
+
if (alertRules.trim()) {
|
|
290
|
+
this.alertRules = json5_1.default.parse(alertRules);
|
|
291
|
+
log.debug(`using alertRules: ${JSON.stringify(this.alertRules, undefined, 2)}`);
|
|
292
|
+
}
|
|
293
|
+
this.alertRulesFilename = alertRulesFilename;
|
|
294
|
+
this.alertRulesFailPercentile = alertRulesFailPercentile;
|
|
295
|
+
this.pushStatsUrl = pushStatsUrl;
|
|
296
|
+
this.pushStatsId = pushStatsId;
|
|
297
|
+
this.serverSecret = serverSecret;
|
|
298
|
+
if (this.pushStatsUrl) {
|
|
299
|
+
const httpAgent = new http.Agent({ keepAlive: false });
|
|
300
|
+
const httpsAgent = new https.Agent({
|
|
301
|
+
keepAlive: false,
|
|
302
|
+
rejectUnauthorized: false,
|
|
303
|
+
});
|
|
304
|
+
this.pushStatsInstance = axios_1.default.create({
|
|
305
|
+
httpAgent,
|
|
306
|
+
httpsAgent,
|
|
307
|
+
baseURL: this.pushStatsUrl,
|
|
308
|
+
auth: {
|
|
309
|
+
username: 'admin',
|
|
310
|
+
password: this.serverSecret,
|
|
311
|
+
},
|
|
312
|
+
maxBodyLength: 20000000,
|
|
313
|
+
transformRequest: [
|
|
314
|
+
...axios_1.default.defaults.transformRequest,
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
316
|
+
(data, headers) => {
|
|
317
|
+
if (headers && typeof data === 'string' && data.length > 16 * 1024) {
|
|
318
|
+
headers['Content-Encoding'] = 'gzip';
|
|
319
|
+
return zlib.gzipSync(data);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
return data;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
initCollectedStats() {
|
|
330
|
+
return this.statsNames.reduce((prev, name) => {
|
|
331
|
+
prev[name] = {
|
|
332
|
+
all: new fast_stats_1.Stats(),
|
|
333
|
+
byHost: {},
|
|
334
|
+
byCodec: {},
|
|
335
|
+
byParticipantAndTrack: {},
|
|
336
|
+
};
|
|
337
|
+
return prev;
|
|
338
|
+
}, {});
|
|
339
|
+
}
|
|
340
|
+
get statsNames() {
|
|
341
|
+
return Object.keys(rtcstats_1.PageStatsNames).concat(Object.keys(rtcstats_1.RtcStatsMetricNames)).concat(Object.keys(this.customMetrics));
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* consumeSessionId
|
|
345
|
+
* @param tabs the number of tabs to allocate in the same session.
|
|
346
|
+
*/
|
|
347
|
+
consumeSessionId(tabs = 1) {
|
|
348
|
+
const id = this.nextSessionId;
|
|
349
|
+
this.nextSessionId += tabs;
|
|
350
|
+
return id;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Adds the session to the list of monitored sessions.
|
|
354
|
+
*/
|
|
355
|
+
addSession(session) {
|
|
356
|
+
log.debug(`addSession ${session.id}`);
|
|
357
|
+
if (this.sessions.has(session.id)) {
|
|
358
|
+
throw new Error(`session id ${session.id} already present`);
|
|
359
|
+
}
|
|
360
|
+
session.once('stop', id => {
|
|
361
|
+
log.debug(`Session ${id} stopped`);
|
|
362
|
+
this.sessions.delete(id);
|
|
363
|
+
});
|
|
364
|
+
this.sessions.set(session.id, session);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Removes the session from list of monitored sessions.
|
|
368
|
+
* @param id the Session id
|
|
369
|
+
*/
|
|
370
|
+
removeSession(id) {
|
|
371
|
+
log.debug(`removeSession ${id}`);
|
|
372
|
+
this.sessions.delete(id);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* It updates the custom label value.
|
|
376
|
+
* @param label the custom metric label
|
|
377
|
+
* @param value the custom metric label value
|
|
378
|
+
*/
|
|
379
|
+
setCustomMetricLabel(label, value) {
|
|
380
|
+
if (!(label in this.customMetricsLabels)) {
|
|
381
|
+
throw new Error(`Unknown custom metric label: ${label}`);
|
|
382
|
+
}
|
|
383
|
+
this.customMetricsLabels[label] = value;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* start
|
|
387
|
+
*/
|
|
388
|
+
async start() {
|
|
389
|
+
if (this.running) {
|
|
390
|
+
log.warn('already running');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
log.debug('start');
|
|
394
|
+
this.running = true;
|
|
395
|
+
if (this.statsPath) {
|
|
396
|
+
log.debug(`Logging stats into ${this.statsPath}`);
|
|
397
|
+
const headers = this.statsNames.reduce((v, name) => v.concat(formatStatsColumns(name)), []);
|
|
398
|
+
this.statsWriter = new StatsWriter(this.statsPath, headers);
|
|
399
|
+
}
|
|
400
|
+
if (this.detailedStatsPath) {
|
|
401
|
+
log.debug(`Logging stats into ${this.statsPath}`);
|
|
402
|
+
this.detailedStatsWriter = new StatsWriter(this.detailedStatsPath, [
|
|
403
|
+
'participantName',
|
|
404
|
+
'trackId',
|
|
405
|
+
...this.statsNames,
|
|
406
|
+
]);
|
|
407
|
+
}
|
|
408
|
+
if (this.prometheusPushgateway) {
|
|
409
|
+
const register = new promClient.Registry();
|
|
410
|
+
const agent = this.prometheusPushgateway.startsWith('https://')
|
|
411
|
+
? new https.Agent({
|
|
412
|
+
keepAlive: true,
|
|
413
|
+
keepAliveMsecs: 60000,
|
|
414
|
+
maxSockets: 5,
|
|
415
|
+
})
|
|
416
|
+
: new http.Agent({
|
|
417
|
+
keepAlive: true,
|
|
418
|
+
keepAliveMsecs: 60000,
|
|
419
|
+
maxSockets: 5,
|
|
420
|
+
});
|
|
421
|
+
this.gateway = new promClient.Pushgateway(this.prometheusPushgateway, {
|
|
422
|
+
timeout: 5000,
|
|
423
|
+
auth: this.prometheusPushgatewayAuth,
|
|
424
|
+
rejectUnauthorized: false,
|
|
425
|
+
agent,
|
|
426
|
+
headers: this.prometheusPushgatewayGzip
|
|
427
|
+
? {
|
|
428
|
+
'Content-Encoding': 'gzip',
|
|
429
|
+
}
|
|
430
|
+
: undefined,
|
|
431
|
+
}, register);
|
|
432
|
+
// promClient.collectDefaultMetrics({ prefix: promPrefix, register })
|
|
433
|
+
this.elapsedTimeMetric = promCreateGauge(register, 'elapsedTime', '', ['datetime', ...Object.keys(this.customMetricsLabels)], () => this.elapsedTimeMetric?.set({
|
|
434
|
+
datetime: this.startTimestampString,
|
|
435
|
+
...this.customMetricsLabels,
|
|
436
|
+
}, (Date.now() - this.startTimestamp) / 1000));
|
|
437
|
+
// Export rtc stats.
|
|
438
|
+
this.statsNames.forEach(name => {
|
|
439
|
+
this.metrics[name] = {
|
|
440
|
+
length: promCreateGauge(register, name, 'length', [
|
|
441
|
+
'host',
|
|
442
|
+
'codec',
|
|
443
|
+
'datetime',
|
|
444
|
+
...Object.keys(this.customMetricsLabels),
|
|
445
|
+
]),
|
|
446
|
+
sum: promCreateGauge(register, name, 'sum', [
|
|
447
|
+
'host',
|
|
448
|
+
'codec',
|
|
449
|
+
'datetime',
|
|
450
|
+
...Object.keys(this.customMetricsLabels),
|
|
451
|
+
]),
|
|
452
|
+
mean: promCreateGauge(register, name, 'mean', [
|
|
453
|
+
'host',
|
|
454
|
+
'codec',
|
|
455
|
+
'datetime',
|
|
456
|
+
...Object.keys(this.customMetricsLabels),
|
|
457
|
+
]),
|
|
458
|
+
stddev: promCreateGauge(register, name, 'stddev', [
|
|
459
|
+
'host',
|
|
460
|
+
'codec',
|
|
461
|
+
'datetime',
|
|
462
|
+
...Object.keys(this.customMetricsLabels),
|
|
463
|
+
]),
|
|
464
|
+
p5: promCreateGauge(register, name, 'p5', [
|
|
465
|
+
'host',
|
|
466
|
+
'codec',
|
|
467
|
+
'datetime',
|
|
468
|
+
...Object.keys(this.customMetricsLabels),
|
|
469
|
+
]),
|
|
470
|
+
p95: promCreateGauge(register, name, 'p95', [
|
|
471
|
+
'host',
|
|
472
|
+
'codec',
|
|
473
|
+
'datetime',
|
|
474
|
+
...Object.keys(this.customMetricsLabels),
|
|
475
|
+
]),
|
|
476
|
+
min: promCreateGauge(register, name, 'min', [
|
|
477
|
+
'host',
|
|
478
|
+
'codec',
|
|
479
|
+
'datetime',
|
|
480
|
+
...Object.keys(this.customMetricsLabels),
|
|
481
|
+
]),
|
|
482
|
+
max: promCreateGauge(register, name, 'max', [
|
|
483
|
+
'host',
|
|
484
|
+
'codec',
|
|
485
|
+
'datetime',
|
|
486
|
+
...Object.keys(this.customMetricsLabels),
|
|
487
|
+
]),
|
|
488
|
+
alertRules: {},
|
|
489
|
+
};
|
|
490
|
+
if (this.enableDetailedStats !== false) {
|
|
491
|
+
this.metrics[name].value = promCreateGauge(register, name, '', [
|
|
492
|
+
'participantName',
|
|
493
|
+
'trackId',
|
|
494
|
+
'datetime',
|
|
495
|
+
...Object.keys(this.customMetricsLabels),
|
|
496
|
+
]);
|
|
497
|
+
}
|
|
498
|
+
if (this.alertRules && this.alertRules[name]) {
|
|
499
|
+
const rule = this.alertRules[name];
|
|
500
|
+
for (const ruleKey of Object.keys(rule)) {
|
|
501
|
+
const ruleName = `alert_${name}_${ruleKey}`;
|
|
502
|
+
this.metrics[name].alertRules[ruleName] = {
|
|
503
|
+
report: promCreateGauge(register, ruleName, 'report', [
|
|
504
|
+
'rule',
|
|
505
|
+
'datetime',
|
|
506
|
+
...Object.keys(this.customMetricsLabels),
|
|
507
|
+
]),
|
|
508
|
+
rule: promCreateGauge(register, ruleName, '', [
|
|
509
|
+
'rule',
|
|
510
|
+
'datetime',
|
|
511
|
+
...Object.keys(this.customMetricsLabels),
|
|
512
|
+
]),
|
|
513
|
+
mean: promCreateGauge(register, ruleName, 'mean', [
|
|
514
|
+
'rule',
|
|
515
|
+
'datetime',
|
|
516
|
+
...Object.keys(this.customMetricsLabels),
|
|
517
|
+
]),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
if (this.alertRules) {
|
|
523
|
+
this.alertTagsMetrics = promCreateGauge(register, `alert_report`, '', [
|
|
524
|
+
'datetime',
|
|
525
|
+
'tag',
|
|
526
|
+
...Object.keys(this.customMetricsLabels),
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
await this.deletePushgatewayStats();
|
|
530
|
+
}
|
|
531
|
+
this.scheduler = new utils_1.Scheduler('stats', this.statsInterval, this.collectStats.bind(this));
|
|
532
|
+
this.scheduler.start();
|
|
533
|
+
}
|
|
534
|
+
async deletePushgatewayStats() {
|
|
535
|
+
if (!this.gateway) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const { resp, body } = await this.gateway.delete({
|
|
540
|
+
jobName: this.prometheusPushgatewayJobName,
|
|
541
|
+
});
|
|
542
|
+
if (body.length) {
|
|
543
|
+
log.warn(`Pushgateway delete error ${resp.statusCode}: ${body}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
log.error(`Pushgateway delete error: ${err.stack}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* collectStats
|
|
552
|
+
*/
|
|
553
|
+
async collectStats(now) {
|
|
554
|
+
if (!this.running) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// log.debug(`statsInterval ${this.sessions.size} sessions`);
|
|
558
|
+
if (!this.sessions.size && !this.externalCollectedStats.size) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// Prepare config.
|
|
562
|
+
this.collectedStatsConfig.pages = 0;
|
|
563
|
+
this.collectedStatsConfig.startTime = this.startTimestamp;
|
|
564
|
+
// Reset collectedStats object.
|
|
565
|
+
Object.values(this.collectedStats).forEach(stats => {
|
|
566
|
+
stats.all.reset();
|
|
567
|
+
Object.values(stats.byHost).forEach(s => s.reset());
|
|
568
|
+
Object.values(stats.byCodec).forEach(s => s.reset());
|
|
569
|
+
stats.byParticipantAndTrack = {};
|
|
570
|
+
});
|
|
571
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
572
|
+
this.collectedStatsConfig.url = `${(0, utils_1.hideAuth)(session.url)}?${session.urlQuery}`;
|
|
573
|
+
this.collectedStatsConfig.pages += session.pages.size || 0;
|
|
574
|
+
const sessionStats = await session.updateStats();
|
|
575
|
+
for (const [name, obj] of Object.entries(sessionStats)) {
|
|
576
|
+
if (obj === undefined) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
//log.log(name, obj)
|
|
580
|
+
try {
|
|
581
|
+
const collectedStats = this.collectedStats[name];
|
|
582
|
+
if (typeof obj === 'number' && isFinite(obj)) {
|
|
583
|
+
collectedStats.all.push(obj);
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
587
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
588
|
+
if (typeof value === 'number' && isFinite(value)) {
|
|
589
|
+
collectedStats.all.push(value);
|
|
590
|
+
// Push host label.
|
|
591
|
+
const { trackId, hostName, participantName } = (0, rtcstats_1.parseRtStatKey)(key);
|
|
592
|
+
let stats = collectedStats.byHost[hostName];
|
|
593
|
+
if (!stats) {
|
|
594
|
+
stats = collectedStats.byHost[hostName] = new fast_stats_1.Stats();
|
|
595
|
+
}
|
|
596
|
+
stats.push(value);
|
|
597
|
+
// Push participant and track values.
|
|
598
|
+
if ((0, utils_1.enabledForSession)(sessionId, this.enableDetailedStats) && participantName) {
|
|
599
|
+
collectedStats.byParticipantAndTrack[`${participantName}:${trackId || ''}`] = value;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else if (typeof value === 'string') {
|
|
603
|
+
// Codec stats.
|
|
604
|
+
collectedStats.all.push(1);
|
|
605
|
+
let stats = collectedStats.byCodec[value];
|
|
606
|
+
if (!stats) {
|
|
607
|
+
stats = collectedStats.byCodec[value] = new fast_stats_1.Stats();
|
|
608
|
+
}
|
|
609
|
+
stats.push(1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
log.error(`session getStats name: ${name} error: ${err.stack}`, err);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Add external collected stats.
|
|
620
|
+
for (const [id, data] of this.externalCollectedStats.entries()) {
|
|
621
|
+
const { addedTime, externalStats, config } = data;
|
|
622
|
+
if (now - addedTime > this.rtcStatsTimeout * 1000) {
|
|
623
|
+
log.debug(`remove externalCollectedStats from ${id}`);
|
|
624
|
+
this.externalCollectedStats.delete(id);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
log.debug(`add external stats from ${id}`);
|
|
628
|
+
// Add external config settings.
|
|
629
|
+
if (config.url) {
|
|
630
|
+
this.collectedStatsConfig.url = config.url;
|
|
631
|
+
}
|
|
632
|
+
if (config.pages) {
|
|
633
|
+
this.collectedStatsConfig.pages += config.pages;
|
|
634
|
+
}
|
|
635
|
+
// Add metrics.
|
|
636
|
+
this.statsNames.forEach(name => {
|
|
637
|
+
const stats = externalStats[name];
|
|
638
|
+
if (!stats) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const collectedStats = this.collectedStats[name];
|
|
642
|
+
collectedStats.all.push(stats.all);
|
|
643
|
+
Object.entries(stats.byHost).forEach(([host, values]) => {
|
|
644
|
+
if (!collectedStats.byHost[host]) {
|
|
645
|
+
collectedStats.byHost[host] = new fast_stats_1.Stats();
|
|
646
|
+
}
|
|
647
|
+
collectedStats.byHost[host].push(values);
|
|
648
|
+
});
|
|
649
|
+
Object.entries(stats.byCodec).forEach(([codec, values]) => {
|
|
650
|
+
if (!collectedStats.byCodec[codec]) {
|
|
651
|
+
collectedStats.byCodec[codec] = new fast_stats_1.Stats();
|
|
652
|
+
}
|
|
653
|
+
collectedStats.byCodec[codec].push(values);
|
|
654
|
+
});
|
|
655
|
+
Object.entries(stats.byParticipantAndTrack).forEach(([label, value]) => {
|
|
656
|
+
collectedStats.byParticipantAndTrack[label] = value;
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
this.emit('stats', this.collectedStats);
|
|
661
|
+
// Push to an external instance.
|
|
662
|
+
if (this.pushStatsInstance) {
|
|
663
|
+
const pushStats = {};
|
|
664
|
+
for (const [name, stats] of Object.entries(this.collectedStats)) {
|
|
665
|
+
pushStats[name] = {
|
|
666
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
667
|
+
all: stats.all.data,
|
|
668
|
+
byHost: {},
|
|
669
|
+
byCodec: {},
|
|
670
|
+
byParticipantAndTrack: {},
|
|
671
|
+
};
|
|
672
|
+
Object.entries(stats.byHost).forEach(([host, stat]) => {
|
|
673
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
674
|
+
pushStats[name].byHost[host] = stat.data;
|
|
675
|
+
});
|
|
676
|
+
Object.entries(stats.byCodec).forEach(([codec, stat]) => {
|
|
677
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
678
|
+
pushStats[name].byCodec[codec] = stat.data;
|
|
679
|
+
});
|
|
680
|
+
Object.entries(stats.byParticipantAndTrack).forEach(([label, value]) => {
|
|
681
|
+
pushStats[name].byParticipantAndTrack[label] = value;
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const res = await this.pushStatsInstance.put('/collected-stats', {
|
|
686
|
+
id: this.pushStatsId,
|
|
687
|
+
stats: pushStats,
|
|
688
|
+
config: this.collectedStatsConfig,
|
|
689
|
+
});
|
|
690
|
+
log.debug(`pushStats message=${res.data.message}`);
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
log.error(`pushStats error: ${err.stack}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Check alerts.
|
|
697
|
+
this.checkAlertRules();
|
|
698
|
+
// Show to console.
|
|
699
|
+
this.consoleShowStats();
|
|
700
|
+
await Promise.allSettled([
|
|
701
|
+
this.writeStats(),
|
|
702
|
+
this.writeDetailedStats(),
|
|
703
|
+
this.sendToPushGateway(),
|
|
704
|
+
this.writeAlertRulesReport(),
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
async writeStats() {
|
|
708
|
+
if (!this.statsWriter)
|
|
709
|
+
return;
|
|
710
|
+
const values = this.statsNames.reduce((v, name) => v.concat(formatStats(this.collectedStats[name].all, true)), []);
|
|
711
|
+
await this.statsWriter.push(values);
|
|
712
|
+
}
|
|
713
|
+
async writeDetailedStats() {
|
|
714
|
+
if (!this.detailedStatsWriter)
|
|
715
|
+
return;
|
|
716
|
+
const participantTrackStats = new Map();
|
|
717
|
+
Object.entries(this.collectedStats).forEach(([name, stats]) => {
|
|
718
|
+
Object.entries(stats.byParticipantAndTrack).forEach(([label, value]) => {
|
|
719
|
+
let stats = participantTrackStats.get(label);
|
|
720
|
+
if (!stats) {
|
|
721
|
+
stats = {};
|
|
722
|
+
participantTrackStats.set(label, stats);
|
|
723
|
+
}
|
|
724
|
+
stats[name] = (0, utils_1.toPrecision)(value, 6);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
for (const [label, trackStats] of participantTrackStats.entries()) {
|
|
728
|
+
const [participantName, trackId] = label.split(':', 2);
|
|
729
|
+
const values = [participantName, trackId];
|
|
730
|
+
for (const name of this.statsNames) {
|
|
731
|
+
values.push(trackStats[name] ?? '');
|
|
732
|
+
}
|
|
733
|
+
await this.detailedStatsWriter.push(values);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* addCollectedStats
|
|
738
|
+
* @param id
|
|
739
|
+
* @param externalStats
|
|
740
|
+
* @param config
|
|
741
|
+
*/
|
|
742
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
743
|
+
addExternalCollectedStats(id, externalStats, config) {
|
|
744
|
+
log.debug(`addExternalCollectedStats from ${id}`);
|
|
745
|
+
const addedTime = Date.now();
|
|
746
|
+
this.externalCollectedStats.set(id, { addedTime, externalStats, config });
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* It display stats on the console.
|
|
750
|
+
*/
|
|
751
|
+
consoleShowStats() {
|
|
752
|
+
if (!this.showStats) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const stats = this.collectedStats;
|
|
756
|
+
let out = sprintfStatsHeader() +
|
|
757
|
+
sprintfStats('System CPU', stats.usedCpu, '.2f', '%', undefined, true) +
|
|
758
|
+
sprintfStats('System GPU', stats.usedGpu, '.2f', '%', undefined, true) +
|
|
759
|
+
sprintfStats('System Memory', stats.usedMemory, '.2f', '%', undefined, true) +
|
|
760
|
+
sprintfStats('CPU/page', stats.cpu, '.2f', '%') +
|
|
761
|
+
sprintfStats('Memory/page', stats.memory, '.2f', 'MB') +
|
|
762
|
+
sprintfStats('Pages', stats.pages, 'd', '') +
|
|
763
|
+
sprintfStats('Errors', stats.errors, 'd', '') +
|
|
764
|
+
sprintfStats('Warnings', stats.warnings, 'd', '') +
|
|
765
|
+
sprintfStats('Peer Connections', stats.peerConnections, 'd', '') +
|
|
766
|
+
sprintfStats('audioSubscribeDelay', stats.audioSubscribeDelay, 'd', 'ms', undefined, true) +
|
|
767
|
+
sprintfStats('videoSubscribeDelay', stats.videoSubscribeDelay, 'd', 'ms', undefined, true) +
|
|
768
|
+
// inbound audio
|
|
769
|
+
sprintfStatsTitle('Inbound audio') +
|
|
770
|
+
sprintfStats('received', stats.audioBytesReceived, '.2f', 'MB', 1e-6) +
|
|
771
|
+
sprintfStats('rate', stats.audioRecvBitrates, '.2f', 'Kbps', 1e-3) +
|
|
772
|
+
sprintfStats('lost', stats.audioRecvPacketsLost, '.2f', '%', undefined, true) +
|
|
773
|
+
sprintfStats('jitter', stats.audioRecvJitter, '.2f', 's', undefined, true) +
|
|
774
|
+
sprintfStats('avgJitterBufferDelay', stats.audioRecvAvgJitterBufferDelay, '.2f', 'ms', 1e3, true) +
|
|
775
|
+
// inbound video
|
|
776
|
+
sprintfStatsTitle('Inbound video') +
|
|
777
|
+
sprintfStats('received', stats.videoRecvBytes, '.2f', 'MB', 1e-6) +
|
|
778
|
+
sprintfStats('decoded', stats.videoFramesDecoded, 'd', 'frames') +
|
|
779
|
+
sprintfStats('rate', stats.videoRecvBitrates, '.2f', 'Kbps', 1e-3) +
|
|
780
|
+
sprintfStats('lost', stats.videoRecvPacketsLost, '.2f', '%', undefined, true) +
|
|
781
|
+
sprintfStats('jitter', stats.videoRecvJitter, '.2f', 's', undefined, true) +
|
|
782
|
+
sprintfStats('avgJitterBufferDelay', stats.videoRecvAvgJitterBufferDelay, '.2f', 'ms', 1e3, true) +
|
|
783
|
+
sprintfStats('width', stats.videoRecvWidth, 'd', 'px', undefined, true) +
|
|
784
|
+
sprintfStats('height', stats.videoRecvHeight, 'd', 'px', undefined, true) +
|
|
785
|
+
sprintfStats('fps', stats.videoRecvFps, 'd', 'fps', undefined, true) +
|
|
786
|
+
sprintfStats('firCountSent', stats.firCountSent, 'd', '', undefined, true) +
|
|
787
|
+
sprintfStats('pliCountSent', stats.pliCountSent, 'd', '', undefined, true) +
|
|
788
|
+
// outbound audio
|
|
789
|
+
sprintfStatsTitle('Outbound audio') +
|
|
790
|
+
sprintfStats('sent', stats.audioBytesSent, '.2f', 'MB', 1e-6) +
|
|
791
|
+
sprintfStats('retransmitted', stats.audioRetransmittedBytesSent, '.2f', 'MB', 1e-6) +
|
|
792
|
+
sprintfStats('rate', stats.audioSentBitrates, '.2f', 'Kbps', 1e-3) +
|
|
793
|
+
sprintfStats('lost', stats.audioSentPacketsLost, '.2f', '%', undefined, true) +
|
|
794
|
+
sprintfStats('roundTripTime', stats.audioSentRoundTripTime, '.3f', 's', undefined, true) +
|
|
795
|
+
// outbound video
|
|
796
|
+
sprintfStatsTitle('Outbound video') +
|
|
797
|
+
sprintfStats('sent', stats.videoSentBytes, '.2f', 'MB', 1e-6) +
|
|
798
|
+
sprintfStats('retransmitted', stats.videoSentRetransmittedBytes, '.2f', 'MB', 1e-6) +
|
|
799
|
+
sprintfStats('rate', stats.videoSentBitrates, '.2f', 'Kbps', 1e-3) +
|
|
800
|
+
sprintfStats('lost', stats.videoSentPacketsLost, '.2f', '%', undefined, true) +
|
|
801
|
+
sprintfStats('roundTripTime', stats.videoSentRoundTripTime, '.3f', 's', undefined, true) +
|
|
802
|
+
sprintfStats('qualityLimitResolutionChanges', stats.videoQualityLimitationResolutionChanges, 'd', '') +
|
|
803
|
+
sprintfStats('qualityLimitationCpu', stats.videoQualityLimitationCpu, 'd', '%') +
|
|
804
|
+
sprintfStats('qualityLimitationBandwidth', stats.videoQualityLimitationBandwidth, 'd', '%') +
|
|
805
|
+
sprintfStats('sentActiveSpatialLayers', stats.videoSentActiveSpatialLayers, 'd', 'layers', undefined, true) +
|
|
806
|
+
sprintfStats('sentMaxBitrate', stats.videoSentMaxBitrate, '.2f', 'Kbps', 1e-3) +
|
|
807
|
+
sprintfStats('width', stats.videoSentWidth, 'd', 'px', undefined, true) +
|
|
808
|
+
sprintfStats('height', stats.videoSentHeight, 'd', 'px', undefined, true) +
|
|
809
|
+
sprintfStats('fps', stats.videoSentFps, 'd', 'fps', undefined, true) +
|
|
810
|
+
sprintfStats('firCountReceived', stats.videoFirCountReceived, 'd', '', undefined, true) +
|
|
811
|
+
sprintfStats('pliCountReceived', stats.videoPliCountReceived, 'd', '', undefined, true);
|
|
812
|
+
if (this.alertRules) {
|
|
813
|
+
const report = this.formatAlertRulesReport();
|
|
814
|
+
if (report.length) {
|
|
815
|
+
out += sprintfStatsTitle('Alert rules report');
|
|
816
|
+
out += report;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (!this.showPageLog) {
|
|
820
|
+
console.clear();
|
|
821
|
+
}
|
|
822
|
+
console.log(out);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* sendToPushGateway
|
|
826
|
+
*/
|
|
827
|
+
async sendToPushGateway() {
|
|
828
|
+
if (!this.gateway || !this.running) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
const elapsedSeconds = (Date.now() - this.startTimestamp) / 1000;
|
|
832
|
+
const datetime = this.startTimestampString;
|
|
833
|
+
Object.entries(this.metrics).forEach(([name, metric]) => {
|
|
834
|
+
if (!this.collectedStats[name]) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const setStats = (stats, host, codec) => {
|
|
838
|
+
const labels = { host, codec, datetime, ...this.customMetricsLabels };
|
|
839
|
+
const { length, sum, mean, stddev, p5, p95, min, max } = formatStats(stats);
|
|
840
|
+
metric.length.set(labels, length);
|
|
841
|
+
metric.sum.set(labels, sum);
|
|
842
|
+
metric.mean.set(labels, mean);
|
|
843
|
+
metric.stddev.set(labels, stddev);
|
|
844
|
+
metric.p5.set(labels, p5);
|
|
845
|
+
metric.p95.set(labels, p95);
|
|
846
|
+
metric.min.set(labels, min);
|
|
847
|
+
metric.max.set(labels, max);
|
|
848
|
+
};
|
|
849
|
+
setStats(this.collectedStats[name].all, 'all', 'all');
|
|
850
|
+
Object.entries(this.collectedStats[name].byHost).forEach(([host, stats]) => {
|
|
851
|
+
setStats(stats, host, 'all');
|
|
852
|
+
});
|
|
853
|
+
Object.entries(this.collectedStats[name].byCodec).forEach(([codec, stats]) => {
|
|
854
|
+
setStats(stats, 'all', codec);
|
|
855
|
+
});
|
|
856
|
+
if (metric.value) {
|
|
857
|
+
Object.entries(this.collectedStats[name].byParticipantAndTrack).forEach(([label, value]) => {
|
|
858
|
+
const [participantName, trackId] = label.split(':', 2);
|
|
859
|
+
metric.value?.set({
|
|
860
|
+
participantName,
|
|
861
|
+
trackId,
|
|
862
|
+
datetime,
|
|
863
|
+
...this.customMetricsLabels,
|
|
864
|
+
}, value);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
// Set alerts metrics.
|
|
868
|
+
if (this.alertRules && this.alertRules[name]) {
|
|
869
|
+
const rule = this.alertRules[name];
|
|
870
|
+
// eslint-disable-next-line prefer-const
|
|
871
|
+
for (let [ruleKey, ruleValues] of Object.entries(rule)) {
|
|
872
|
+
if (ruleKey === 'tags') {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (!Array.isArray(ruleValues)) {
|
|
876
|
+
ruleValues = [ruleValues];
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
ruleValues = ruleValues;
|
|
880
|
+
}
|
|
881
|
+
for (const ruleValue of ruleValues) {
|
|
882
|
+
// Send rule values as metrics.
|
|
883
|
+
if (ruleValue.$after !== undefined && elapsedSeconds < ruleValue.$after) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const ruleName = `alert_${name}_${ruleKey}`;
|
|
887
|
+
const ruleObj = this.metrics[name].alertRules[ruleName];
|
|
888
|
+
const remove = ruleValue.$before !== undefined && elapsedSeconds > ruleValue.$before;
|
|
889
|
+
// Send rule report as metric.
|
|
890
|
+
const ruleDesc = this.getAlertRuleDesc(ruleKey, ruleValue);
|
|
891
|
+
const report = this.alertRulesReport.get(name);
|
|
892
|
+
if (report) {
|
|
893
|
+
const ruleReport = report.get(ruleDesc);
|
|
894
|
+
if (ruleReport) {
|
|
895
|
+
const labels = {
|
|
896
|
+
rule: ruleDesc,
|
|
897
|
+
datetime,
|
|
898
|
+
...this.customMetricsLabels,
|
|
899
|
+
};
|
|
900
|
+
if (!remove) {
|
|
901
|
+
ruleObj.report.set(labels, ruleReport.failAmountPercentile);
|
|
902
|
+
ruleObj.mean.set(labels, ruleReport.valueStats.amean());
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
ruleObj.report.remove(labels);
|
|
906
|
+
ruleObj.mean.remove(labels);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
// Send rules values as metrics.
|
|
911
|
+
if (ruleValue.$eq !== undefined) {
|
|
912
|
+
const labels = {
|
|
913
|
+
rule: `${name} ${ruleKey} =`,
|
|
914
|
+
datetime,
|
|
915
|
+
...this.customMetricsLabels,
|
|
916
|
+
};
|
|
917
|
+
if (!remove) {
|
|
918
|
+
ruleObj.rule.set(labels, ruleValue.$eq);
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
ruleObj.rule.remove(labels);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (ruleValue.$lt !== undefined) {
|
|
925
|
+
const labels = {
|
|
926
|
+
rule: `${name} ${ruleKey} <`,
|
|
927
|
+
datetime,
|
|
928
|
+
...this.customMetricsLabels,
|
|
929
|
+
};
|
|
930
|
+
if (!remove) {
|
|
931
|
+
ruleObj.rule.set(labels, ruleValue.$lt);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
ruleObj.rule.remove(labels);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (ruleValue.$lte !== undefined) {
|
|
938
|
+
const labels = {
|
|
939
|
+
rule: `${name} ${ruleKey} <=`,
|
|
940
|
+
datetime,
|
|
941
|
+
...this.customMetricsLabels,
|
|
942
|
+
};
|
|
943
|
+
if (!remove) {
|
|
944
|
+
ruleObj.rule.set(labels, ruleValue.$lte);
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
ruleObj.rule.remove(labels);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (ruleValue.$gt !== undefined) {
|
|
951
|
+
const labels = {
|
|
952
|
+
rule: `${name} ${ruleKey} >`,
|
|
953
|
+
datetime,
|
|
954
|
+
...this.customMetricsLabels,
|
|
955
|
+
};
|
|
956
|
+
if (!remove) {
|
|
957
|
+
ruleObj.rule.set(labels, ruleValue.$gt);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
ruleObj.rule.remove(labels);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (ruleValue.$gte !== undefined) {
|
|
964
|
+
const labels = {
|
|
965
|
+
rule: `${name} ${ruleKey} >=`,
|
|
966
|
+
datetime,
|
|
967
|
+
...this.customMetricsLabels,
|
|
968
|
+
};
|
|
969
|
+
if (!remove) {
|
|
970
|
+
ruleObj.rule.set(labels, ruleValue.$gte);
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
ruleObj.rule.remove(labels);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
const alertRulesReportTags = this.getAlertRulesTags();
|
|
981
|
+
if (alertRulesReportTags && this.alertTagsMetrics) {
|
|
982
|
+
for (const [tag, stat] of alertRulesReportTags.entries()) {
|
|
983
|
+
this.alertTagsMetrics.set({ datetime, tag, ...this.customMetricsLabels }, calculateFailAmountPercentile(stat, this.alertRulesFailPercentile));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
const { resp, body } = await this.gateway.push({
|
|
988
|
+
jobName: this.prometheusPushgatewayJobName,
|
|
989
|
+
});
|
|
990
|
+
if (body.length) {
|
|
991
|
+
log.warn(`Pushgateway error ${resp.statusCode}: ${body}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
log.error(`Pushgateway push error: ${err.stack}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* alertRuleDesc
|
|
1000
|
+
*/
|
|
1001
|
+
getAlertRuleDesc(ruleKey, ruleValue) {
|
|
1002
|
+
const ruleDescs = [];
|
|
1003
|
+
if (ruleValue.$eq !== undefined) {
|
|
1004
|
+
ruleDescs.push(`= ${ruleValue.$eq}`);
|
|
1005
|
+
}
|
|
1006
|
+
if (ruleValue.$gt !== undefined) {
|
|
1007
|
+
ruleDescs.push(`> ${ruleValue.$gt}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (ruleValue.$gte !== undefined) {
|
|
1010
|
+
ruleDescs.push(`>= ${ruleValue.$gte}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (ruleValue.$lt !== undefined) {
|
|
1013
|
+
ruleDescs.push(`< ${ruleValue.$lt}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (ruleValue.$lte !== undefined) {
|
|
1016
|
+
ruleDescs.push(`<= ${ruleValue.$lte}`);
|
|
1017
|
+
}
|
|
1018
|
+
let ruleDesc = `${ruleKey} ${ruleDescs.join(' and ')}`;
|
|
1019
|
+
if (ruleValue.$after !== undefined) {
|
|
1020
|
+
ruleDesc += ` after ${ruleValue.$after}s`;
|
|
1021
|
+
}
|
|
1022
|
+
if (ruleValue.$before !== undefined) {
|
|
1023
|
+
ruleDesc += ` before ${ruleValue.$before}s`;
|
|
1024
|
+
}
|
|
1025
|
+
return ruleDesc;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* checkAlertRules
|
|
1029
|
+
*/
|
|
1030
|
+
checkAlertRules() {
|
|
1031
|
+
if (!this.alertRules || !this.running) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const now = Date.now();
|
|
1035
|
+
const elapsedSeconds = (now - this.startTimestamp) / 1000;
|
|
1036
|
+
for (const [key, rule] of Object.entries(this.alertRules)) {
|
|
1037
|
+
if (!this.collectedStats[key]) {
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
let failPercentile = this.alertRulesFailPercentile;
|
|
1041
|
+
const value = formatStats(this.collectedStats[key].all);
|
|
1042
|
+
// eslint-disable-next-line prefer-const
|
|
1043
|
+
for (let [ruleKey, ruleValues] of Object.entries(rule)) {
|
|
1044
|
+
if (['tags', 'failPercentile'].includes(ruleKey)) {
|
|
1045
|
+
if (ruleKey === 'failPercentile') {
|
|
1046
|
+
failPercentile = ruleValues;
|
|
1047
|
+
}
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (!Array.isArray(ruleValues)) {
|
|
1051
|
+
ruleValues = [ruleValues];
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
ruleValues = ruleValues;
|
|
1055
|
+
}
|
|
1056
|
+
let ruleElapsedSeconds = elapsedSeconds;
|
|
1057
|
+
for (const ruleValue of ruleValues) {
|
|
1058
|
+
if ((ruleValue.$after !== undefined && elapsedSeconds < ruleValue.$after) ||
|
|
1059
|
+
(ruleValue.$before !== undefined && elapsedSeconds > ruleValue.$before)) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (ruleValue.$after !== undefined) {
|
|
1063
|
+
ruleElapsedSeconds -= ruleValue.$after;
|
|
1064
|
+
}
|
|
1065
|
+
const checkValue = value[ruleKey];
|
|
1066
|
+
if (!isFinite(checkValue)) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const ruleDesc = this.getAlertRuleDesc(ruleKey, ruleValue);
|
|
1070
|
+
let failed = false;
|
|
1071
|
+
let failAmount = 0;
|
|
1072
|
+
if ((ruleValue.$skip_lt !== undefined && checkValue < ruleValue.$skip_lt) ||
|
|
1073
|
+
(ruleValue.$skip_lte !== undefined && checkValue <= ruleValue.$skip_lte) ||
|
|
1074
|
+
(ruleValue.$skip_gt !== undefined && checkValue > ruleValue.$skip_gt) ||
|
|
1075
|
+
(ruleValue.$skip_gte !== undefined && checkValue >= ruleValue.$skip_gte)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (ruleValue.$eq !== undefined) {
|
|
1079
|
+
if (checkValue !== ruleValue.$eq) {
|
|
1080
|
+
failed = true;
|
|
1081
|
+
failAmount = calculateFailAmount(checkValue, ruleValue.$eq);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
if (ruleValue.$lt !== undefined) {
|
|
1086
|
+
if (checkValue >= ruleValue.$lt) {
|
|
1087
|
+
failed = true;
|
|
1088
|
+
failAmount = calculateFailAmount(checkValue, ruleValue.$lt);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
else if (ruleValue.$lte !== undefined) {
|
|
1092
|
+
if (checkValue > ruleValue.$lte) {
|
|
1093
|
+
failed = true;
|
|
1094
|
+
failAmount = calculateFailAmount(checkValue, ruleValue.$lte);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (!failed) {
|
|
1098
|
+
if (ruleValue.$gt !== undefined) {
|
|
1099
|
+
if (checkValue <= ruleValue.$gt) {
|
|
1100
|
+
failed = true;
|
|
1101
|
+
failAmount = calculateFailAmount(checkValue, ruleValue.$gt);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
else if (ruleValue.$gte !== undefined) {
|
|
1105
|
+
if (checkValue < ruleValue.$gte) {
|
|
1106
|
+
failed = true;
|
|
1107
|
+
failAmount = calculateFailAmount(checkValue, ruleValue.$gte);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
// Report if failed or not.
|
|
1113
|
+
this.updateRulesReport(key, checkValue, ruleDesc, failed, failAmount, now, ruleElapsedSeconds, failPercentile);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* addFailedRule
|
|
1120
|
+
*/
|
|
1121
|
+
updateRulesReport(key, checkValue, ruleDesc, failed, failAmount, now, elapsedSeconds, failPercentile) {
|
|
1122
|
+
if (failed) {
|
|
1123
|
+
log.debug(`updateRulesReport ${key}.${ruleDesc} failed: ${failed} checkValue: ${checkValue} failAmount: ${failAmount} elapsedSeconds: ${elapsedSeconds}`);
|
|
1124
|
+
}
|
|
1125
|
+
let report = this.alertRulesReport.get(key);
|
|
1126
|
+
if (!report) {
|
|
1127
|
+
report = new Map();
|
|
1128
|
+
this.alertRulesReport.set(key, report);
|
|
1129
|
+
}
|
|
1130
|
+
let reportValue = report.get(ruleDesc);
|
|
1131
|
+
if (!reportValue) {
|
|
1132
|
+
reportValue = {
|
|
1133
|
+
totalFails: 0,
|
|
1134
|
+
totalFailsTime: 0,
|
|
1135
|
+
totalFailsTimePerc: 0,
|
|
1136
|
+
lastFailed: 0,
|
|
1137
|
+
valueStats: new fast_stats_1.Stats(),
|
|
1138
|
+
failAmountStats: new fast_stats_1.Stats(),
|
|
1139
|
+
failAmountPercentile: 0,
|
|
1140
|
+
};
|
|
1141
|
+
report.set(ruleDesc, reportValue);
|
|
1142
|
+
}
|
|
1143
|
+
if (failed) {
|
|
1144
|
+
reportValue.totalFails += 1;
|
|
1145
|
+
if (reportValue.lastFailed) {
|
|
1146
|
+
reportValue.totalFailsTime += (now - reportValue.lastFailed) / 1000;
|
|
1147
|
+
}
|
|
1148
|
+
reportValue.lastFailed = now;
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
reportValue.lastFailed = 0;
|
|
1152
|
+
}
|
|
1153
|
+
reportValue.totalFailsTimePerc = Math.round((100 * reportValue.totalFailsTime) / elapsedSeconds);
|
|
1154
|
+
reportValue.valueStats.push(checkValue);
|
|
1155
|
+
reportValue.failAmountStats.push(failAmount);
|
|
1156
|
+
reportValue.failAmountPercentile = calculateFailAmountPercentile(reportValue.failAmountStats, failPercentile);
|
|
1157
|
+
}
|
|
1158
|
+
getAlertRulesTags() {
|
|
1159
|
+
if (!this.alertRules) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const alertRulesReportTags = new Map();
|
|
1163
|
+
for (const [key, report] of this.alertRulesReport.entries()) {
|
|
1164
|
+
const tags = this.alertRules[key].tags || [];
|
|
1165
|
+
for (const tag of tags) {
|
|
1166
|
+
if (!alertRulesReportTags.has(tag)) {
|
|
1167
|
+
alertRulesReportTags.set(tag, new fast_stats_1.Stats());
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
for (const reportValue of report.values()) {
|
|
1171
|
+
const { failAmountPercentile } = reportValue;
|
|
1172
|
+
for (const tag of tags) {
|
|
1173
|
+
const stat = alertRulesReportTags.get(tag);
|
|
1174
|
+
if (!stat) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
stat.push(failAmountPercentile);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return alertRulesReportTags;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* formatAlertRulesReport
|
|
1185
|
+
* @param ext
|
|
1186
|
+
*/
|
|
1187
|
+
formatAlertRulesReport(ext = null) {
|
|
1188
|
+
if (!this.alertRulesReport || !this.alertRules) {
|
|
1189
|
+
return '';
|
|
1190
|
+
}
|
|
1191
|
+
// Update tags values.
|
|
1192
|
+
const alertRulesReportTags = this.getAlertRulesTags();
|
|
1193
|
+
// JSON output.
|
|
1194
|
+
if (ext === 'json') {
|
|
1195
|
+
const out = {
|
|
1196
|
+
tags: {},
|
|
1197
|
+
reports: {},
|
|
1198
|
+
};
|
|
1199
|
+
for (const [key, report] of this.alertRulesReport.entries()) {
|
|
1200
|
+
for (const [reportDesc, reportValue] of report.entries()) {
|
|
1201
|
+
const { totalFails, totalFailsTime, valueStats, totalFailsTimePerc, failAmountStats, failAmountPercentile } = reportValue;
|
|
1202
|
+
if (totalFails) {
|
|
1203
|
+
out.reports[`${key} ${reportDesc}`] = {
|
|
1204
|
+
totalFails,
|
|
1205
|
+
totalFailsTime: Math.round(totalFailsTime),
|
|
1206
|
+
valueAverage: valueStats.amean(),
|
|
1207
|
+
totalFailsTimePerc,
|
|
1208
|
+
failAmount: failAmountPercentile,
|
|
1209
|
+
count: failAmountStats.length,
|
|
1210
|
+
// failAmountStats: (failAmountStats as any).data as number[],
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
for (const [tag, stat] of alertRulesReportTags.entries()) {
|
|
1216
|
+
out.tags[tag] = calculateFailAmountPercentile(stat, this.alertRulesFailPercentile);
|
|
1217
|
+
}
|
|
1218
|
+
return JSON.stringify(out, null, 2);
|
|
1219
|
+
}
|
|
1220
|
+
// Textual output.
|
|
1221
|
+
let out = '';
|
|
1222
|
+
// Calculate max column size.
|
|
1223
|
+
let colSize = 20;
|
|
1224
|
+
for (const [key, report] of this.alertRulesReport.entries()) {
|
|
1225
|
+
for (const [reportDesc, reportValue] of report.entries()) {
|
|
1226
|
+
const { totalFails, totalFailsTimePerc } = reportValue;
|
|
1227
|
+
if (totalFails && totalFailsTimePerc > 0) {
|
|
1228
|
+
const check = `${key} ${reportDesc}`;
|
|
1229
|
+
colSize = Math.max(colSize, check.length);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (ext) {
|
|
1234
|
+
out += (0, sprintf_js_1.sprintf)(`| %(check)-${colSize}s | %(total)-10s | %(totalFailsTime)-15s | %(totalFailsTimePerc)-15s | %(failAmount)-15s |\n`, {
|
|
1235
|
+
check: 'Condition',
|
|
1236
|
+
total: 'Fails',
|
|
1237
|
+
totalFailsTime: 'Fail time (s)',
|
|
1238
|
+
totalFailsTimePerc: 'Fail time (%)',
|
|
1239
|
+
failAmount: 'Fail amount %',
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
out += (0, sprintf_js_1.sprintf)((0, chalk_1.default) `{bold %(check)-${colSize}s} {bold %(total)-10s} {bold %(totalFailsTime)-15s} {bold %(totalFailsTimePerc)-15s} {bold %(failAmount)-15s}\n`, {
|
|
1244
|
+
check: 'Condition',
|
|
1245
|
+
total: 'Fails',
|
|
1246
|
+
totalFailsTime: 'Fail time (s)',
|
|
1247
|
+
totalFailsTimePerc: 'Fail time (%)',
|
|
1248
|
+
failAmount: 'Fail amount %',
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
for (const [key, report] of this.alertRulesReport.entries()) {
|
|
1252
|
+
for (const [reportDesc, reportValue] of report.entries()) {
|
|
1253
|
+
const { totalFails, totalFailsTime, failAmountPercentile, totalFailsTimePerc } = reportValue;
|
|
1254
|
+
if (totalFails && totalFailsTimePerc > 0) {
|
|
1255
|
+
if (ext) {
|
|
1256
|
+
out += (0, sprintf_js_1.sprintf)(`| %(check)-${colSize}s | %(totalFails)-10s | %(totalFailsTime)-15s | %(totalFailsTimePerc)-15s | %(failAmountPercentile)-15s |\n`, {
|
|
1257
|
+
check: `${key} ${reportDesc}`,
|
|
1258
|
+
totalFails,
|
|
1259
|
+
totalFailsTime: Math.round(totalFailsTime),
|
|
1260
|
+
totalFailsTimePerc,
|
|
1261
|
+
failAmountPercentile,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
out += (0, sprintf_js_1.sprintf)((0, chalk_1.default) `{red {bold %(check)-${colSize}s}} {bold %(totalFails)-10s} {bold %(totalFailsTime)-15s} {bold %(totalFailsTimePerc)-15s} {bold %(failAmountPercentile)-15s}\n`, {
|
|
1266
|
+
check: `${key} ${reportDesc}`,
|
|
1267
|
+
totalFails,
|
|
1268
|
+
totalFailsTime: Math.round(totalFailsTime),
|
|
1269
|
+
totalFailsTimePerc,
|
|
1270
|
+
failAmountPercentile,
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// Tags report.
|
|
1277
|
+
if (ext) {
|
|
1278
|
+
out += (0, sprintf_js_1.sprintf)(`%(fill)s\n`, { fill: '-'.repeat(colSize + 15 + 7) });
|
|
1279
|
+
out += (0, sprintf_js_1.sprintf)(`| %(name)-${colSize}s | %(failPerc)-15s |\n`, {
|
|
1280
|
+
name: 'Tag',
|
|
1281
|
+
failPerc: 'Fail %',
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
else {
|
|
1285
|
+
out += (0, sprintf_js_1.sprintf)(`%(fill)s\n`, { fill: '-'.repeat(colSize + 15) });
|
|
1286
|
+
out += (0, sprintf_js_1.sprintf)((0, chalk_1.default) `{bold %(name)-${colSize}s} {bold %(failPerc)-15s}\n`, {
|
|
1287
|
+
name: 'Tag',
|
|
1288
|
+
failPerc: 'Fail %',
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
for (const [tag, stat] of alertRulesReportTags.entries()) {
|
|
1292
|
+
const failPerc = calculateFailAmountPercentile(stat, this.alertRulesFailPercentile);
|
|
1293
|
+
if (ext) {
|
|
1294
|
+
out += (0, sprintf_js_1.sprintf)(`| %(tag)-${colSize}s | %(failPerc)-15s |\n`, {
|
|
1295
|
+
tag,
|
|
1296
|
+
failPerc,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
const color = failPerc < 5 ? 'green' : failPerc < 25 ? 'yellowBright' : failPerc < 50 ? 'yellow' : 'red';
|
|
1301
|
+
out += (0, sprintf_js_1.sprintf)((0, chalk_1.default) `{${color} {bold %(tag)-${colSize}s %(failPerc)-15s}}\n`, {
|
|
1302
|
+
tag,
|
|
1303
|
+
failPerc,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* writeAlertRulesReport
|
|
1311
|
+
*/
|
|
1312
|
+
async writeAlertRulesReport() {
|
|
1313
|
+
if (!this.alertRules || !this.alertRulesFilename || !this.running) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
log.debug(`writeAlertRulesReport writing in ${this.alertRulesFilename}`);
|
|
1317
|
+
try {
|
|
1318
|
+
const ext = this.alertRulesFilename.split('.').slice(-1)[0];
|
|
1319
|
+
const report = this.formatAlertRulesReport(ext);
|
|
1320
|
+
if (!report.length) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
let out;
|
|
1324
|
+
if (ext === 'log') {
|
|
1325
|
+
const lines = report.split('\n').filter(line => line.length);
|
|
1326
|
+
const name = `Alert rules report (${new Date().toISOString()})`;
|
|
1327
|
+
out = (0, sprintf_js_1.sprintf)(`-- %(name)s %(fill)s\n`, {
|
|
1328
|
+
name,
|
|
1329
|
+
fill: '-'.repeat(Math.max(4, lines[0].length - name.length - 4)),
|
|
1330
|
+
});
|
|
1331
|
+
out += report;
|
|
1332
|
+
out += (0, sprintf_js_1.sprintf)(`%(fill)s\n`, {
|
|
1333
|
+
fill: '-'.repeat(lines[lines.length - 1].length),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
out = report;
|
|
1338
|
+
}
|
|
1339
|
+
await fs.promises.mkdir(path.dirname(this.alertRulesFilename), {
|
|
1340
|
+
recursive: true,
|
|
1341
|
+
});
|
|
1342
|
+
await fs.promises.writeFile(this.alertRulesFilename, out);
|
|
1343
|
+
}
|
|
1344
|
+
catch (err) {
|
|
1345
|
+
log.error(`writeAlertRulesReport error: ${err.stack}`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Stop the stats collector and the added Sessions.
|
|
1350
|
+
*/
|
|
1351
|
+
async stop() {
|
|
1352
|
+
if (!this.running) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
this.running = false;
|
|
1356
|
+
log.debug('stop');
|
|
1357
|
+
if (this.scheduler) {
|
|
1358
|
+
this.scheduler.stop();
|
|
1359
|
+
this.scheduler = undefined;
|
|
1360
|
+
}
|
|
1361
|
+
for (const session of this.sessions.values()) {
|
|
1362
|
+
try {
|
|
1363
|
+
session.removeAllListeners();
|
|
1364
|
+
await session.stop();
|
|
1365
|
+
}
|
|
1366
|
+
catch (err) {
|
|
1367
|
+
log.error(`session stop error: ${err.stack}`);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
this.sessions.clear();
|
|
1371
|
+
this.statsWriter = null;
|
|
1372
|
+
// delete metrics
|
|
1373
|
+
if (this.gateway) {
|
|
1374
|
+
await this.deletePushgatewayStats();
|
|
1375
|
+
this.gateway = null;
|
|
1376
|
+
this.metrics = {};
|
|
1377
|
+
}
|
|
1378
|
+
this.collectedStats = this.initCollectedStats();
|
|
1379
|
+
this.externalCollectedStats.clear();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
exports.Stats = Stats;
|
|
1383
|
+
//# sourceMappingURL=stats.js.map
|