@percy/monitoring 1.30.7-beta.3
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/dist/cpu.js +173 -0
- package/dist/index.js +114 -0
- package/dist/memory.js +103 -0
- package/dist/utils.js +14 -0
- package/package.json +38 -0
package/dist/cpu.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import si from 'systeminformation';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import logger from '@percy/logger';
|
|
5
|
+
import { pathsExist } from './utils.js';
|
|
6
|
+
const CGROUP_CPU_STATS = '/sys/fs/cgroup/cpu.stat';
|
|
7
|
+
const CGROUP_CPU_MAX = '/sys/fs/cgroup/cpu.max';
|
|
8
|
+
const CGROUP_FILES = [CGROUP_CPU_MAX, CGROUP_CPU_STATS];
|
|
9
|
+
const log = logger('monitoring:cpu');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves CPU information.
|
|
13
|
+
* @param {*} os - The OS module (if needed, otherwise remove this parameter).
|
|
14
|
+
* @param {*} param1 - An object containing CPU details.
|
|
15
|
+
* @returns {{ currentUsagePercent: number, cores: number, cgroupExists: boolean }} CPU information.
|
|
16
|
+
*/
|
|
17
|
+
async function getCPUUsageInfo(os) {
|
|
18
|
+
try {
|
|
19
|
+
if (os.includes('linux') && (await pathsExist(CGROUP_FILES))) {
|
|
20
|
+
return await getLinuxCPUUsage();
|
|
21
|
+
} else {
|
|
22
|
+
return await getCPUUsage();
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// Don't raise this error to avoid user build failure
|
|
26
|
+
// show error in debug mode only
|
|
27
|
+
log.debug(`Error: ${error}`);
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* reading cpu stats from cgroup files
|
|
34
|
+
*/
|
|
35
|
+
async function readCpuStatFromCgroup() {
|
|
36
|
+
const content = await fs.readFile(CGROUP_CPU_STATS, 'utf8');
|
|
37
|
+
const stats = {};
|
|
38
|
+
|
|
39
|
+
// Parse the cpu.stat file
|
|
40
|
+
content.split('\n').forEach(line => {
|
|
41
|
+
const [key, value] = line.trim().split(' ');
|
|
42
|
+
if (key && value) {
|
|
43
|
+
stats[key] = parseInt(value);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return stats;
|
|
47
|
+
}
|
|
48
|
+
async function getTotalCores() {
|
|
49
|
+
let availableCPUs = (await si.cpu()).cores;
|
|
50
|
+
try {
|
|
51
|
+
// Read cpu.max for quota and period
|
|
52
|
+
const cpuMaxContent = await fs.readFile(CGROUP_CPU_MAX, 'utf8');
|
|
53
|
+
const [quotaStr, periodStr] = cpuMaxContent.trim().split(' ');
|
|
54
|
+
let quota, period;
|
|
55
|
+
if (quotaStr !== 'max') {
|
|
56
|
+
// Parse quota and period values
|
|
57
|
+
quota = parseInt(quotaStr);
|
|
58
|
+
period = parseInt(periodStr);
|
|
59
|
+
availableCPUs = quota / period;
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// supressing this err, as we will use fallback system
|
|
63
|
+
// level cpu details
|
|
64
|
+
}
|
|
65
|
+
return availableCPUs;
|
|
66
|
+
}
|
|
67
|
+
async function getClientCPUDetails() {
|
|
68
|
+
const cores = await getTotalCores();
|
|
69
|
+
return {
|
|
70
|
+
arch: os.arch(),
|
|
71
|
+
cores
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* linux cpu load is calculated is same for
|
|
77
|
+
* containerLevel and machineLevel
|
|
78
|
+
* @returns {{ currentUsagePercent: number, cores: number }} CPU information.
|
|
79
|
+
*/
|
|
80
|
+
async function getLinuxCPUUsage() {
|
|
81
|
+
try {
|
|
82
|
+
const availableCPUs = await getTotalCores();
|
|
83
|
+
|
|
84
|
+
// Get first CPU usage reading
|
|
85
|
+
const startStats = await readCpuStatFromCgroup();
|
|
86
|
+
|
|
87
|
+
// Wait for 1 second, ie 10^6 microsecod
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
89
|
+
|
|
90
|
+
// Get second CPU usage reading
|
|
91
|
+
const endStats = await readCpuStatFromCgroup();
|
|
92
|
+
|
|
93
|
+
// Calculate CPU usage
|
|
94
|
+
const usageDelta = endStats.usage_usec - startStats.usage_usec;
|
|
95
|
+
|
|
96
|
+
// Calculate percentage (similar to previous implementation)
|
|
97
|
+
// usageDelta is in microseconds, we measured over 1 second (1_000_000 microseconds)
|
|
98
|
+
// NOTE: cpuPercentage can be > 100% as something process can use
|
|
99
|
+
// more than allocated cpu range
|
|
100
|
+
const cpuPercent = usageDelta / (1_000_000 * availableCPUs) * 100;
|
|
101
|
+
return {
|
|
102
|
+
cores: availableCPUs,
|
|
103
|
+
currentUsagePercent: cpuPercent,
|
|
104
|
+
cgroupExists: true
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// TODO: Log error here
|
|
108
|
+
log.debug('Linux c_group cpu usage error:', error);
|
|
109
|
+
// using fallback method to get details
|
|
110
|
+
return await getCPUUsage();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Each cpu.times has following 4 details
|
|
116
|
+
* {
|
|
117
|
+
user: time spent running user code
|
|
118
|
+
nice: time spent running user code at low priority
|
|
119
|
+
sys: time spent running system/kernel code
|
|
120
|
+
idle: time CPU was idle
|
|
121
|
+
irq: time spent handling interrupts
|
|
122
|
+
}
|
|
123
|
+
*/
|
|
124
|
+
async function computeCpuUsageStats() {
|
|
125
|
+
let totalTickTime = 0;
|
|
126
|
+
let totalIdleTime = 0;
|
|
127
|
+
os.cpus().forEach(cpu => {
|
|
128
|
+
for (let type in cpu.times) {
|
|
129
|
+
totalTickTime += cpu.times[type];
|
|
130
|
+
}
|
|
131
|
+
totalIdleTime += cpu.times.idle;
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
totalTickTime,
|
|
135
|
+
totalIdleTime
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* this is a fallback method, if
|
|
142
|
+
* 1. For LINUX, we can't find cgroup details
|
|
143
|
+
* 2. For Win/OSX operating system
|
|
144
|
+
* @returns {{ currentUsagePercent: number, cores: number, cgroupExists: boolean }}
|
|
145
|
+
*/
|
|
146
|
+
async function getCPUUsage() {
|
|
147
|
+
const initialCpuUsage = await computeCpuUsageStats();
|
|
148
|
+
// wait for 1 second, to connect cpu load for 10^6 micro-second
|
|
149
|
+
// ie. 1 second
|
|
150
|
+
await new Promise(res => setTimeout(res, 1000));
|
|
151
|
+
const finalCpuUsage = await computeCpuUsageStats();
|
|
152
|
+
|
|
153
|
+
// Calculate differences
|
|
154
|
+
const deltaTick = finalCpuUsage.totalTickTime - initialCpuUsage.totalTickTime;
|
|
155
|
+
const deltaIdle = finalCpuUsage.totalIdleTime - initialCpuUsage.totalIdleTime;
|
|
156
|
+
|
|
157
|
+
// Calculate % usage
|
|
158
|
+
let cpuUsagePercent = null;
|
|
159
|
+
|
|
160
|
+
// Case: If no cpu usage is done from 1 sec, then delta will be 0
|
|
161
|
+
// raising error zero division error
|
|
162
|
+
// ideally it shouldn't happen
|
|
163
|
+
if (deltaTick > 0) {
|
|
164
|
+
cpuUsagePercent = (1 - deltaIdle / deltaTick) * 100;
|
|
165
|
+
} else cpuUsagePercent = 0;
|
|
166
|
+
let cores = await getTotalCores();
|
|
167
|
+
return {
|
|
168
|
+
cores,
|
|
169
|
+
currentUsagePercent: cpuUsagePercent,
|
|
170
|
+
cgroupExists: false
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export { getCPUUsageInfo, getClientCPUDetails };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import logger from '@percy/logger';
|
|
4
|
+
import { getCPUUsageInfo, getClientCPUDetails } from './cpu.js';
|
|
5
|
+
import { getMemoryUsageInfo, getClientMemoryDetails } from './memory.js';
|
|
6
|
+
export default class Monitoring {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.os = this.getOS();
|
|
9
|
+
this.monitoringId = null;
|
|
10
|
+
this.running = false;
|
|
11
|
+
this.isContainer = this.isContainerLevel();
|
|
12
|
+
this.isPod = this.isPodLevel();
|
|
13
|
+
this.isMachine = this.isMachineLevel();
|
|
14
|
+
this.lastExecutedAt = null;
|
|
15
|
+
this.pod = this.isPodLevel();
|
|
16
|
+
this.cpuInfo = {};
|
|
17
|
+
this.memoryUsageInfo = {};
|
|
18
|
+
this.log = logger('monitoring');
|
|
19
|
+
}
|
|
20
|
+
getOS() {
|
|
21
|
+
return os.platform();
|
|
22
|
+
}
|
|
23
|
+
getPercyEnv() {
|
|
24
|
+
const percyKeys = Object.keys(process.env).filter(env => env.toLowerCase().includes('percy') && !env.toLowerCase().includes('token'));
|
|
25
|
+
let envs = {};
|
|
26
|
+
percyKeys.forEach(env => {
|
|
27
|
+
envs[env] = process.env[env];
|
|
28
|
+
});
|
|
29
|
+
return envs;
|
|
30
|
+
}
|
|
31
|
+
isContainerLevel() {
|
|
32
|
+
return fs.existsSync('/.dockerenv');
|
|
33
|
+
}
|
|
34
|
+
isPodLevel() {
|
|
35
|
+
return fs.existsSync('/var/run/secrets/kubernetes.io');
|
|
36
|
+
}
|
|
37
|
+
isMachineLevel() {
|
|
38
|
+
return !this.isContainerLevel();
|
|
39
|
+
}
|
|
40
|
+
async logSystemInfo() {
|
|
41
|
+
try {
|
|
42
|
+
const cpu = await getClientCPUDetails();
|
|
43
|
+
const mem = await getClientMemoryDetails();
|
|
44
|
+
const percyEnvs = this.getPercyEnv();
|
|
45
|
+
this.log.debug(`[Operating System] Platform: ${this.os}, Type: ${os.type()}, Release: ${os.release()}`);
|
|
46
|
+
this.log.debug(`[CPU] Arch: ${cpu.arch}, cores: ${cpu.cores}`);
|
|
47
|
+
this.log.debug(`[Memory] Total: ${mem.total / 1024 ** 3} gb, Swap Space: ${mem.swaptotal / 1024 ** 3} gb`);
|
|
48
|
+
this.log.debug(`Container Level: ${this.isContainer}, Pod Level: ${this.isPod}, Machine Level: ${this.isMachine}`);
|
|
49
|
+
this.log.debug(`Percy Envs: ${JSON.stringify(percyEnvs)}`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// suppress error
|
|
52
|
+
this.log.debug(`Error logging system info: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* It will start monitoring at certain interval
|
|
58
|
+
* by default every 5 seconds
|
|
59
|
+
*/
|
|
60
|
+
async startMonitoring(options = {}) {
|
|
61
|
+
const {
|
|
62
|
+
interval = 5000
|
|
63
|
+
} = options;
|
|
64
|
+
// early return if already monitoring
|
|
65
|
+
if (this.monitoringId) return;
|
|
66
|
+
await this.executeMonitoring();
|
|
67
|
+
this.monitoringId = setInterval(() => {
|
|
68
|
+
this.executeMonitoring();
|
|
69
|
+
}, interval);
|
|
70
|
+
this.log.debug('Started monitoring sytem metrics');
|
|
71
|
+
}
|
|
72
|
+
async executeMonitoring() {
|
|
73
|
+
this.running = true;
|
|
74
|
+
await this.monitoringCPUUsage();
|
|
75
|
+
await this.monitorMemoryUsage();
|
|
76
|
+
this.lastExecutedAt = Date.now();
|
|
77
|
+
}
|
|
78
|
+
stopMonitoring() {
|
|
79
|
+
if (this.monitoringId) {
|
|
80
|
+
clearInterval(this.monitoringId);
|
|
81
|
+
this.monitoringId = null;
|
|
82
|
+
this.cpuInfo = {};
|
|
83
|
+
this.memoryUsageInfo = {};
|
|
84
|
+
this.running = false;
|
|
85
|
+
this.lastExecutedAt = null;
|
|
86
|
+
this.log.debug('Stopped monitoring sytem metrics');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async monitoringCPUUsage() {
|
|
90
|
+
const cpuInfo = await getCPUUsageInfo(this.os, {
|
|
91
|
+
containerLevel: this.isContainer,
|
|
92
|
+
machineLevel: this.isMachine
|
|
93
|
+
});
|
|
94
|
+
this.cpuInfo = cpuInfo;
|
|
95
|
+
this.log.debug(`cpuInfo: ${JSON.stringify(cpuInfo)}`);
|
|
96
|
+
}
|
|
97
|
+
async monitorMemoryUsage() {
|
|
98
|
+
const memoryInfo = await getMemoryUsageInfo(this.os, {
|
|
99
|
+
containerLevel: this.isContainer,
|
|
100
|
+
machineLevel: this.isMachine
|
|
101
|
+
});
|
|
102
|
+
this.memoryUsageInfo = memoryInfo;
|
|
103
|
+
this.log.debug(`memoryInfo: ${JSON.stringify(memoryInfo)}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Whenever, this will get called to get
|
|
107
|
+
// cpu and memory usage info
|
|
108
|
+
getMonitoringInfo() {
|
|
109
|
+
return {
|
|
110
|
+
cpuInfo: this.cpuInfo,
|
|
111
|
+
memoryUsageInfo: this.memoryUsageInfo
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import si from 'systeminformation';
|
|
3
|
+
import logger from '@percy/logger';
|
|
4
|
+
import { pathsExist } from './utils.js';
|
|
5
|
+
const CGROUP_MEMORY_CURRENT = '/sys/fs/cgroup/memory.current';
|
|
6
|
+
const CGROUP_MEMORY_MAX = '/sys/fs/cgroup/memory.max';
|
|
7
|
+
const CGROUP_FILES = [CGROUP_MEMORY_CURRENT, CGROUP_MEMORY_MAX];
|
|
8
|
+
const log = logger('monitoring:memory');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves CPU information.
|
|
12
|
+
* @param {*} os - The OS module (if needed, otherwise remove this parameter).
|
|
13
|
+
* @param {*} param1 - An object containing CPU details.
|
|
14
|
+
* @returns {{ currentUsagePercent: number, totalMemory: number }} Memory information.
|
|
15
|
+
*/
|
|
16
|
+
async function getMemoryUsageInfo(os) {
|
|
17
|
+
try {
|
|
18
|
+
if (os.includes('linux') && (await pathsExist(CGROUP_FILES))) {
|
|
19
|
+
return await getLinuxMemoryUsage();
|
|
20
|
+
} else {
|
|
21
|
+
return await getMemoryUsage();
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
log.debug(`Error: ${error}`);
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @returns { swaptotal: number, total: number }
|
|
31
|
+
*/
|
|
32
|
+
async function getClientMemoryDetails() {
|
|
33
|
+
// values are in bytes
|
|
34
|
+
const {
|
|
35
|
+
swaptotal
|
|
36
|
+
} = await si.mem();
|
|
37
|
+
const total = await getTotalMemory();
|
|
38
|
+
return {
|
|
39
|
+
swaptotal,
|
|
40
|
+
total
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function getTotalMemory() {
|
|
44
|
+
let maxAllocatedMemory = (await si.mem()).total;
|
|
45
|
+
try {
|
|
46
|
+
let maxMemory = await fs.readFile(CGROUP_MEMORY_MAX);
|
|
47
|
+
// if memory_max == max ( take system max value )
|
|
48
|
+
// as there is no limit set, it's the max value
|
|
49
|
+
if (maxMemory !== 'max') {
|
|
50
|
+
maxAllocatedMemory = parseInt(maxMemory);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// supressing this err, as we will use system level metric
|
|
54
|
+
// as fallback
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// in bytes
|
|
58
|
+
return maxAllocatedMemory;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* get memory usage using linux c_group ( control group )
|
|
63
|
+
* @returns {{ currentUsagePercent: number, totalMemory: number }} Memory information.
|
|
64
|
+
*/
|
|
65
|
+
async function getLinuxMemoryUsage() {
|
|
66
|
+
try {
|
|
67
|
+
const maxAllocatedMemory = await getTotalMemory(); // in bytes
|
|
68
|
+
const currentMemoryUsage = parseInt(await fs.readFile(CGROUP_MEMORY_CURRENT)); // in bytes
|
|
69
|
+
const memoryUsagePercentage = currentMemoryUsage / maxAllocatedMemory * 100;
|
|
70
|
+
return {
|
|
71
|
+
totalMemory: maxAllocatedMemory,
|
|
72
|
+
currentUsagePercent: memoryUsagePercentage
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log.debug('Linux c_group memory usage error:', error);
|
|
76
|
+
|
|
77
|
+
// Use the fallback method, to get memory usage details
|
|
78
|
+
return await getMemoryUsage();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fallback method, for
|
|
84
|
+
* 1. LINUX, if cgroups files doesn't exists
|
|
85
|
+
* 2. MacOSX, Windows based OS
|
|
86
|
+
*
|
|
87
|
+
* @returns {{ currentUsagePercent: number, totalMemory: number }} Memory information.
|
|
88
|
+
*/
|
|
89
|
+
async function getMemoryUsage() {
|
|
90
|
+
// Get total and free memory
|
|
91
|
+
const memInfo = await si.mem();
|
|
92
|
+
const totalMemory = memInfo.total;
|
|
93
|
+
const freeMemory = memInfo.available;
|
|
94
|
+
|
|
95
|
+
// Calculate memory usage percentage
|
|
96
|
+
const usedMemory = totalMemory - freeMemory;
|
|
97
|
+
const memoryUsagePercentage = usedMemory / totalMemory * 100;
|
|
98
|
+
return {
|
|
99
|
+
currentUsagePercent: memoryUsagePercentage,
|
|
100
|
+
totalMemory
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export { getMemoryUsageInfo, getClientMemoryDetails };
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
async function pathsExist(paths) {
|
|
3
|
+
let exists = true;
|
|
4
|
+
try {
|
|
5
|
+
for (const file of paths) {
|
|
6
|
+
// throws error if file is not accessible
|
|
7
|
+
await fs.access(file);
|
|
8
|
+
}
|
|
9
|
+
} catch (error) {
|
|
10
|
+
exists = false;
|
|
11
|
+
}
|
|
12
|
+
return exists;
|
|
13
|
+
}
|
|
14
|
+
export { pathsExist };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@percy/monitoring",
|
|
3
|
+
"version": "1.30.7-beta.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/percy/cli",
|
|
8
|
+
"directory": "packages/monitoring"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"tag": "beta"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=14"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node ../../scripts/build",
|
|
27
|
+
"lint": "eslint --ignore-path ../../.gitignore .",
|
|
28
|
+
"test": "node ../../scripts/test",
|
|
29
|
+
"test:coverage": "yarn test --coverage"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@percy/config": "1.30.7-beta.3",
|
|
33
|
+
"@percy/logger": "1.30.7-beta.3",
|
|
34
|
+
"@percy/sdk-utils": "1.30.7-beta.3",
|
|
35
|
+
"systeminformation": "^5.25.11"
|
|
36
|
+
},
|
|
37
|
+
"gitHead": "06141a5d15278e9cb14550a96c745336acf8bafd"
|
|
38
|
+
}
|