@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 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
+ }