ConnectivityExporter/src/index.ts

265 lines
7.2 KiB
TypeScript

import * as client from "prom-client";
import * as yaml from "yaml";
import { readFileSync, watchFile } from "fs";
import * as net from "net";
import * as ping from "ping";
import * as Traceroute from "nodejs-traceroute";
import * as argparse from "argparse";
import { strict as assert } from "assert";
const argp = new argparse.ArgumentParser();
argp.addArgument(["-c", "--config"], {
help: "Path of the config file",
type: String,
required: true,
dest: "config",
});
argp.addArgument(["-p", "--port"], {
help: "The port to listen at",
type: Number,
defaultValue: 9285,
dest: "port",
});
const args: {
config: string;
port: number;
} = argp.parseArgs();
async function testPing(host: string) {
let probe = await ping.promise.probe(host, {
timeout: 5,
});
if (!probe.alive) return -1;
return Number(probe.avg);
}
async function testTraceroute(host: string) {
const tracer = new Traceroute();
return new Promise((yes, no) => {
console.log("Tracepath to:", host);
tracer
.on("pid", (pid) => {
console.log(`pid: ${pid}`);
})
.on("destination", (destination) => {
console.log(`destination: ${destination}`);
})
.on("hop", (hop) => {
console.log(`hop: ${JSON.stringify(hop)}`);
})
.on("close", (code) => {
console.log(`close: code ${code}`);
})
.on("error", no);
tracer.trace("github.com");
});
}
async function testTCP(host: string, port: number) {
return new Promise((yes, no) => {
console.log("Checking TCP", host, port);
let socket = net.connect(port, host);
socket.on("error", () => {
yes(false);
socket.destroy();
});
socket.on("connect", () => {
yes(true);
socket.destroy();
});
});
}
let getMetrics: () => Promise<string>;
type IConfig = {
targets: {
host: string;
icmp: boolean;
traceroute: boolean;
tcp: (number | { port: number })[];
}[];
};
function load() {
let config: IConfig;
try {
console.log("Loading: ", args.config);
config = yaml.parse(readFileSync(args.config, "utf8"));
// Verify config
assert(typeof config === "object", "Config is invalid!");
assert(typeof config.targets !== "undefined", "Field targets required!");
assert(Array.isArray(config.targets), "Field targets must be an array");
config.targets.forEach((target, idx) => {
assert(typeof target === "object", "Each target must be an object!");
assert(
typeof target.host === "string",
`The host must be a string in the ${idx} target`
);
assert(
typeof target.icmp === "undefined" ||
typeof target.icmp === "boolean",
`Field icmp must be undefined or boolean in target ${target.host}`
);
assert(
typeof target.traceroute === "undefined" ||
typeof target.traceroute === "boolean",
`Field traceroute must be undefined or boolean in target ${target.host}`
);
assert(
typeof target.tcp === "undefined" || Array.isArray(target.tcp),
`Field tcp must be undefined or array in target ${target.host}`
);
if (target.tcp) {
target.tcp.forEach((tcp) => {
assert(
typeof tcp === "number" || typeof tcp === "object",
"Each entry in TCP has to be a number or an object"
);
if (typeof tcp === "object") {
assert(
typeof tcp.port === "number",
"The port in TCP is required!"
);
}
});
}
});
} catch (err) {
console.log(err);
return;
}
console.log("Config verified. Putting in production");
// Enable config
const reg = new client.Registry();
const reachable = new client.Gauge({
name: "connectivity_ping_reachable",
help: "Checks if the target is pingable by this host",
labelNames: ["target"],
registers: [reg],
});
const pingTimes = new client.Gauge({
name: "connectivity_ping_time",
help: "Contains the current ping time from this host to target",
labelNames: ["target"],
registers: [reg],
});
const hopCount = new client.Gauge({
name: "connectivity_hops_count",
help: "The number of hops between this host and the target",
labelNames: ["target"],
registers: [reg],
});
const tcpCheck = new client.Gauge({
name: "connectivity_tcp_available",
help: "Checks if connectivity to specified tcp port is available",
labelNames: ["target", "port"],
registers: [reg],
});
getMetrics = async () => {
console.log("Running");
await Promise.all(
config.targets.map(async (target) => {
let jobs = [];
if (target.icmp) {
jobs.push(
testPing(target.host).then((ping) => {
reachable.set(
{
target: target.host,
},
ping >= 0 ? 1 : 0
);
pingTimes.set(
{
target: target.host,
},
ping as any
);
})
);
}
if (target.traceroute && false) {
//Currently not working
jobs.push(testTraceroute(target.host).then(() => {}));
}
if (target.tcp) {
jobs.push(
...target.tcp.map((check) =>
testTCP(
target.host,
typeof check === "number" ? check : check.port
).then((avail) => {
tcpCheck.set(
{
target: target.host,
port:
typeof check === "number" ? check : check.port,
},
avail ? 1 : 0
);
})
)
);
}
return Promise.all(jobs);
})
);
return reg.metrics();
};
}
process.on("SIGHUP", () => load());
watchFile(args.config, load);
load();
import * as http from "http";
import { URL } from "url";
http
.createServer(async (req, res) => {
const url = new URL(req.url, "http://127.0.0.1");
if (url.pathname == "/metrics") {
if (getMetrics)
getMetrics()
.then((data) => res.end(data))
.catch((err) => {
console.log("Error", err);
res.statusCode = 400;
res.end("Something went wrong!");
});
else {
res.statusCode = 500;
res.end("Not ready!");
}
} else {
res.end("<a href='/metrics'>Metrics</a>");
}
})
.listen(args.port, () => {
console.log("Listening at:", args.port);
});