Created
July 5, 2021 11:15
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Config example:
error.png

ok.png
