@bitcall/pm2-pulse-agent 1.0.1-beta.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/README.md +1 -0
- package/agent.js +179 -0
- package/monitoring.proto +32 -0
- package/package.json +14 -0
- package/scripts/setup-agent.js +72 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MONITORING-SERVICE
|
package/agent.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const { exec } = require('child_process')
|
|
3
|
+
const grpc = require('@grpc/grpc-js')
|
|
4
|
+
const protoLoader = require('@grpc/proto-loader')
|
|
5
|
+
require('dotenv').config()
|
|
6
|
+
|
|
7
|
+
// Fixed agent defaults (only token + thresholds are configurable)
|
|
8
|
+
const DEFAULT_SERVER = 'localhost:50051'
|
|
9
|
+
const DEFAULT_VPS_ID = 'server-' + os.hostname()
|
|
10
|
+
const DEFAULT_CHECK_INTERVAL = 10 // seconds (accurate CPU measurement)
|
|
11
|
+
const DEFAULT_CPU_THRESHOLD = 70
|
|
12
|
+
const DEFAULT_RAM_THRESHOLD = 80
|
|
13
|
+
const DEFAULT_AUTH_TOKEN = process.env.AUTH_TOKEN || ''
|
|
14
|
+
|
|
15
|
+
class MonitoringAgent {
|
|
16
|
+
static instance = null
|
|
17
|
+
|
|
18
|
+
static getInstance() {
|
|
19
|
+
if (!MonitoringAgent.instance) {
|
|
20
|
+
MonitoringAgent.instance = new MonitoringAgent()
|
|
21
|
+
}
|
|
22
|
+
return MonitoringAgent.instance
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
if (MonitoringAgent.instance) {
|
|
27
|
+
throw new Error('MonitoringAgent is a singleton. Use MonitoringAgent.getInstance().')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.server = process.env.MONITOR_SERVER || DEFAULT_SERVER
|
|
31
|
+
this.vpsId = process.env.VPS_ID || DEFAULT_VPS_ID
|
|
32
|
+
this.checkInterval = DEFAULT_CHECK_INTERVAL
|
|
33
|
+
this.authToken = DEFAULT_AUTH_TOKEN
|
|
34
|
+
this.cpuThreshold = parseInt(process.env.CPU_THRESHOLD, 10) || DEFAULT_CPU_THRESHOLD
|
|
35
|
+
this.ramThreshold = parseInt(process.env.RAM_THRESHOLD, 10) || DEFAULT_RAM_THRESHOLD
|
|
36
|
+
|
|
37
|
+
const credentials = grpc.credentials.createInsecure()
|
|
38
|
+
this.client = new proto.MonitoringService(this.server, credentials)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getCpu() {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
const startSnapshot = os.cpus()
|
|
44
|
+
const startTotals = startSnapshot.map(c => Object.values(c.times).reduce((a, b) => a + b))
|
|
45
|
+
const startIdle = startSnapshot.reduce((acc, c) => acc + c.times.idle, 0)
|
|
46
|
+
|
|
47
|
+
// Wait 2 seconds for accurate measurement
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
const endSnapshot = os.cpus()
|
|
50
|
+
const endTotals = endSnapshot.map(c => Object.values(c.times).reduce((a, b) => a + b))
|
|
51
|
+
const endIdle = endSnapshot.reduce((acc, c) => acc + c.times.idle, 0)
|
|
52
|
+
|
|
53
|
+
const totalStart = startTotals.reduce((a, b) => a + b, 0)
|
|
54
|
+
const totalEnd = endTotals.reduce((a, b) => a + b, 0)
|
|
55
|
+
|
|
56
|
+
const idleDiff = endIdle - startIdle
|
|
57
|
+
const totalDiff = totalEnd - totalStart
|
|
58
|
+
|
|
59
|
+
const usage = totalDiff > 0 ? 100 - (100 * idleDiff) / totalDiff : 0
|
|
60
|
+
resolve(Math.max(0, Math.min(100, Math.round(usage))))
|
|
61
|
+
}, 2000)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getRamStats() {
|
|
66
|
+
const totalBytes = os.totalmem()
|
|
67
|
+
const freeBytes = os.freemem()
|
|
68
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes)
|
|
69
|
+
const percent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0
|
|
70
|
+
return {
|
|
71
|
+
percent,
|
|
72
|
+
usedMb: Math.round(usedBytes / 1024 / 1024),
|
|
73
|
+
totalMb: Math.round(totalBytes / 1024 / 1024)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getPm2() {
|
|
78
|
+
return new Promise(resolve => {
|
|
79
|
+
exec('pm2 jlist', (err, stdout) => {
|
|
80
|
+
if (err) return resolve([])
|
|
81
|
+
const list = JSON.parse(stdout)
|
|
82
|
+
resolve(
|
|
83
|
+
list.map(p => ({
|
|
84
|
+
name: p.name,
|
|
85
|
+
status: p.pm2_env?.status || 'unknown',
|
|
86
|
+
restarts: p.pm2_env?.restart_time || 0,
|
|
87
|
+
cpu_percent: p.monit?.cpu || 0,
|
|
88
|
+
memory_mb: p.monit?.memory ? Math.round(p.monit.memory / 1024 / 1024) : 0
|
|
89
|
+
}))
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async sendMetrics(isUrgent = false) {
|
|
96
|
+
const cpu = await this.getCpu()
|
|
97
|
+
const ramStats = this.getRamStats()
|
|
98
|
+
const ramPercent = ramStats.percent
|
|
99
|
+
const processes = await this.getPm2()
|
|
100
|
+
|
|
101
|
+
const alerts = []
|
|
102
|
+
if (cpu > this.cpuThreshold) alerts.push(`High CPU: ${cpu}%`)
|
|
103
|
+
if (ramPercent > this.ramThreshold) alerts.push(`High RAM: ${ramPercent}%`)
|
|
104
|
+
processes.forEach(p => {
|
|
105
|
+
if (p.status !== 'online') alerts.push(`${p.name} is ${p.status}`)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const hasAlerts = alerts.length > 0
|
|
109
|
+
console.log(
|
|
110
|
+
`[${new Date().toLocaleTimeString()}] CPU: ${cpu}% | RAM: ${ramStats.usedMb}/${ramStats.totalMb}MB (${ramPercent}%) | Alerts: ${alerts.length}${
|
|
111
|
+
hasAlerts ? ' 🚨' : ''
|
|
112
|
+
}`
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const metricsMessage = {
|
|
116
|
+
vps_id: this.vpsId,
|
|
117
|
+
cpu,
|
|
118
|
+
ram: ramPercent,
|
|
119
|
+
ram_used_mb: ramStats.usedMb,
|
|
120
|
+
ram_total_mb: ramStats.totalMb,
|
|
121
|
+
processes,
|
|
122
|
+
alerts,
|
|
123
|
+
urgent: isUrgent || hasAlerts,
|
|
124
|
+
timestamp: Date.now()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('DEBUG - Sending message:', JSON.stringify(metricsMessage, null, 2))
|
|
128
|
+
|
|
129
|
+
// Add auth token to metadata if configured
|
|
130
|
+
const metadata = new grpc.Metadata()
|
|
131
|
+
if (this.authToken) {
|
|
132
|
+
metadata.add('authorization', `Bearer ${this.authToken}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.client.SendMetrics(metricsMessage, metadata, (err, response) => {
|
|
136
|
+
if (err) console.error('Error:', err.message)
|
|
137
|
+
else if (hasAlerts) console.log('✓ Alert sent to server')
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
start() {
|
|
142
|
+
const intervalSeconds = parseInt(this.checkInterval, 10)
|
|
143
|
+
console.log('='.repeat(60))
|
|
144
|
+
console.log('🚀 PM2 Monitoring Agent Started')
|
|
145
|
+
console.log('='.repeat(60))
|
|
146
|
+
console.log(`VPS ID: ${this.vpsId}`)
|
|
147
|
+
console.log(`Server: ${this.server}`)
|
|
148
|
+
console.log(`Authentication: ${this.authToken ? '✓ Enabled' : '✗ Disabled'}`)
|
|
149
|
+
console.log(`Check interval: ${intervalSeconds} second(s)`)
|
|
150
|
+
console.log(`CPU Threshold: ${this.cpuThreshold}%`)
|
|
151
|
+
console.log(`RAM Threshold: ${this.ramThreshold}%`)
|
|
152
|
+
console.log('='.repeat(60) + '\n')
|
|
153
|
+
|
|
154
|
+
// Run once on start
|
|
155
|
+
this.sendMetrics()
|
|
156
|
+
|
|
157
|
+
// Then run on interval
|
|
158
|
+
setInterval(async () => {
|
|
159
|
+
await this.sendMetrics()
|
|
160
|
+
}, intervalSeconds * 1000)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Load proto
|
|
165
|
+
const proto = grpc.loadPackageDefinition(
|
|
166
|
+
protoLoader.loadSync('monitoring.proto', {
|
|
167
|
+
keepCase: true,
|
|
168
|
+
longs: String,
|
|
169
|
+
enums: String,
|
|
170
|
+
defaults: true,
|
|
171
|
+
oneofs: true
|
|
172
|
+
})
|
|
173
|
+
).monitoring
|
|
174
|
+
|
|
175
|
+
module.exports = MonitoringAgent
|
|
176
|
+
|
|
177
|
+
if (require.main === module) {
|
|
178
|
+
MonitoringAgent.getInstance().start()
|
|
179
|
+
}
|
package/monitoring.proto
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package monitoring;
|
|
4
|
+
|
|
5
|
+
service MonitoringService {
|
|
6
|
+
rpc SendMetrics(MetricsData) returns (Response);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
message MetricsData {
|
|
10
|
+
string vps_id = 1;
|
|
11
|
+
double cpu = 2;
|
|
12
|
+
double ram = 3;
|
|
13
|
+
double ram_used_mb = 8;
|
|
14
|
+
double ram_total_mb = 9;
|
|
15
|
+
repeated Process processes = 4;
|
|
16
|
+
repeated string alerts = 5;
|
|
17
|
+
bool urgent = 6;
|
|
18
|
+
int64 timestamp = 7;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
message Process {
|
|
22
|
+
string name = 1;
|
|
23
|
+
string status = 2;
|
|
24
|
+
int32 restarts = 3;
|
|
25
|
+
double cpu_percent = 4;
|
|
26
|
+
int64 memory_mb = 5;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
message Response {
|
|
30
|
+
bool success = 1;
|
|
31
|
+
string message = 2;
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bitcall/pm2-pulse-agent",
|
|
3
|
+
"version": "1.0.1-beta.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"agent": "node agent.js",
|
|
6
|
+
"monitor": "node agent.js",
|
|
7
|
+
"setup-agent": "node scripts/setup-agent.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@grpc/grpc-js": "^1.10.1",
|
|
11
|
+
"@grpc/proto-loader": "^0.7.10",
|
|
12
|
+
"dotenv": "^17.2.3"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const readline = require('readline')
|
|
4
|
+
|
|
5
|
+
const envPath = path.join(process.cwd(), '.env')
|
|
6
|
+
|
|
7
|
+
function loadEnvFile() {
|
|
8
|
+
if (!fs.existsSync(envPath)) return []
|
|
9
|
+
return fs.readFileSync(envPath, 'utf8').split(/\r?\n/)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getEnvValue(lines, key) {
|
|
13
|
+
const prefix = `${key}=`
|
|
14
|
+
const line = lines.find(l => l.startsWith(prefix))
|
|
15
|
+
if (!line) return ''
|
|
16
|
+
return line.slice(prefix.length)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function upsertEnvValue(lines, key, value) {
|
|
20
|
+
const prefix = `${key}=`
|
|
21
|
+
const idx = lines.findIndex(l => l.startsWith(prefix))
|
|
22
|
+
const line = `${key}=${value}`
|
|
23
|
+
if (idx === -1) {
|
|
24
|
+
lines.push(line)
|
|
25
|
+
} else {
|
|
26
|
+
lines[idx] = line
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function askQuestion(rl, prompt) {
|
|
31
|
+
return new Promise(resolve => {
|
|
32
|
+
rl.question(prompt, answer => resolve(answer.trim()))
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function run() {
|
|
37
|
+
const lines = loadEnvFile()
|
|
38
|
+
const currentToken = getEnvValue(lines, 'AUTH_TOKEN')
|
|
39
|
+
const currentServer = getEnvValue(lines, 'MONITOR_SERVER') || 'localhost:50051'
|
|
40
|
+
const currentVpsId = getEnvValue(lines, 'VPS_ID') || `server-${require('os').hostname()}`
|
|
41
|
+
|
|
42
|
+
const rl = readline.createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const tokenPrompt = `Secret token (AUTH_TOKEN) [${currentToken ? 'set' : 'empty'}]: `
|
|
48
|
+
const serverPrompt = `Monitor server (MONITOR_SERVER) [${currentServer}]: `
|
|
49
|
+
const vpsPrompt = `Default VPS ID (VPS_ID) [${currentVpsId}]: `
|
|
50
|
+
|
|
51
|
+
const tokenInput = await askQuestion(rl, tokenPrompt)
|
|
52
|
+
const serverInput = await askQuestion(rl, serverPrompt)
|
|
53
|
+
const vpsInput = await askQuestion(rl, vpsPrompt)
|
|
54
|
+
|
|
55
|
+
rl.close()
|
|
56
|
+
|
|
57
|
+
const finalToken = tokenInput !== '' ? tokenInput : currentToken
|
|
58
|
+
const finalServer = serverInput !== '' ? serverInput : currentServer
|
|
59
|
+
const finalVpsId = vpsInput !== '' ? vpsInput : currentVpsId
|
|
60
|
+
|
|
61
|
+
upsertEnvValue(lines, 'AUTH_TOKEN', finalToken)
|
|
62
|
+
upsertEnvValue(lines, 'MONITOR_SERVER', finalServer)
|
|
63
|
+
upsertEnvValue(lines, 'VPS_ID', finalVpsId)
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(envPath, lines.join('\n'))
|
|
66
|
+
console.log(`Saved: ${envPath}`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
run().catch(err => {
|
|
70
|
+
console.error('Setup failed:', err.message)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
})
|