107 lines
3.3 KiB
JavaScript
107 lines
3.3 KiB
JavaScript
import assert from 'node:assert';
|
|
import child_process from 'node:child_process';
|
|
import debugModule from 'debug';
|
|
import path from 'node:path';
|
|
import stream from 'node:stream';
|
|
import { StringDecoder } from 'node:string_decoder';
|
|
|
|
const debug = debugModule('box:logs');
|
|
const TransformStream = stream.Transform;
|
|
|
|
const LOGTAIL_CMD = path.join(import.meta.dirname, 'scripts/logtail.sh');
|
|
const KILL_CHILD_CMD = path.join(import.meta.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;
|
|
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;
|
|
}
|
|
|
|
export default {
|
|
tail,
|
|
journalctl,
|
|
LogStream
|
|
};
|