This commit is contained in:
root 2025-05-01 09:44:32 +02:00
commit e05db3ba4d
5 changed files with 313 additions and 42 deletions

3
.gitignore vendored
View file

@ -137,3 +137,6 @@ dist
# pm2 ecosystem file
ecosystem.config.js
# firebase service file
firebase-service-account.json

View file

@ -3,6 +3,19 @@ const ping = require("ping");
const pm2 = require("pm2");
const fs = require("fs");
const path = require("path");
const axios = require("axios");
const admin = require("firebase-admin");
// Initialize Firebase Admin SDK
try {
const serviceAccount = require("../../firebase-service-account.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
console.log("Firebase Admin SDK initialized successfully");
} catch (error) {
console.error("Error initializing Firebase Admin SDK:", error);
}
const router = express.Router();
@ -30,6 +43,10 @@ function ensureLogDirectories() {
}
}
// Track previous states for notifications
let previousServersStatus = {};
let previousPM2Status = {};
let serversStatus = {};
REMOTE_SERVERS.forEach(server => {
serversStatus[server.name] = {
@ -37,11 +54,41 @@ REMOTE_SERVERS.forEach(server => {
lastChecked: null,
responseTime: null,
};
// Initialize previous status
previousServersStatus[server.name] = false;
});
// Add PM2 services status object
let pm2ServicesStatus = {};
// Function to send FCM notification
async function sendFCMNotification(message, topic) {
try {
if (!admin.apps.length) {
console.warn("Firebase Admin not initialized, skipping notification");
return;
}
// Create the message object according to Firebase Admin SDK format
const fcmMessage = {
topic: topic,
notification: {
title: 'Server Status Alert',
body: message
},
data: {
type: 'server_status',
timestamp: Date.now().toString()
}
};
await admin.messaging().send(fcmMessage);
console.log(`Notification sent: ${message}`);
} catch (error) {
console.error('Error sending notification:', error);
}
}
async function checkServers() {
try {
ensureLogDirectories();
@ -51,13 +98,37 @@ async function checkServers() {
const res = await ping.promise.probe(server.host, {
timeout: 4, // Set a timeout of 4 seconds
});
serversStatus[server.name].online = res.alive;
// Get previous status before updating
const wasOnline = previousServersStatus[server.name];
const isNowOnline = res.alive;
// Update status
serversStatus[server.name].online = isNowOnline;
serversStatus[server.name].responseTime = res.time;
// Send notifications for status changes
if (wasOnline === false && isNowOnline) {
await sendFCMNotification(`Server ${server.name} is back online`, 'service_online');
} else if (wasOnline === true && !isNowOnline) {
await sendFCMNotification(`Server ${server.name} is offline`, 'service_offline');
}
// Update previous status
previousServersStatus[server.name] = isNowOnline;
} catch (error) {
console.error(`Error pinging ${server.host}:`, error);
serversStatus[server.name].online = false;
serversStatus[server.name].responseTime = null;
// Check if status changed from online to offline
if (previousServersStatus[server.name] === true) {
await sendFCMNotification(`Server ${server.name} is unreachable`, 'service_offline');
previousServersStatus[server.name] = false;
}
}
serversStatus[server.name].lastChecked = new Date().toISOString();
// Log server status to the appropriate folder
@ -85,47 +156,73 @@ async function checkServers() {
}
async function checkPM2Services() {
return new Promise((resolve, reject) => {
pm2.connect(function(err) {
if (err) {
console.error('Error connecting to PM2:', err);
pm2.disconnect();
resolve();
return;
}
return new Promise((resolve, reject) => {
pm2.connect(function(err) {
if (err) {
console.error('Error connecting to PM2:', err);
pm2.disconnect();
resolve();
return;
}
pm2.list((err, list) => {
if (err) {
console.error('Error getting PM2 process list:', err);
pm2.disconnect();
resolve();
return;
}
pm2.list(async (err, list) => {
if (err) {
console.error('Error getting PM2 process list:', err);
pm2.disconnect();
resolve();
return;
}
// Update PM2 services status
list.forEach(process => {
// Calculate uptime correctly - pm_uptime is a timestamp, not a duration
const uptimeMs = process.pm2_env.pm_uptime ?
Date.now() - process.pm2_env.pm_uptime :
null;
try {
// Process each PM2 service sequentially with proper async handling
for (const process of list) {
const uptimeMs = process.pm2_env.pm_uptime ?
Date.now() - process.pm2_env.pm_uptime :
null;
pm2ServicesStatus[process.name] = {
name: process.name,
id: process.pm_id,
status: process.pm2_env.status,
cpu: process.monit ? process.monit.cpu : null,
memory: process.monit ? process.monit.memory : null,
uptime: uptimeMs, // Store the uptime in milliseconds
restarts: process.pm2_env.restart_time,
lastChecked: new Date().toISOString()
};
});
const processName = process.name;
const isNowOnline = process.pm2_env.status === 'online';
pm2.disconnect();
resolve();
});
});
});
// Check if we've seen this process before
if (previousPM2Status[processName] === undefined) {
// First time seeing this process - initialize and don't send notification
previousPM2Status[processName] = isNowOnline;
console.log(`Initializing PM2 service status for ${processName}: ${isNowOnline ? 'online' : 'offline'}`);
}
// Check if status changed
else if (previousPM2Status[processName] === false && isNowOnline) {
await sendFCMNotification(`PM2 service ${processName} is back online`, 'service_online');
console.log(`PM2 service ${processName} changed from offline to online`);
}
else if (previousPM2Status[processName] === true && !isNowOnline) {
await sendFCMNotification(`PM2 service ${processName} is offline (status: ${process.pm2_env.status})`, 'service_offline');
console.log(`PM2 service ${processName} changed from online to ${process.pm2_env.status}`);
}
// Update previous status
previousPM2Status[processName] = isNowOnline;
// Update status object
pm2ServicesStatus[processName] = {
name: processName,
id: process.pm_id,
status: process.pm2_env.status,
cpu: process.monit ? process.monit.cpu : null,
memory: process.monit ? process.monit.memory : null,
uptime: uptimeMs,
restarts: process.pm2_env.restart_time,
lastChecked: new Date().toISOString()
};
}
} catch (error) {
console.error('Error processing PM2 services:', error);
}
pm2.disconnect();
resolve();
});
});
});
}
async function checkAll() {

107
package-lock.json generated
View file

@ -15,13 +15,17 @@
"dotenv": "^16.5.0",
"express": "^4.21.2",
"firebase-admin": "^13.3.0",
<<<<<<< HEAD
"googleapis": "^148.0.0",
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"nodejs": "^0.0.0",
"ping": "^0.4.4",
"pm2": "^6.0.5",
"whois-json": "^2.0.4"
}
},
<<<<<<< HEAD
"node_modules/@discordjs/builders": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.1.tgz",
@ -149,6 +153,8 @@
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/@fastify/busboy": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz",
@ -884,6 +890,7 @@
"license": "BSD-3-Clause",
"optional": true
},
<<<<<<< HEAD
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
@ -917,6 +924,8 @@
"npm": ">=7.0.0"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -1019,9 +1028,15 @@
"license": "MIT"
},
"node_modules/@types/node": {
<<<<<<< HEAD
"version": "22.15.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
"integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
=======
"version": "22.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
"integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -1097,6 +1112,7 @@
"license": "MIT",
"optional": true
},
<<<<<<< HEAD
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@ -1116,6 +1132,8 @@
"npm": ">=7.0.0"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -2061,7 +2079,12 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
<<<<<<< HEAD
"license": "MIT"
=======
"license": "MIT",
"optional": true
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
},
"node_modules/fast-json-patch": {
"version": "3.1.1",
@ -2173,6 +2196,7 @@
"@google-cloud/storage": "^7.14.0"
}
},
<<<<<<< HEAD
"node_modules/firebase-admin/node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@ -2186,6 +2210,8 @@
"uuid": "dist/esm/bin/uuid"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
@ -2280,6 +2306,22 @@
"node": ">=14"
}
},
<<<<<<< HEAD
=======
"node_modules/gaxios/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
@ -2440,6 +2482,23 @@
"node": ">=14"
}
},
<<<<<<< HEAD
=======
"node_modules/google-gax/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"optional": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
@ -2449,6 +2508,7 @@
"node": ">=14"
}
},
<<<<<<< HEAD
"node_modules/googleapis": {
"version": "148.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-148.0.0.tgz",
@ -2479,6 +2539,8 @@
"node": ">=14.0.0"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -3038,12 +3100,15 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
<<<<<<< HEAD
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"license": "MIT"
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@ -3097,12 +3162,15 @@
"node": ">=10"
}
},
<<<<<<< HEAD
"node_modules/magic-bytes.js": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
"license": "MIT"
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -4577,6 +4645,23 @@
"license": "MIT",
"optional": true
},
<<<<<<< HEAD
=======
"node_modules/teeny-request/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"optional": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/title-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz",
@ -4613,12 +4698,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
<<<<<<< HEAD
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -4670,6 +4758,7 @@
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
"license": "MIT"
},
<<<<<<< HEAD
"node_modules/undici": {
"version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
@ -4679,6 +4768,8 @@
"node": ">=18.17"
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@ -4708,12 +4799,15 @@
"upper-case": "^1.1.1"
}
},
<<<<<<< HEAD
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"license": "BSD"
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -4730,16 +4824,26 @@
}
},
"node_modules/uuid": {
<<<<<<< HEAD
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
=======
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
<<<<<<< HEAD
"uuid": "dist/bin/uuid"
=======
"uuid": "dist/esm/bin/uuid"
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
}
},
"node_modules/vary": {
@ -4867,6 +4971,7 @@
"license": "ISC",
"optional": true
},
<<<<<<< HEAD
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
@ -4888,6 +4993,8 @@
}
}
},
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",

View file

@ -26,7 +26,10 @@
"dotenv": "^16.5.0",
"express": "^4.21.2",
"firebase-admin": "^13.3.0",
<<<<<<< HEAD
"googleapis": "^148.0.0",
=======
>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8
"nodejs": "^0.0.0",
"ping": "^0.4.4",
"pm2": "^6.0.5",

61
test-firebase.js Normal file
View file

@ -0,0 +1,61 @@
const admin = require('firebase-admin');
const path = require('path');
// Get path to service account file
const serviceAccountPath = path.join(__dirname, 'firebase-service-account.json');
// Log the path to verify
console.log(`Loading Firebase service account from: ${serviceAccountPath}`);
// Initialize Firebase Admin SDK
try {
const serviceAccount = require(serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
console.log("Firebase Admin SDK initialized successfully");
// Function to send FCM notification
async function sendTestNotification() {
try {
// Send to both topics to test both scenarios
const topics = ['service_online', 'service_offline'];
for (const topic of topics) {
// Correctly format the message for Firebase Cloud Messaging
const message = {
topic: topic,
notification: {
title: 'Test Notification',
body: `This is a test notification to the ${topic} topic`
},
data: {
type: 'test',
timestamp: Date.now().toString()
}
};
console.log(`Sending test notification to topic: ${topic}`);
// Use the send method instead of sendToTopic
const response = await admin.messaging().send(message);
console.log(`Successfully sent message to ${topic}:`, response);
}
console.log("All test notifications sent successfully!");
process.exit(0);
} catch (error) {
console.error('Error sending notification:', error);
process.exit(1);
}
}
// Execute the test
sendTestNotification();
} catch (error) {
console.error("Error initializing Firebase Admin SDK:", error);
console.error("Make sure firebase-service-account.json exists in the repository root");
process.exit(1);
}