feat: eval in ping #236 and port number in ping #211

This commit is contained in:
Raj Nandan Sharma
2025-02-04 10:34:00 +05:30
parent 5ddddf8b5d
commit 8404415a93
7 changed files with 294 additions and 79 deletions
+2 -1
View File
@@ -26,4 +26,5 @@ uploads/*
static/uploads/*
!static/uploads/upload.dir
temp.txt
temp.txt
temp.js
+110
View File
@@ -24,3 +24,113 @@ You can add as many IP addresses as you want to monitor. The IP address should b
<p class="note danger">
Please note that atleast one of the Host V4 or Host V6 is required.
<p>
## Eval
The eval is used to define the JavaScript code that should be used to evaluate the response. It is optional and has be a valid JavaScript code.
This is an anonymous JS function, it should return a **Promise**, that resolves or rejects to `{status, latency}`, by default it looks like this.
> **_NOTE:_** The eval function should always return a json object. The json object can have only status(UP/DOWN/DEGRADED) and latency(number)
> `{status:"DEGRADED", latency: 200}`.
```javascript
(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
return {
status: alive ? "UP" : "DOWN",
latency: parseInt(latencyTotal / arrayOfPings.length)
};
});
```
- `responseDataBase64` **REQUIRED** is a string. It is the base64 encoded response data. To use it you will have to decode it and the JSON parse it. Once parse it will be an array of objects.
```js
let decodedResp = atob(responseDataBase64);
let jsonResp = JSON.parse(decodedResp);
console.log(jsonResp);
/*
[
{
"host": "smtp.resend.com",
"port": 587,
"type": "IP4",
"status": "open",
"latency": 36.750917
},
{
"host": "66.51.120.219",
"port": 465,
"type": "IP4",
"status": "open",
"latency": 27.782792
},
{
"host": "2606:4700:4700::1111",
"port": 443,
"status": "open",
"type": "IP6",
"latency": 5.684375
}
]
*/
```
### Understanding the Input
- `host`: The host that was pinged.
- `port`: The port that was pinged. Defaults to 80 if not provided.
- `type`: The type of IP address. Can be `IP4` or `IP6`.
- `status`: The status of the ping. Can be `open` , `error` or `timeout`.
- `open`: The host is reachable.
- `error`: There was an error while pinging the host.
- `timeout`: The host did not respond in time.
- `latency`: The time taken to ping the host. This is in milliseconds.
### Example
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
```javascript
(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let areAllOpen = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
let avgLatency = latencyTotal / arrayOfPings.length;
if (areAllOpen && avgLatency > 10) {
return {
status: "DEGRADED",
latency: avgLatency
};
}
return {
status: areAllOpen ? "UP" : "DOWN",
latency: avgLatency
};
});
```
+46 -14
View File
@@ -81,21 +81,11 @@
let invalidFormMessage = "";
async function isValidEval() {
async function isValidEval(ev) {
try {
// let evalResp = await eval(newMonitor.apiConfig.eval + `(200, 1000, "e30=")`);
new Function(newMonitor.apiConfig.eval);
new Function(ev);
return true; // The code is valid
// if (
// evalResp === undefined ||
// evalResp === null ||
// evalResp.status === undefined ||
// evalResp.status === null ||
// evalResp.latency === undefined ||
// evalResp.latency === null
// ) {
// return false;
// }
} catch (error) {
invalidFormMessage = error.message + " in eval.";
return false;
@@ -171,7 +161,7 @@
return;
}
if (!(await isValidEval())) {
if (!(await isValidEval(newMonitor.apiConfig.eval))) {
invalidFormMessage = invalidFormMessage + "Invalid eval";
return;
}
@@ -208,6 +198,27 @@
invalidFormMessage = "hostsV4 or hostsV6 is required";
return;
}
if (!!newMonitor.pingConfig.pingEval) {
newMonitor.pingConfig.pingEval = newMonitor.pingConfig.pingEval.trim();
if (newMonitor.pingConfig.pingEval.endsWith(";")) {
invalidFormMessage = "Eval should not end with semicolon";
return;
}
//has to start with ( and end with )
if (
!newMonitor.pingConfig.pingEval.startsWith("(") ||
!newMonitor.pingConfig.pingEval.endsWith(")")
) {
invalidFormMessage =
"Eval should start with ( and end with ). It is an anonymous function";
return;
}
if (!(await isValidEval(newMonitor.pingConfig.pingEval))) {
invalidFormMessage = invalidFormMessage + "Invalid eval";
return;
}
}
newMonitor.type_data = JSON.stringify(newMonitor.pingConfig);
} else if (newMonitor.monitor_type === "DNS") {
//validating host
@@ -620,7 +631,9 @@
<Label for="eval">Eval</Label>
<p class="my-1 text-xs text-muted-foreground">
You can write a custom eval function to evaluate the response. The
function should return an object with status and latency. <a
function should return a promise that resolves to an object with status
and latency. <a
target="_blank"
class="font-medium text-primary"
href="https://kener.ing/docs/monitors-api#eval">Read the docs</a
> to learn
@@ -714,6 +727,25 @@
</Button>
</div>
</div>
<div class="mt-2">
<Label for="pingEval">Eval</Label>
<p class="my-1 text-xs text-muted-foreground">
You can write a custom eval function to evaluate the response. The
function should return a promise that resolves to an object with
status and latency. <a
target="_blank"
class="font-medium text-primary"
href="https://kener.ing/docs/monitors-ping#eval"
>Read the docs</a
> to learn
</p>
<textarea
bind:value={newMonitor.pingConfig.pingEval}
id="pingEval"
class="h-96 w-full rounded-sm border p-2"
placeholder="Leave blank or write a custom eval function"
></textarea>
</div>
</div>
</div>
{:else if newMonitor.monitor_type == "DNS"}
+2 -1
View File
@@ -55,7 +55,8 @@
},
pingConfig: {
hostsV4: [],
hostsV6: []
hostsV6: [],
pingEval: ""
},
dnsConfig: {
host: "",
+75 -32
View File
@@ -1,6 +1,6 @@
// @ts-nocheck
import axios from "axios";
import ping from "ping";
import { Ping, ExtractIPv6HostAndPort } from "./ping.js";
import { UP, DOWN, DEGRADED } from "./constants.js";
import {
GetMinuteStartNowTimestampUTC,
@@ -49,6 +49,26 @@ const defaultEval = `(async function (statusCode, responseTime, responseData) {
}
})`;
const defaultPingEval = `(async function (responseDataBase64) {
let arrayOfPings = JSON.parse(atob(responseDataBase64));
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
return acc + ping.latency;
}, 0);
let alive = arrayOfPings.reduce((acc, ping) => {
if (ping.status === "open") {
return acc && true;
} else {
return false;
}
}, true);
return {
status: alive ? 'UP' : 'DOWN',
latency: parseInt(latencyTotal / arrayOfPings.length),
}
})`;
async function manualIncident(monitor) {
let startTs = GetMinuteStartNowTimestampUTC();
let incidentArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
@@ -97,50 +117,65 @@ async function manualIncident(monitor) {
return manualData;
}
const pingCall = async (hostsV4, hostsV6) => {
const pingCall = async (hostsV4, hostsV6, pingEval, tag) => {
if (hostsV4 === undefined) hostsV4 = [];
if (hostsV6 === undefined) hostsV6 = [];
let alive = true;
let latencyTotal = 0;
let countHosts = hostsV4.length + hostsV6.length;
let arrayOfPings = [];
for (let i = 0; i < hostsV4.length; i++) {
const host = hostsV4[i].trim();
const hostFull = hostsV4[i].trim();
let res;
let splitHost = hostFull.split(":");
let host = splitHost[0];
let port = 80;
if (splitHost.length > 1) {
port = parseInt(splitHost[1]);
}
try {
let res = await ping.promise.probe(host);
alive = alive && res.alive;
latencyTotal += res.time;
if (!res.alive) {
throw new Error(JSON.stringify(res));
}
res = await Ping(host, port, 3000);
} catch (error) {
alive = alive && false;
latencyTotal += 30;
console.log(`Error in pingCall IP4 for ${host}`, error);
console.log(`Error in pingCall IP4 for ${hostFull}`, error);
} finally {
arrayOfPings.push({
host: host,
port: port,
type: "IP4",
status: res.status,
latency: res.latency
});
}
}
for (let i = 0; i < hostsV6.length; i++) {
const host = hostsV6[i].trim();
const hostFull = hostsV6[i].trim();
let { host, port } = ExtractIPv6HostAndPort(hostFull);
let res;
try {
let res = await ping.promise.probe(host, {
v6: true,
timeout: false
});
alive = alive && res.alive;
if (!res.alive) {
throw new Error(JSON.stringify(res));
}
latencyTotal += res.time;
res = await Ping(host, port, 3000);
} catch (error) {
alive = alive && false;
latencyTotal += 30;
console.log(`Error in pingCall IP6 for ${host}`, error);
console.log(`Error in pingCall IP6 for ${hostFull}`, error);
} finally {
arrayOfPings.push({
host: host,
port: port,
status: res.status,
type: "IP6",
latency: res.latency
});
}
}
let respBase64 = Buffer.from(JSON.stringify(arrayOfPings)).toString("base64");
let evalResp = undefined;
try {
evalResp = await eval(pingEval + `("${respBase64}")`);
} catch (error) {
console.log(`Error in pingEval for ${tag}`, error.message);
}
//reduce to get the status
return {
status: alive ? UP : DOWN,
latency: parseInt(latencyTotal / countHosts),
status: evalResp.status,
latency: evalResp.latency,
type: REALTIME
};
};
@@ -376,7 +411,15 @@ const Minuter = async (monitor) => {
});
}
} else if (monitor.monitor_type === "PING") {
let pingResponse = await pingCall(monitor.type_data.hostsV4, monitor.type_data.hostsV6);
if (!!!monitor.type_data.pingEval) {
monitor.type_data.pingEval = defaultPingEval;
}
let pingResponse = await pingCall(
monitor.type_data.hostsV4,
monitor.type_data.hostsV6,
monitor.type_data.pingEval,
monitor.tag
);
realTimeData[startOfMinute] = pingResponse;
} else if (monitor.monitor_type === "DNS") {
const dnsResolver = new DNSResolver(monitor.type_data.nameServer);
-31
View File
@@ -78,34 +78,3 @@ class DNSResolver {
}
export default DNSResolver;
// const resolver = new DNSResolver();
// const domain = process.argv[2] || "google.com";
// const recordType = process.argv[3] || "A";
// resolver.getRecord(domain, recordType).then(
// function (records) {
// Object.entries(records).forEach(([type, records]) => {
// if (records.length === 0) return []; // Skip empty records
// return records;
// console.log(">>>>>>---- dns:167 ", records);
// console.log(`\n${type} Records:`);
// records.forEach((record) => {
// console.log("----------------------------------------");
// console.log(`Type: ${type}`);
// console.log(`Name: ${record.name}`);
// console.log(`TTL: ${record.ttl}`);
// // Format the output based on record type
// if (type === "MX") {
// console.log(`Priority: ${record.data.priority}`);
// console.log(`Exchange: ${record.data.exchange}`);
// } else {
// console.log(`Data: ${record.data}`);
// // console.log(">>>>>>---- dns-resolver:120 ", record);
// }
// });
// });
// },
// function (err) {
// console.error(err);
// }
// );
+59
View File
@@ -0,0 +1,59 @@
// @ts-nocheck
import net from "net"; // Use import instead of require
/**
* Check if a TCP port is open on a given IPv4/IPv6 host and measure latency.
*
* @param {string} host - The IP address or hostname (IPv4 or IPv6) to check.
* @param {number} port - The port number to check.
* @param {number} timeout - Connection timeout in milliseconds.
* @returns {Promise<{ status: string, latency: number }>} - Resolves to an object with status ("open" or "closed") and latency in ms.
*/
const Ping = function (host, port, timeout = 3000) {
return new Promise((resolve) => {
const socket = new net.Socket();
const start = process.hrtime.bigint(); // High-precision timestamp
let resolved = false;
const onFinish = (status) => {
if (!resolved) {
resolved = true;
const end = process.hrtime.bigint();
const latency = Number(end - start) / 1e6; // Convert nanoseconds to milliseconds
socket.destroy();
resolve({ status, latency });
}
};
socket.setTimeout(timeout);
socket.once("connect", () => onFinish("open"));
socket.once("timeout", () => onFinish("timeout"));
socket.once("error", () => onFinish("error"));
// Check if it's an IPv6 address (contains ':')
const options = host.includes(":") ? { host, port, family: 6 } : { host, port };
socket.connect(options);
});
};
/**
* @param {string} input
*/
function ExtractIPv6HostAndPort(input) {
const parts = input.split(":"); // Split by colons
// If there's a valid port at the end, extract it
const lastPart = parts[parts.length - 1];
const port = /^\d+$/.test(lastPart) ? parseInt(parts.pop(), 10) : null; // Check if last part is a number
// Reconstruct the IPv6 address
const host = parts.join(":");
// Ensure it's a valid IPv6 format
if (host.includes(":")) {
return { host, port }; // Port may be null if not present
}
return null; // Return null if the format is incorrect
}
export { Ping, ExtractIPv6HostAndPort };