mostly because code is being autogenerated by all the AI stuff using this prefix. it's also used in the stack trace.
107 lines
3.3 KiB
JavaScript
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
|
|
};
|