Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@szkrd
Created July 5, 2021 11:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save szkrd/afa6db4e0f916fa8e06c56df45a3b450 to your computer and use it in GitHub Desktop.
Save szkrd/afa6db4e0f916fa8e06c56df45a3b450 to your computer and use it in GitHub Desktop.
watch for changes in a log file in a memory efficient way (like tail -f)
// npm init --yes && npm i -S chalk chokidar split
const fs = require("fs");
const { stat } = require("fs").promises;
const { red, cyan } = require("chalk");
const chokidar = require("chokidar");
const split = require("split");
let config = require("./config.json"); // { okPattern: string, nokPattern: string, logFile: string }
try {
config = require("./config.user.json");
} catch (err) {}
const statuses = { unchecked: 0, connected: 1, disconnected: 2 };
let lastFileSize = 0;
let lastCumulatedStatus = statuses.unchecked;
const operations = [];
const kilo = (num = 0) => Math.round(num / 1024) + "k";
function getCumulatedStatus() {
const opSorted = operations.slice().sort((a, b) => a.fileSize - b.fileSize);
let opStatus = statuses.unchecked;
opSorted.forEach((op) => {
if (op.status !== statuses.unchecked) opStatus = op.status;
});
return opStatus;
}
function handleStatusChange(current = statuses.unchecked) {
if (current === statuses.connected) {
console.log(cyan("CONNECTED"));
}
if (current === statuses.disconnected) {
console.log(red("DISCONNECTED"));
}
}
async function onChange(event, path) {
console.log(`event: ${event}`);
const logStat = await stat(path);
const fileSize = logStat.size;
const changeSize = fileSize - lastFileSize;
lastFileSize = fileSize;
if (changeSize <= 0) return;
if (event === "unlink") {
lastFileSize = 0;
operations.length = 0;
}
console.log(`size: ${kilo(logStat.size)} | diff: ${kilo(changeSize)}`);
let lineCount = 0;
const operation = {
started: Date.now(),
status: statuses.unchecked,
finished: null,
fileSize: fileSize,
};
// stream reader and split example is from
// https://sanori.github.io/2019/03/Line-by-line-Processing-in-node-js/
fs.createReadStream(path, {
flags: "rs",
autoClose: true,
start: fileSize - changeSize,
end: fileSize,
highWaterMark: 1024,
})
.pipe(split())
.on("data", (line) => {
if (line.includes(config.okPattern))
operation.status = statuses.connected;
if (line.includes(config.nokPattern))
operation.status = statuses.disconnected;
lineCount++;
})
.on("end", () => {
operation.finished = Date.now();
operations.push(operation);
const cumulatedStatus = getCumulatedStatus();
if (lastCumulatedStatus !== cumulatedStatus)
handleStatusChange(cumulatedStatus);
lastCumulatedStatus = cumulatedStatus;
console.log(
`lines processed: ${lineCount} | status: ${operation.status} | cumulatedStatus: ${cumulatedStatus}`
);
});
}
function main() {
const fn = config.logFile;
console.log(`watching: "${fn}"`);
try {
// polling is not nice, but (probably using stat) this will trigger a flush
// (on the OS level) to the checked file, without polling we would have stale data
chokidar
.watch(fn, { usePolling: true, interval: 2000 })
.on("all", onChange);
} catch (err) {
console.error(red("unhandled exception:"), error);
}
}
// ---
main();
@szkrd
Copy link
Author

szkrd commented Jan 28, 2022

Config example:

{
  "logFile": "c:\\Program Files\\Palo Alto Networks\\GlobalProtect\\pan_gp_event.log",
  "changeWallpaper": true,
  "okImage": "ok.png",
  "nokImage": "error.png",
  "okPattern": ["Gateway login finished"],
  "nokPattern": ["Tunnel is down", "User was logged out of Gateway"]
}

error.png
error

ok.png
ok

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment