#!/usr/bin/env node import debugModule from 'debug'; import fs from 'node:fs'; import net from 'node:net'; import path from 'node:path'; import paths from './src/paths.js'; import util from 'node:util'; const debug = debugModule('syslog:server'); let gServer = null; // https://docs.docker.com/engine/logging/drivers/syslog/ // example: <34>1 2023-09-07T14:33:22Z myhost myapp pid msgid [exampleSDID@32473 iut="3" eventSource="Application"] An example message // the structured data can be "-" when missing function parseRFC5424Message(rawMessage) { const syslogRegex = /^<(?\d+)>(?\d+) (?\S+) (?\S+) (?\S+) (?\S+) (?\S+) (?:\[(?.*?)\]|-)(?.*)$/s; // /s means .* will match newline . (?: is non-capturing group const match = rawMessage.match(syslogRegex); if (!match) return null; const { priority, version, timestamp, hostname, appName, procId, msgId, structuredData, message } = match.groups; return { pri: parseInt(priority, 10), // priority version: parseInt(version, 10), // version timestamp, // timestamp hostname, // hostname appName, // app name procId, // process ID msgId, // message ID structuredData: structuredData || null, // structured data (if present) message // message }; } async function start() { debug('=========================================='); debug(' Cloudron Syslog Daemon '); debug('=========================================='); gServer = net.createServer(); gServer.on('error', function (error) { console.error(`server error: ${error}`); }); gServer.on('connection', function (socket) { socket.on('data', function (data) { const msg = data.toString('utf8').trim(); // strip any trailing empty new lines. it's unclear why we get it in the first place for (const line of msg.split('\n')) { // empirically, multiple messages can arrive in a single packet const info = parseRFC5424Message(line); if (!info) return debug(`Unable to parse: [${msg}]`); if (!info.appName) return debug(`Ignore unknown app: [${msg}]`); const appLogDir = path.join(paths.LOG_DIR, info.appName); try { fs.mkdirSync(appLogDir, { recursive: true }); fs.appendFileSync(`${appLogDir}/app.log`, `${info.timestamp} ${info.message.trim()}\n`); } catch (error) { debug(error); } } }); socket.on('error', function (error) { debug(`socket error: ${error}`); }); }); await fs.promises.rm(paths.SYSLOG_SOCKET_FILE, { force: true }); await util.promisify(gServer.listen.bind(gServer))(paths.SYSLOG_SOCKET_FILE); debug(`Listening on ${paths.SYSLOG_SOCKET_FILE}`); } async function stop() { await fs.promises.rm(paths.SYSLOG_SOCKET_FILE, { force: true }); gServer.unref(); // TODO : cleanup client connections. otherwise server.close() won't return gServer = null; } async function main() { await start(); process.on('SIGTERM', async function () { debug('Received SIGTERM. Shutting down.'); await stop(); setTimeout(process.exit.bind(process), 1000); }); } if (process.argv[1] === import.meta.filename) { main(); } export default { start, stop };