mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
103 lines
3.4 KiB
JavaScript
Executable File
103 lines
3.4 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 = /^<(\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 [, pri, version, timestamp, hostname, appName, procId, msgId, structuredData, message] = match;
|
|
|
|
return {
|
|
pri: parseInt(pri, 10), // priority
|
|
version: parseInt(version, 10), // version
|
|
timestamp, // timestamp
|
|
hostname, // hostname
|
|
appName, // app name
|
|
procId, // process ID
|
|
msgId, // message ID
|
|
structuredData: 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
|
|
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();
|
|
}
|