diff --git a/src/apps.js b/src/apps.js index b83f1e026..90031c1b5 100644 --- a/src/apps.js +++ b/src/apps.js @@ -164,7 +164,7 @@ const appstore = require('./appstore.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('fs'), - LogStream = require('./log-stream.js'), + logs = require('./logs.js'), mail = require('./mail.js'), manifestFormat = require('cloudron-manifestformat'), mounts = require('./mounts.js'), @@ -179,7 +179,6 @@ const appstore = require('./appstore.js'), services = require('./services.js'), settings = require('./settings.js'), shell = require('./shell.js'), - spawn = require('child_process').spawn, storage = require('./storage.js'), superagent = require('superagent'), system = require('./system.js'), @@ -2025,19 +2024,10 @@ async function getLogs(app, options) { const appId = app.id; - const lines = options.lines === -1 ? '+1' : options.lines, - format = options.format || 'json', - follow = options.follow; - - assert.strictEqual(typeof format, 'string'); - - const args = [ '--lines=' + lines ]; - if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs - const logPaths = await getLogPaths(app); - const cp = spawn('/usr/bin/tail', args.concat(logPaths)); + const cp = logs.tail(logPaths, { lines: options.lines, follow: options.follow }); - const logStream = new LogStream({ format, source: appId }); + const logStream = new logs.LogStream({ format: options.format || 'json', source: appId }); logStream.close = cp.kill.bind(cp, 'SIGKILL'); // hook for caller. closing stream kills the child process cp.stdout.pipe(logStream); diff --git a/src/cloudron.js b/src/cloudron.js index 981a12e7c..036aa9881 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -41,7 +41,7 @@ const apps = require('./apps.js'), eventlog = require('./eventlog.js'), execSync = require('child_process').execSync, fs = require('fs'), - LogStream = require('./log-stream.js'), + logs = require('./logs.js'), mail = require('./mail.js'), notifications = require('./notifications.js'), oidc = require('./oidc.js'), @@ -53,7 +53,6 @@ const apps = require('./apps.js'), services = require('./services.js'), settings = require('./settings.js'), shell = require('./shell.js'), - spawn = require('child_process').spawn, sysinfo = require('./sysinfo.js'), tasks = require('./tasks.js'), users = require('./users.js'); @@ -217,27 +216,16 @@ async function getLogs(unit, options) { assert.strictEqual(typeof unit, 'string'); assert(options && typeof options === 'object'); - assert.strictEqual(typeof options.lines, 'number'); - assert.strictEqual(typeof options.format, 'string'); - assert.strictEqual(typeof options.follow, 'boolean'); + debug(`Getting logs for ${unit}`); - const lines = options.lines === -1 ? '+1' : options.lines, - format = options.format || 'json', - follow = options.follow; - - debug('Getting logs for %s as %s', unit, format); - - let args = [ '--lines=' + lines ]; - if (follow) args.push('--follow'); - - // need to handle box.log without subdir - if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log')); - else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log')); + let logFile = ''; + if (unit === 'box') logFile = path.join(paths.LOG_DIR, 'box.log'); // box.log is at the top + else if (unit.startsWith('crash-')) logFile = path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'); else throw new BoxError(BoxError.BAD_FIELD, `No such unit '${unit}'`); - const cp = spawn('/usr/bin/tail', args); + const cp = logs.tail([logFile], { lines: options.lines, follow: options.follow }); - const logStream = new LogStream({ format, source: unit }); + const logStream = new logs.LogStream({ format: options.format || 'json', source: unit }); logStream.close = cp.kill.bind(cp, 'SIGKILL'); // hook for caller. closing stream kills the child process cp.stdout.pipe(logStream); diff --git a/src/log-stream.js b/src/logs.js similarity index 59% rename from src/log-stream.js rename to src/logs.js index 33097179a..05d62701f 100644 --- a/src/log-stream.js +++ b/src/logs.js @@ -1,9 +1,14 @@ 'use strict'; -const stream = require('stream'), +const assert = require('assert'), + path = require('path'), + spawn = require('child_process').spawn, + stream = require('stream'), { StringDecoder } = require('string_decoder'), TransformStream = stream.Transform; +const LOGTAIL_CMD = path.join(__dirname, 'scripts/logtail.sh'); + class LogStream extends TransformStream { constructor(options) { super(); @@ -49,4 +54,36 @@ class LogStream extends TransformStream { } } -exports = module.exports = LogStream; +function tail(filePaths, options) { + assert(Array.isArray(filePaths)); + assert.strictEqual(typeof options, 'object'); + + const lines = options.lines === -1 ? '+1' : options.lines, + follow = options.follow; + + const args = [ '--lines=' + lines ]; + if (follow) args.push('--follow'); + + return spawn(LOGTAIL_CMD, args.concat(filePaths)); +} + +function journalctl(unit, options) { + assert.strictEqual(typeof unit, 'string'); + assert.strictEqual(typeof options, 'object'); + + const args = []; + args.push('--lines=' + (options.lines === -1 ? 'all' : options.lines)); + args.push(`--unit=${unit}`); + args.push('--no-pager'); + args.push('--output=short-iso'); + + if (options.follow) args.push('--follow'); + + return spawn('journalctl', args); +} + +exports = module.exports = { + tail, + journalctl, + LogStream +}; diff --git a/src/scripts/logtail.sh b/src/scripts/logtail.sh new file mode 100755 index 000000000..7c0b6b974 --- /dev/null +++ b/src/scripts/logtail.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eu + +args=$(getopt -o "" -l "follow,lines:" -n "$0" -- "$@") +eval set -- "${args}" + +follow="" +lines="" + +while true; do + case "$1" in + --follow) follow="--follow --retry --quiet"; shift;; # same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs + --lines) lines="$2"; shift 2;; + --) break;; + *) echo "Unknown option $1"; exit 1;; + esac +done + +exec tail ${follow} --lines=${lines} "$@" \ No newline at end of file diff --git a/src/services.js b/src/services.js index 33b312ff7..f95a007fb 100644 --- a/src/services.js +++ b/src/services.js @@ -45,7 +45,7 @@ const addonConfigs = require('./addonconfigs.js'), hat = require('./hat.js'), http = require('http'), infra = require('./infra_version.js'), - LogStream = require('./log-stream.js'), + logs = require('./logs.js'), mail = require('./mail.js'), os = require('os'), path = require('path'), @@ -57,7 +57,6 @@ const addonConfigs = require('./addonconfigs.js'), settings = require('./settings.js'), sftp = require('./sftp.js'), shell = require('./shell.js'), - spawn = require('child_process').spawn, superagent = require('superagent'), system = require('./system.js'); @@ -425,10 +424,6 @@ async function getServiceLogs(id, options) { assert.strictEqual(typeof id, 'string'); assert(options && typeof options === 'object'); - assert.strictEqual(typeof options.lines, 'number'); - assert.strictEqual(typeof options.format, 'string'); - assert.strictEqual(typeof options.follow, 'boolean'); - const [name, instance ] = id.split(':'); if (instance) { @@ -439,41 +434,18 @@ async function getServiceLogs(id, options) { debug(`Getting logs for ${name}`); - const lines = options.lines, - format = options.format || 'json', - follow = options.follow; + let cp; - let cmd, args = []; - - // docker and unbound use journald if (name === 'docker' || name === 'unbound') { - cmd = 'journalctl'; - - args.push('--lines=' + (lines === -1 ? 'all' : lines)); - args.push(`--unit=${name}`); - args.push('--no-pager'); - args.push('--output=short-iso'); - - if (follow) args.push('--follow'); + cp = logs.journalctl(name, options); } else if (name === 'nginx') { - cmd = '/usr/bin/tail'; - - args.push('--lines=' + (lines === -1 ? '+1' : lines)); - if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs - args.push('/var/log/nginx/access.log'); - args.push('/var/log/nginx/error.log'); + cp = logs.tail(['/var/log/nginx/access.log', '/var/log/nginx/error.log'], { lines: options.lines, follow: options.follow }); } else { - cmd = '/usr/bin/tail'; - - args.push('--lines=' + (lines === -1 ? '+1' : lines)); - if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs const containerName = APP_SERVICES[name] ? `${name}-${instance}` : name; - args.push(path.join(paths.LOG_DIR, containerName, 'app.log')); + cp = logs.tail([path.join(paths.LOG_DIR, containerName, 'app.log')], { lines: options.lines, follow: options.follow }); } - const cp = spawn(cmd, args); - - const logStream = new LogStream({ format, source: name }); + const logStream = new logs.LogStream({ format: options.format || 'json', source: name }); logStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process cp.stdout.pipe(logStream); diff --git a/src/tasks.js b/src/tasks.js index 5e2e50927..cef47bdec 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -46,12 +46,11 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), database = require('./database.js'), debug = require('debug')('box:tasks'), - LogStream = require('./log-stream.js'), + logs = require('./logs.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), shell = require('./shell.js'), - spawn = require('child_process').spawn, _ = require('underscore'); let gTasks = {}; // indexed by task id @@ -269,19 +268,8 @@ function getLogs(taskId, options) { debug(`Getting logs for ${taskId}`); - const lines = options.lines === -1 ? '+1' : options.lines, - format = options.format || 'json', - follow = options.follow; - - const cmd = '/usr/bin/tail'; - let args = [ '--lines=' + lines ]; - - if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs - args.push(`${paths.TASKS_LOG_DIR}/${taskId}.log`); - - const cp = spawn(cmd, args); - - const logStream = new LogStream({ format, source: taskId }); + const cp = logs.tail([`${paths.TASKS_LOG_DIR}/${taskId}.log`], { lines: options.lines, follow: options.follow }); + const logStream = new logs.LogStream({ format: options.format || 'json', source: taskId }); logStream.close = cp.kill.bind(cp, 'SIGKILL'); // hook for caller. closing stream kills the child process cp.stdout.pipe(logStream); diff --git a/src/test/log-stream-test.js b/src/test/logs-test.js similarity index 88% rename from src/test/log-stream-test.js rename to src/test/logs-test.js index b86eb63a9..b9f8e6342 100644 --- a/src/test/log-stream-test.js +++ b/src/test/logs-test.js @@ -5,14 +5,14 @@ const expect = require('expect.js'), fs = require('fs'), - LogStream = require('../log-stream.js'), + logs = require('../logs.js'), stream = require('stream'); describe('log stream', function () { it('can create stream', function (done) { fs.writeFileSync('/tmp/test-input.log', '2022-10-09T15:19:48.740Z message', 'utf8'); const input = fs.createReadStream('/tmp/test-input.log'); - const log = new LogStream({ format: 'json', source: 'test' }); + const log = new logs.LogStream({ format: 'json', source: 'test' }); const output = fs.createWriteStream('/tmp/test-output.log'); stream.pipeline(input, log, output, function (error) {