Last active
June 19, 2025 06:44
-
-
Save ArunSelvan25/de914c1e1b4b8ac9d7e6ad69a60ad70d to your computer and use it in GitHub Desktop.
Minimal WebSocket Tail-F Server for Laravel Logs
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
import { WebSocketServer } from 'ws'; | |
import fs from 'fs'; | |
import path from 'path'; | |
import readline from 'readline'; | |
/***************************************************************************************** | |
* Minimal WebSocket‑based “tail‑f” server for Laravel log files | |
* ------------------------------------------------------------------ | |
* • One WebSocket per browser tab. | |
* • Client sends { event:'getLumenLog', data:{ date:'DD‑MM‑YYYY' } }. | |
* • Server: | |
* 1. Streams the entire file for that date. | |
* 2. Watches the file and pushes any new lines to the same socket. | |
* 3. Cleans up watcher on socket close or when the client switches dates. | |
*****************************************************************************************/ | |
const logDir = path.join(process.cwd(), 'storage', 'logs'); // Laravel log folder | |
const wss = new WebSocketServer({ port: 3001 }); // WS server | |
/* Map<ws, { filePath, watcher, lastSize }> keeps per‑client watch state */ | |
const clientWatchers = new Map(); | |
/** Convert DD‑MM‑YYYY → YYYY‑MM‑DD (local timezone safe) */ | |
function parseDateFromDDMMYYYY(dateStr) { | |
const [day, month, year] = dateStr.split('-').map(Number); | |
const localDate = new Date(year, month - 1, day); | |
// Format to YYYY-MM-DD in local time | |
const yyyy = localDate.getFullYear(); | |
const mm = String(localDate.getMonth() + 1).padStart(2, '0'); | |
const dd = String(localDate.getDate()).padStart(2, '0'); | |
const formattedDate = `${yyyy}-${mm}-${dd}`; | |
return formattedDate; | |
} | |
/** Build full log‑file path from DD‑MM‑YYYY */ | |
function getLogFilePathFromDate(dateStr) { | |
const date = parseDateFromDDMMYYYY(dateStr); | |
return path.join(logDir, `laravel-${date}.log`); | |
} | |
/*───────────────────────────────────────────────────────────────────────────* | |
* CORE: start / stop watching a log file for ONE WS client * | |
*───────────────────────────────────────────────────────────────────────────*/ | |
/** | |
* Begin streaming `filePath` to a specific WebSocket (`ws`). | |
* 1. Send the whole file once. | |
* 2. Watch for new bytes and push each appended line in real‑time. | |
*/ | |
function startWatchingFile(ws, filePath) { | |
let lastSize = 0; | |
// 1. Read and send full file content initially | |
fs.readFile(filePath, 'utf-8', (err, data) => { | |
if (!err && data) { | |
const lines = data.trimEnd().split("\n").reverse(); | |
for (const line of lines) { | |
if (line.trim()) { | |
const payload = JSON.stringify({ event: 'log', data: line }); | |
if (ws.readyState === ws.OPEN) ws.send(payload); | |
} | |
} | |
lastSize = Buffer.byteLength(data); // Set lastSize to current file size | |
} | |
// 2. Start watching for changes after initial send | |
const watcher = fs.watch(filePath, (eventType) => { | |
if (eventType !== 'change') return; | |
fs.stat(filePath, (err, stats) => { | |
if (err || stats.size <= lastSize) return; | |
const start = lastSize; | |
const end = stats.size - 1; | |
lastSize = stats.size; | |
const stream = fs.createReadStream(filePath, { start, end }); | |
/* Stream only the fresh bytes */ | |
const rl = readline.createInterface({ input: stream }); | |
rl.on('line', (line) => { | |
if (line.trim()) { | |
const payload = JSON.stringify({ event: 'log', data: line }); | |
if (ws.readyState === ws.OPEN) ws.send(payload); | |
} | |
}); | |
}); | |
}); | |
/* Save watcher so we can close it later */ | |
clientWatchers.set(ws, { filePath, watcher, lastSize }); | |
}); | |
} | |
/** Stop and clean up the watcher associated with a WebSocket client */ | |
function stopWatchingFile(ws) { | |
const info = clientWatchers.get(ws); | |
if (info?.watcher) { | |
info.watcher.close(); // stop fs.watch | |
} | |
clientWatchers.delete(ws); // remove from map | |
} | |
/*───────────────────────────────────────────────────────────────────────────* | |
* WEBSOCKET EVENT HANDLERS * | |
*───────────────────────────────────────────────────────────────────────────*/ | |
wss.on('connection', (ws) => { | |
ws.on('close', () => { | |
stopWatchingFile(ws); | |
}); | |
ws.on('message', async (message) => { | |
try { | |
const data = JSON.parse(message.toString()); | |
if (data.event === 'getLumenLog') { | |
const dateStr = data?.data?.date; | |
const filePath = getLogFilePathFromDate(dateStr); | |
if (!filePath) return; | |
/* Ignore if client already watching this file */ | |
const existingWatcher = clientWatchers.get(ws); | |
if (existingWatcher?.filePath === filePath) return; | |
/* Switch file: clean old watcher, ensure new file exists, start watcher */ | |
stopWatchingFile(ws); | |
try { | |
await fs.promises.access(filePath, fs.constants.F_OK); | |
} catch { | |
// File does not exist — inform client and return | |
if (ws.readyState === ws.OPEN) { | |
ws.send(JSON.stringify({ | |
event: 'error', | |
message: `Log file not found for date ${dateStr}` | |
})); | |
} | |
return; | |
} | |
startWatchingFile(ws, filePath); | |
} | |
} catch (err) { | |
} | |
}); | |
}); |
Author
ArunSelvan25
commented
Jun 19, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment