diff --git a/CHANGES b/CHANGES index 03b4abed5..e146922ea 100644 --- a/CHANGES +++ b/CHANGES @@ -3221,3 +3221,5 @@ [9.2.0] * apppasswords: generate easier to type passwords +* logs: escape and unescape new lines + diff --git a/box.js b/box.js index a4cd37a42..215604b61 100755 --- a/box.js +++ b/box.js @@ -38,8 +38,10 @@ async function setupNetworking() { function exitSync(status) { const ts = new Date().toISOString(); if (status.message) fs.write(logFd, `${ts} ${status.message}\n`, function () {}); - const msg = status.error.stack.replace(/\n/g, `\n${ts} `); // prefix each line with ts - if (status.error) fs.write(logFd, `${ts} ${msg}\n`, function () {}); + if (status.error) { + const escapedStack = status.error.stack.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + fs.write(logFd, `${ts} ${escapedStack}\n`, function () {}); + } fs.fsyncSync(logFd); fs.closeSync(logFd); process.exit(status.code); diff --git a/dashboard/src/components/LogsViewer.vue b/dashboard/src/components/LogsViewer.vue index e7c2cf0b2..4cf038f4f 100644 --- a/dashboard/src/components/LogsViewer.vue +++ b/dashboard/src/components/LogsViewer.vue @@ -209,7 +209,7 @@ body { color: white; font-family: monospace; font-size: 14px; - white-space: nowrap; + white-space: pre-wrap; width: 100%; } diff --git a/dashboard/src/models/LogsModel.js b/dashboard/src/models/LogsModel.js index 552faf63d..1bb2781c0 100644 --- a/dashboard/src/models/LogsModel.js +++ b/dashboard/src/models/LogsModel.js @@ -82,7 +82,8 @@ export function create(type, id, options = {}) { } const time = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : ''; - const html = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message))); + const escaped = ansiToHtml(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message))); + const html = escaped.replace(/\n/g, '
'); eventSource._lastMessage = { time, html }; lineHandler(time, html); diff --git a/src/apps.js b/src/apps.js index ae61ee58f..6e9cfacdc 100644 --- a/src/apps.js +++ b/src/apps.js @@ -1377,7 +1377,8 @@ async function appendLogLine(app, line) { const logFilePath = path.join(paths.LOG_DIR, app.id, 'app.log'); const isoDate = new Date(new Date().toUTCString()).toISOString(); - if (!safe.fs.appendFileSync(logFilePath, `${isoDate} ${line}\n`)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`); + const escaped = line.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + if (!safe.fs.appendFileSync(logFilePath, `${isoDate} ${escaped}\n`)) console.error(`Could not append log line for app ${app.id} at ${logFilePath}: ${safe.error.message}`); } async function checkManifest(manifest) { diff --git a/src/logger.js b/src/logger.js index deeb16c26..f38e09cec 100644 --- a/src/logger.js +++ b/src/logger.js @@ -5,7 +5,8 @@ const LOG_ENABLED = process.env.BOX_ENV !== 'test' || !!process.env.LOG; function output(namespace, args) { const ts = new Date().toISOString(); - process.stdout.write(`${ts} ${namespace}: ${util.format(...args)}\n`); + const msg = util.format(...args).replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + process.stdout.write(`${ts} ${namespace}: ${msg}\n`); } export default function logger(namespace) { diff --git a/src/logs.js b/src/logs.js index 3fea2ce71..a44af9561 100644 --- a/src/logs.js +++ b/src/logs.js @@ -34,9 +34,12 @@ class LogStream extends TransformStream { message = line.slice(data[0].length+1); } + // unescape \\n → newline and \\\\ → backslash (writers escape newlines to keep one entry per line) + message = (message || line).replace(/\\(\\|n)/g, (_, c) => c === 'n' ? '\n' : '\\'); + 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 + message: message, source: this._options.source }) + '\n'; } diff --git a/src/taskworker.js b/src/taskworker.js index d7709354c..26a8fd4fb 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -70,8 +70,12 @@ async function setupNetworking() { // taskworker.sh forwards the exit code of the actual worker. It's either a raw signal number OR the exit code. So, choose exit codes > 31 // 50 - internal error , 70 - SIGTERM exit function exitSync(status) { - if (status.error) fs.write(logFd, status.error.stack + '\n', function () {}); - fs.write(logFd, `${(new Date()).toISOString()} Exiting with code ${status.code}\n`, function () {}); + const ts = (new Date()).toISOString(); + if (status.error) { + const escapedStack = status.error.stack.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + fs.write(logFd, `${ts} ${escapedStack}\n`, function () {}); + } + fs.write(logFd, `${ts} Exiting with code ${status.code}\n`, function () {}); fs.fsyncSync(logFd); fs.closeSync(logFd); process.exit(status.code); diff --git a/syslog.js b/syslog.js index 40bd0624d..0590ac91d 100755 --- a/syslog.js +++ b/syslog.js @@ -59,7 +59,8 @@ async function start() { try { fs.mkdirSync(appLogDir, { recursive: true }); - fs.appendFileSync(`${appLogDir}/app.log`, `${info.timestamp} ${info.message.trim()}\n`); + const escaped = info.message.trim().replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); + fs.appendFileSync(`${appLogDir}/app.log`, `${info.timestamp} ${escaped}\n`); } catch (error) { log(error); }