'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 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 };