Files
cloudron-box/src/logs.js
Girish Ramakrishnan 12e073e8cf use node: prefix for requires
mostly because code is being autogenerated by all the AI stuff using
this prefix. it's also used in the stack trace.
2025-08-14 12:55:35 +05:30

107 lines
3.3 KiB
JavaScript

'use strict';
const assert = require('node:assert'),
child_process = require('node:child_process'),
debug = require('debug')('box:logs'),
path = require('node:path'),
stream = require('node:stream'),
{ StringDecoder } = require('node:string_decoder'),
TransformStream = stream.Transform;
const LOGTAIL_CMD = path.join(__dirname, 'scripts/logtail.sh');
const KILL_CHILD_CMD = path.join(__dirname, 'scripts/kill-child.sh');
class LogStream extends TransformStream {
constructor(options) {
super();
this._options = Object.assign({ source: 'unknown', format: 'json' }, options);
this._decoder = new StringDecoder();
this._soFar = '';
this._lastknownTimestamp = 0;
}
_format(line) {
if (this._options.format !== 'json') return line + '\n';
const data = line.split(' '); // logs are <ISOtimestamp> <msg>
let timestamp = (new Date(data[0])).getTime();
let message;
if (isNaN(timestamp)) {
timestamp = this._lastknownTimestamp;
message = line;
} else {
this._lastknownTimestamp = timestamp;
message = line.slice(data[0].length+1);
}
return JSON.stringify({
realtimeTimestamp: timestamp * 1000, // timestamp info can be missing (0) for app logs via logPaths
message: message || line, // send the line if message parsing failed
source: this._options.source
}) + '\n';
}
_transform(chunk, encoding, callback) {
const data = this._soFar + this._decoder.write(chunk);
let start = this._soFar.length, end = -1;
while ((end = data.indexOf('\n', start)) !== -1) {
const line = data.slice(start, end); // does not include end
this.push(this._format(line));
start = end + 1;
}
this._soFar = data.slice(start);
callback(null);
}
_flush(callback) {
const line = this._soFar + this._decoder.end();
if (line) this.push(this._format(line));
callback(null);
}
}
function tail(filePaths, options) {
assert(Array.isArray(filePaths));
assert.strictEqual(typeof options, 'object');
const lines = options.lines === -1 ? '+1' : options.lines;
const args = [ `--lines=${lines}` ];
if (options.follow) args.push('--follow');
const cp = options.sudo
? child_process.spawn('/usr/bin/sudo', [ LOGTAIL_CMD, ...args, ...filePaths ])
: child_process.spawn('/usr/bin/tail', args.concat(filePaths));
cp.terminate = () => {
child_process.execFile('/usr/bin/sudo', [ KILL_CHILD_CMD, cp.pid, process.pid ], { encoding: 'utf8' }, (error, stdout, stderr) => {
if (error) debug(`tail: failed to kill children`, stdout, stderr);
});
};
return cp;
}
function journalctl(unit, options) {
assert.strictEqual(typeof unit, 'string');
assert.strictEqual(typeof options, 'object');
const args = [
'--lines=' + (options.lines === -1 ? 'all' : options.lines),
`--unit=${unit}`,
'--no-pager',
'--output=short-iso'
];
if (options.follow) args.push('--follow');
const cp = child_process.spawn('journalctl', args);
cp.terminate = () => cp.kill('SIGKILL');
return cp;
}
exports = module.exports = {
tail,
journalctl,
LogStream
};