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