analytics-node-agent 1.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/.vscode/settings.json +4 -0
- package/index.js +241 -0
- package/nodemon.json +9 -0
- package/package.json +18 -0
package/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import si from 'systeminformation'
|
|
3
|
+
|
|
4
|
+
const app = express()
|
|
5
|
+
const PORT = process.env.PORT || 9100
|
|
6
|
+
|
|
7
|
+
// Force every response to be plain JSON — avoids Content-Type issues on Linux
|
|
8
|
+
app.use((req, res, next) => {
|
|
9
|
+
res.setHeader('Content-Type', 'application/json')
|
|
10
|
+
next()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Strips systeminformation's non-plain objects → guaranteed POJO for axios / Mongoose
|
|
14
|
+
const toPlain = obj => JSON.parse(JSON.stringify(obj ?? null))
|
|
15
|
+
|
|
16
|
+
// ── Cache static CPU info at startup (never changes) ──────────────────────────
|
|
17
|
+
let cpuStaticCache = null
|
|
18
|
+
let memLayoutCache = null // also static — physical DIMM slots never change
|
|
19
|
+
|
|
20
|
+
async function getCpuStatic() {
|
|
21
|
+
if (cpuStaticCache) return cpuStaticCache
|
|
22
|
+
try {
|
|
23
|
+
const [cpu, cpuCache] = await Promise.all([
|
|
24
|
+
si.cpu(),
|
|
25
|
+
si.cpuCache(),
|
|
26
|
+
])
|
|
27
|
+
cpuStaticCache = toPlain({
|
|
28
|
+
manufacturer: cpu.manufacturer ?? null,
|
|
29
|
+
brand: cpu.brand ?? null,
|
|
30
|
+
vendor: cpu.vendor ?? null,
|
|
31
|
+
family: cpu.family ?? null,
|
|
32
|
+
model: cpu.model ?? null,
|
|
33
|
+
stepping: cpu.stepping ?? null,
|
|
34
|
+
revision: cpu.revision ?? null,
|
|
35
|
+
speed: cpu.speed ?? null,
|
|
36
|
+
speedMin: cpu.speedMin ?? null,
|
|
37
|
+
speedMax: cpu.speedMax ?? null,
|
|
38
|
+
cores: cpu.cores ?? null,
|
|
39
|
+
physicalCores: cpu.physicalCores ?? null,
|
|
40
|
+
processors: cpu.processors ?? null,
|
|
41
|
+
socket: cpu.socket ?? null,
|
|
42
|
+
flags: cpu.flags ?? null,
|
|
43
|
+
virtualization: cpu.virtualization ?? false,
|
|
44
|
+
cache: {
|
|
45
|
+
l1d: cpuCache.l1d ?? null,
|
|
46
|
+
l1i: cpuCache.l1i ?? null,
|
|
47
|
+
l2: cpuCache.l2 ?? null,
|
|
48
|
+
l3: cpuCache.l3 ?? null,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.warn('[agent] CPU static info failed:', err.message)
|
|
53
|
+
cpuStaticCache = null
|
|
54
|
+
}
|
|
55
|
+
return cpuStaticCache
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getMemLayout() {
|
|
59
|
+
if (memLayoutCache) return memLayoutCache
|
|
60
|
+
try {
|
|
61
|
+
const layout = await si.memLayout()
|
|
62
|
+
memLayoutCache = toPlain(
|
|
63
|
+
(Array.isArray(layout) ? layout : []).map(slot => ({
|
|
64
|
+
size: slot.size ?? null,
|
|
65
|
+
bank: slot.bank ?? null,
|
|
66
|
+
type: slot.type ?? null,
|
|
67
|
+
clockSpeed: slot.clockSpeed ?? null,
|
|
68
|
+
formFactor: slot.formFactor ?? null,
|
|
69
|
+
manufacturer: slot.manufacturer ?? null,
|
|
70
|
+
partNum: slot.partNum ?? null,
|
|
71
|
+
serialNum: slot.serialNum ?? null,
|
|
72
|
+
voltageConfigured: slot.voltageConfigured ?? null,
|
|
73
|
+
}))
|
|
74
|
+
)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn('[agent] memLayout failed:', err.message)
|
|
77
|
+
memLayoutCache = []
|
|
78
|
+
}
|
|
79
|
+
return memLayoutCache
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Warm up caches on startup
|
|
83
|
+
getCpuStatic()
|
|
84
|
+
getMemLayout()
|
|
85
|
+
|
|
86
|
+
// ── Health ────────────────────────────────────────────────────────────────────
|
|
87
|
+
app.get('/health', (req, res) => {
|
|
88
|
+
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// ── CPU Static Info ───────────────────────────────────────────────────────────
|
|
92
|
+
app.get('/cpu-info', async (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const info = await getCpuStatic()
|
|
95
|
+
if (!info) return res.status(503).end(JSON.stringify({ error: 'CPU info unavailable' }))
|
|
96
|
+
res.end(JSON.stringify({ cpuInfo: info }))
|
|
97
|
+
} catch (err) {
|
|
98
|
+
res.status(500).end(JSON.stringify({ error: err.message }))
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ── System (CPU load + RAM + Network) ────────────────────────────────────────
|
|
103
|
+
app.get('/system', async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
const [cpuLoad, mem, net] = await Promise.all([
|
|
106
|
+
si.currentLoad(),
|
|
107
|
+
si.mem(),
|
|
108
|
+
si.networkStats(),
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
// memLayout is cached — no extra I/O on every poll
|
|
112
|
+
const layout = await getMemLayout()
|
|
113
|
+
|
|
114
|
+
const cpu = toPlain({
|
|
115
|
+
currentLoad: parseFloat((cpuLoad.currentLoad ?? 0).toFixed(2)),
|
|
116
|
+
avgLoad: parseFloat((cpuLoad.avgLoad ?? 0).toFixed(2)),
|
|
117
|
+
currentLoadUser: parseFloat((cpuLoad.currentLoadUser ?? 0).toFixed(2)),
|
|
118
|
+
currentLoadSystem: parseFloat((cpuLoad.currentLoadSystem ?? 0).toFixed(2)),
|
|
119
|
+
currentLoadIdle: parseFloat((cpuLoad.currentLoadIdle ?? 0).toFixed(2)),
|
|
120
|
+
currentLoadIrq: parseFloat((cpuLoad.currentLoadIrq ?? 0).toFixed(2)),
|
|
121
|
+
cores: (cpuLoad.cpus ?? []).map((core, i) => ({
|
|
122
|
+
core: i,
|
|
123
|
+
currentLoad: parseFloat((core.load ?? 0).toFixed(2)),
|
|
124
|
+
currentLoadUser: parseFloat((core.loadUser ?? 0).toFixed(2)),
|
|
125
|
+
currentLoadSystem: parseFloat((core.loadSystem ?? 0).toFixed(2)),
|
|
126
|
+
currentLoadIdle: parseFloat((core.loadIdle ?? 0).toFixed(2)),
|
|
127
|
+
currentLoadIrq: parseFloat((core.loadIrq ?? 0).toFixed(2)),
|
|
128
|
+
})),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const ram = toPlain({
|
|
132
|
+
total: mem.total ?? null,
|
|
133
|
+
used: mem.active ?? null,
|
|
134
|
+
free: mem.available ?? null,
|
|
135
|
+
buffcache: mem.buffcache ?? null,
|
|
136
|
+
usedPercent: parseFloat(((mem.active / mem.total) * 100).toFixed(2)),
|
|
137
|
+
swapTotal: mem.swaptotal ?? null,
|
|
138
|
+
swapUsed: mem.swapused ?? null,
|
|
139
|
+
swapFree: mem.swapfree ?? null,
|
|
140
|
+
swapPercent: mem.swaptotal > 0
|
|
141
|
+
? parseFloat(((mem.swapused / mem.swaptotal) * 100).toFixed(2))
|
|
142
|
+
: 0,
|
|
143
|
+
layout, // already a plain array from cache
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const network = toPlain(
|
|
147
|
+
(Array.isArray(net) ? net : []).map(n => ({
|
|
148
|
+
iface: n.iface ?? null,
|
|
149
|
+
operstate: n.operstate ?? null,
|
|
150
|
+
rx_bytes: n.rx_bytes ?? null,
|
|
151
|
+
tx_bytes: n.tx_bytes ?? null,
|
|
152
|
+
rx_sec: Math.max(0, n.rx_sec ?? 0),
|
|
153
|
+
tx_sec: Math.max(0, n.tx_sec ?? 0),
|
|
154
|
+
rx_dropped: n.rx_dropped ?? null,
|
|
155
|
+
tx_dropped: n.tx_dropped ?? null,
|
|
156
|
+
rx_errors: n.rx_errors ?? null,
|
|
157
|
+
tx_errors: n.tx_errors ?? null,
|
|
158
|
+
ms: n.ms ?? null,
|
|
159
|
+
}))
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
res.end(JSON.stringify({ cpu, ram, network }))
|
|
163
|
+
} catch (err) {
|
|
164
|
+
res.status(500).end(JSON.stringify({ error: err.message }))
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// ── Disk (filesystem + block devices + I/O stats) ────────────────────────────
|
|
169
|
+
app.get('/disk', async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const [fsSizes, fsStats, blockDevices, diskIO] = await Promise.all([
|
|
172
|
+
si.fsSize(),
|
|
173
|
+
si.fsStats(),
|
|
174
|
+
si.blockDevices(),
|
|
175
|
+
si.disksIO(),
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
// Normalise fsStats → always an array
|
|
179
|
+
const fsStatsArray = Array.isArray(fsStats)
|
|
180
|
+
? fsStats
|
|
181
|
+
: (fsStats && typeof fsStats === 'object' ? [fsStats] : [])
|
|
182
|
+
|
|
183
|
+
const disk = toPlain(
|
|
184
|
+
(Array.isArray(fsSizes) ? fsSizes : [])
|
|
185
|
+
.filter(d => d.size > 0)
|
|
186
|
+
.map(d => {
|
|
187
|
+
const stat = fsStatsArray.find(s => s.fs === d.fs) ?? null
|
|
188
|
+
return {
|
|
189
|
+
fs: d.fs ?? null,
|
|
190
|
+
mount: d.mount ?? null,
|
|
191
|
+
type: d.type ?? null,
|
|
192
|
+
size: d.size ?? null,
|
|
193
|
+
used: d.used ?? null,
|
|
194
|
+
available: d.available ?? null,
|
|
195
|
+
usePercent: parseFloat((d.use ?? 0).toFixed(2)),
|
|
196
|
+
rx_sec: stat ? Math.max(0, stat.rx_sec ?? 0) : null,
|
|
197
|
+
wx_sec: stat ? Math.max(0, stat.wx_sec ?? 0) : null,
|
|
198
|
+
tx_sec: stat ? Math.max(0, stat.tx_sec ?? 0) : null,
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
const blockDevs = toPlain(
|
|
204
|
+
(Array.isArray(blockDevices) ? blockDevices : [])
|
|
205
|
+
.filter(d => d.type === 'disk' || d.type === 'part')
|
|
206
|
+
.map(d => ({
|
|
207
|
+
name: d.name ?? null,
|
|
208
|
+
type: d.type ?? null,
|
|
209
|
+
fstype: d.fstype ?? null,
|
|
210
|
+
mount: d.mount ?? null,
|
|
211
|
+
size: d.size ?? null,
|
|
212
|
+
physical: d.physical ?? null,
|
|
213
|
+
removable: d.removable ?? false,
|
|
214
|
+
protocol: d.protocol ?? null,
|
|
215
|
+
label: d.label ?? null,
|
|
216
|
+
}))
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const ioStats = toPlain([{
|
|
220
|
+
rIO: diskIO?.rIO ?? null,
|
|
221
|
+
wIO: diskIO?.wIO ?? null,
|
|
222
|
+
tIO: diskIO?.tIO ?? null,
|
|
223
|
+
rIO_sec: diskIO?.rIO_sec ?? null,
|
|
224
|
+
wIO_sec: diskIO?.wIO_sec ?? null,
|
|
225
|
+
tIO_sec: diskIO?.tIO_sec ?? null,
|
|
226
|
+
rBytesPerSec: diskIO?.rBytesPerSec ?? null,
|
|
227
|
+
wBytesPerSec: diskIO?.wBytesPerSec ?? null,
|
|
228
|
+
tBytesPerSec: diskIO?.tBytesPerSec ?? null,
|
|
229
|
+
ms: diskIO?.ms ?? null,
|
|
230
|
+
}])
|
|
231
|
+
|
|
232
|
+
res.end(JSON.stringify({ disk, blockDevices: blockDevs, diskIO: ioStats }))
|
|
233
|
+
} catch (err) {
|
|
234
|
+
res.status(500).end(JSON.stringify({ error: err.message }))
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
239
|
+
app.listen(PORT, () => {
|
|
240
|
+
console.log(`✅ node-agent running on port ${PORT}`)
|
|
241
|
+
})
|
package/nodemon.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
{
|
|
3
|
+
"name": "analytics-node-agent",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node index.js",
|
|
9
|
+
"dev": "nodemon index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"express": "^4.18.2",
|
|
13
|
+
"systeminformation": "^5.22.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"nodemon": "^3.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|