Files
cloudron-box/syslog.js
2025-09-15 14:01:47 +02:00

103 lines
3.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
exports = module.exports = {
start,
stop
};
const debug = require('debug')('syslog:server'),
fs = require('node:fs'),
net = require('node:net'),
path = require('node:path'),
paths = require('./src/paths.js'),
util = require('node:util');
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 = /^<(?<priority>\d+)>(?<version>\d+) (?<timestamp>\S+) (?<hostname>\S+) (?<appName>\S+) (?<procId>\S+) (?<msgId>\S+) (?:\[(?<structuredData>.*?)\]|-)(?<message>.*)$/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 (require.main === module) {
main();
}