@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.
Files changed (53) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +296 -0
  3. package/app.min.js +2 -0
  4. package/build/src/app.d.ts +6 -0
  5. package/build/src/app.js +207 -0
  6. package/build/src/app.js.map +1 -0
  7. package/build/src/config.d.ts +104 -0
  8. package/build/src/config.js +880 -0
  9. package/build/src/config.js.map +1 -0
  10. package/build/src/generate-config-docs.d.ts +1 -0
  11. package/build/src/generate-config-docs.js +41 -0
  12. package/build/src/generate-config-docs.js.map +1 -0
  13. package/build/src/index.d.ts +9 -0
  14. package/build/src/index.js +26 -0
  15. package/build/src/index.js.map +1 -0
  16. package/build/src/media.d.ts +33 -0
  17. package/build/src/media.js +113 -0
  18. package/build/src/media.js.map +1 -0
  19. package/build/src/rtcstats.d.ts +302 -0
  20. package/build/src/rtcstats.js +418 -0
  21. package/build/src/rtcstats.js.map +1 -0
  22. package/build/src/server.d.ts +173 -0
  23. package/build/src/server.js +639 -0
  24. package/build/src/server.js.map +1 -0
  25. package/build/src/session.d.ts +277 -0
  26. package/build/src/session.js +1552 -0
  27. package/build/src/session.js.map +1 -0
  28. package/build/src/stats.d.ts +243 -0
  29. package/build/src/stats.js +1383 -0
  30. package/build/src/stats.js.map +1 -0
  31. package/build/src/utils.d.ts +249 -0
  32. package/build/src/utils.js +1220 -0
  33. package/build/src/utils.js.map +1 -0
  34. package/build/src/visqol.d.ts +6 -0
  35. package/build/src/visqol.js +61 -0
  36. package/build/src/visqol.js.map +1 -0
  37. package/build/src/vmaf.d.ts +83 -0
  38. package/build/src/vmaf.js +624 -0
  39. package/build/src/vmaf.js.map +1 -0
  40. package/build/tsconfig.tsbuildinfo +1 -0
  41. package/package.json +129 -0
  42. package/src/app.ts +241 -0
  43. package/src/config.ts +852 -0
  44. package/src/generate-config-docs.ts +47 -0
  45. package/src/index.ts +9 -0
  46. package/src/media.ts +151 -0
  47. package/src/rtcstats.ts +507 -0
  48. package/src/server.ts +645 -0
  49. package/src/session.ts +1908 -0
  50. package/src/stats.ts +1668 -0
  51. package/src/utils.ts +1295 -0
  52. package/src/visqol.ts +62 -0
  53. 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