From fa90217de7b20d4e342494efda2821334dbb4e0f Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 11 Aug 2021 17:38:27 +0200 Subject: [PATCH] feat(cli-repl): use mongodb log format for log files MONGOSH-944 (#1066) Switch the log format to a mongod-style log format, and split out the implementation parts that could be shared by another application (specifically, Compass). This uses log message ids starting from 1000000 to avoid conflicts with mongod log ids. --- packages/cli-repl/package-lock.json | 108 ++------ packages/cli-repl/package.json | 3 +- packages/cli-repl/src/cli-repl.spec.ts | 12 +- packages/cli-repl/src/cli-repl.ts | 92 ++----- packages/cli-repl/src/log-writer.spec.ts | 203 +++++++++++++++ packages/cli-repl/src/log-writer.ts | 245 ++++++++++++++++++ packages/cli-repl/src/mongosh-repl.ts | 4 +- packages/cli-repl/src/run.ts | 2 +- .../src/setup-logger-and-telemetry.spec.ts | 179 +++++++------ .../src/setup-logger-and-telemetry.ts | 122 +++++---- packages/cli-repl/test/e2e.spec.ts | 4 +- packages/shell-api/src/mongo.ts | 4 +- .../shell-api/src/shell-internal-state.ts | 6 +- packages/types/src/index.ts | 2 +- 14 files changed, 683 insertions(+), 303 deletions(-) create mode 100644 packages/cli-repl/src/log-writer.spec.ts create mode 100644 packages/cli-repl/src/log-writer.ts diff --git a/packages/cli-repl/package-lock.json b/packages/cli-repl/package-lock.json index c95480ca3..d9fb0d777 100644 --- a/packages/cli-repl/package-lock.json +++ b/packages/cli-repl/package-lock.json @@ -362,35 +362,6 @@ "integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==", "dev": true }, - "@types/pino": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.3.tgz", - "integrity": "sha512-YtT58N7Tt7B7f5B/upuq694p4eT4icM9TuhgYeKhm+dnF0Ahm7q5YJp1i7vC2mBMdWgH1IvOa2XK6rhUjBv0GQ==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/pino-std-serializers": "*", - "@types/sonic-boom": "*" - } - }, - "@types/pino-std-serializers": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/pino-std-serializers/-/pino-std-serializers-2.4.1.tgz", - "integrity": "sha512-17XcksO47M24IVTVKPeAByWUd3Oez7EbIjXpSbzMPhXVzgjGtrOa49gKBwxH9hb8dKv58OelsWQ+A1G1l9S3wQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/sonic-boom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@types/sonic-boom/-/sonic-boom-0.7.0.tgz", - "integrity": "sha512-AfqR0fZMoUXUNwusgXKxcE9DPlHNDHQp6nKYUd4PSRpLobF5CCevSpyTEBcVZreqaWKCnGBr9KI1fHMTttoB7A==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/text-table": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@types/text-table/-/text-table-0.2.1.tgz", @@ -507,11 +478,6 @@ "hijack-stream": "^1.0.0" } }, - "atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" - }, "axios": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz", @@ -529,6 +495,11 @@ "is-retry-allowed": "^1.1.0" } }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -538,6 +509,23 @@ "file-uri-to-path": "1.0.0" } }, + "bson": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.4.1.tgz", + "integrity": "sha512-Uu4OCZa0jouQJCKOk1EmmyqtdWAP5HVLru4lQxTwzJzxT+sJ13lVpEZU/MATDxtHiekWMAL84oQY3Xn1LpJVSg==", + "requires": { + "buffer": "^5.6.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "chai-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", @@ -639,16 +627,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "fast-redact": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-2.0.0.tgz", - "integrity": "sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA==" - }, - "fast-safe-stringify": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", - "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" - }, "fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", @@ -663,11 +641,6 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, - "flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" - }, "follow-redirects": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", @@ -734,6 +707,11 @@ "resolved": "https://registry.npmjs.org/hijack-stream/-/hijack-stream-1.0.0.tgz", "integrity": "sha512-9riBbIorIgSvsLQHL/rKEK6vJBexhgSRZC/tkieuei7a1U+CHgrXJVqW+RPswgEyuPbxcGCpx0QXO3iJuKRrrw==" }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -913,24 +891,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, - "pino": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-5.17.0.tgz", - "integrity": "sha512-LqrqmRcJz8etUjyV0ddqB6OTUutCgQULPFg2b4dtijRHUsucaAdBgSUW58vY6RFSX+NT8963F+q0tM6lNwGShA==", - "requires": { - "fast-redact": "^2.0.0", - "fast-safe-stringify": "^2.0.7", - "flatstr": "^1.0.12", - "pino-std-serializers": "^2.4.2", - "quick-format-unescaped": "^3.0.3", - "sonic-boom": "^0.7.5" - } - }, - "pino-std-serializers": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.4.2.tgz", - "integrity": "sha512-WaL504dO8eGs+vrK+j4BuQQq6GLKeCCcHaMB2ItygzVURcL1CycwNEUHTD/lHFHs/NL5qAz2UKrjYWXKSf4aMQ==" - }, "pretty-bytes": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", @@ -947,11 +907,6 @@ "strip-ansi": "^6.0.0" } }, - "quick-format-unescaped": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz", - "integrity": "sha512-dy1yjycmn9blucmJLXOfZDx1ikZJUi6E8bBZLnhPG5gBrVhHXx2xVyqqgKBubVNEXmx51dBACMHpoMQK/N/AXQ==" - }, "remove-array-items": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/remove-array-items/-/remove-array-items-1.1.1.tgz", @@ -981,15 +936,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" }, - "sonic-boom": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-0.7.7.tgz", - "integrity": "sha512-Ei5YOo5J64GKClHIL/5evJPgASXFVpfVYbJV9PILZQytTK6/LCwHvsZJW2Ig4p9FMC2OrBrMnXKgRN/OEoAWfg==", - "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index df18dbc43..2b3450033 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -54,11 +54,11 @@ "ansi-escape-sequences": "^5.1.2", "askcharacter": "^1.0.0", "askpassword": "^1.2.4", + "bson": "^4.4.1", "is-recoverable-error": "^1.0.2", "lodash.set": "^4.3.2", "mongodb-redact": "^0.2.2", "nanobus": "^4.4.0", - "pino": "^5.16.0", "pretty-bytes": "^5.3.0", "pretty-repl": "^3.1.1", "semver": "^7.1.2", @@ -73,7 +73,6 @@ "@types/chai-as-promised": "^7.1.3", "@types/lodash.set": "^4.3.6", "@types/node": "^14.14.5", - "@types/pino": "^6.3.3", "@types/text-table": "^0.2.1", "@types/yargs-parser": "^15.0.0", "chai-as-promised": "^7.1.1", diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index 0dd29cbab..5d69412f5 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -26,7 +26,7 @@ describe('CliRepl', () => { const tmpdir = useTmpdir(); async function log(): Promise { - return readReplLogfile(path.join(tmpdir.path, `${cliRepl.logId}_log`)); + return readReplLogfile(path.join(tmpdir.path, `${cliRepl.logWriter.logId}_log`)); } async function startWithExpectedImmediateExit(cliRepl: CliRepl, host: string): Promise { @@ -165,22 +165,22 @@ describe('CliRepl', () => { }); it('writes syntax errors to the log file', async() => { - expect((await log()).filter(entry => entry.stack?.startsWith('SyntaxError:'))).to.have.lengthOf(0); + expect((await log()).filter(entry => entry.attr?.stack?.startsWith('SyntaxError:'))).to.have.lengthOf(0); input.write('\n'); await waitBus(cliRepl.bus, 'mongosh:error'); - expect((await log()).filter(entry => entry.stack?.startsWith('SyntaxError:'))).to.have.lengthOf(1); + expect((await log()).filter(entry => entry.attr?.stack?.startsWith('SyntaxError:'))).to.have.lengthOf(1); }); it('writes JS errors to the log file', async() => { input.write('throw new Error("plain js error")\n'); await waitBus(cliRepl.bus, 'mongosh:error'); - expect((await log()).filter(entry => entry.stack?.startsWith('Error: plain js error'))).to.have.lengthOf(1); + expect((await log()).filter(entry => entry.attr?.stack?.startsWith('Error: plain js error'))).to.have.lengthOf(1); }); it('writes Mongosh errors to the log file', async() => { input.write('db.auth()\n'); await waitBus(cliRepl.bus, 'mongosh:error'); - expect((await log()).filter(entry => entry.stack?.startsWith('MongoshInvalidInputError:'))).to.have.lengthOf(1); + expect((await log()).filter(entry => entry.attr?.stack?.startsWith('MongoshInvalidInputError:'))).to.have.lengthOf(1); }); it('emits the error event when exit() fails', async() => { @@ -191,7 +191,7 @@ describe('CliRepl', () => { } catch (e) { const [emitted] = await onerror; expect(emitted).to.be.instanceOf(MongoshInternalError); - expect((await log()).filter(entry => entry.stack?.startsWith('MongoshInternalError:'))).to.have.lengthOf(1); + expect((await log()).filter(entry => entry.attr?.stack?.startsWith('MongoshInternalError:'))).to.have.lengthOf(1); return; } expect.fail('expected error'); diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index c79174aad..18b524d07 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -7,18 +7,17 @@ import { SnippetManager } from '@mongosh/snippet-manager'; import Analytics from 'analytics-node'; import askpassword from 'askpassword'; import Nanobus from 'nanobus'; -import pino from 'pino'; import semver from 'semver'; import { Readable, Writable } from 'stream'; import type { StyleDefinition } from './clr'; import { ConfigManager, ShellHomeDirectory, ShellHomePaths } from './config-directory'; import { CliReplErrors } from './error-codes'; +import { MongoLogManager, MongoLogWriter } from './log-writer'; import { MongocryptdManager } from './mongocryptd-manager'; import MongoshNodeRepl, { MongoshNodeReplOptions } from './mongosh-repl'; import setupLoggerAndTelemetry from './setup-logger-and-telemetry'; import { MongoshBus, CliUserConfig } from '@mongosh/types'; -import { once } from 'events'; -import { createWriteStream, promises as fs } from 'fs'; +import { promises as fs } from 'fs'; import path from 'path'; import { promisify } from 'util'; @@ -56,9 +55,10 @@ class CliRepl { shellHomeDirectory: ShellHomeDirectory; configDirectory: ConfigManager; config: CliUserConfigOnDisk; + logManager: MongoLogManager; + logWriter?: MongoLogWriter; input: Readable; output: Writable; - logId: string; analyticsOptions?: AnalyticsOptions; analytics?: Analytics; warnedAboutInaccessibleFiles = false; @@ -74,7 +74,6 @@ class CliRepl { this.input = options.input; this.output = options.output; this.analyticsOptions = options.analyticsOptions; - this.logId = new bson.ObjectId().toString(); this.onExit = options.onExit; this.config = { userId: new bson.ObjectId().toString(), @@ -85,7 +84,7 @@ class CliRepl { this.configDirectory = new ConfigManager( this.shellHomeDirectory) .on('error', (err: Error) => - this.bus.emit('mongosh:error', err)) + this.bus.emit('mongosh:error', err, 'config')) .on('new-config', (config: CliUserConfigOnDisk) => this.bus.emit('mongosh:new-user', config.userId, config.enableTelemetry)) .on('update-config', (config: CliUserConfigOnDisk) => @@ -96,11 +95,18 @@ class CliRepl { this.shellHomeDirectory, this.bus); + this.logManager = new MongoLogManager({ + directory: this.shellHomeDirectory.localPath('.'), + retentionDays: 30, + onerror: (err: Error) => this.bus.emit('mongosh:error', err, 'log'), + onwarn: (err: Error, path: string) => this.warnAboutInaccessibleFile(err, path) + }); + // We can't really do anything meaningfull if the output stream is broken or // closed. To avoid throwing an error while writing to it, let's send it to // the telemetry instead this.output.on('error', (err: Error) => { - this.bus.emit('mongosh:error', err); + this.bus.emit('mongosh:error', err, 'io'); }); this.mongoshRepl = new MongoshNodeRepl({ @@ -129,21 +135,23 @@ class CliRepl { } this.ensurePasswordFieldIsPresentInAuth(driverOptions); - if (!this.cliOptions.quiet) { - this.output.write(`Current Mongosh Log ID:\t${this.logId}\n`); - } - try { await this.shellHomeDirectory.ensureExists(); } catch (err) { this.warnAboutInaccessibleFile(err); } - const logStream = await this.openLogStream(); + await this.logManager.cleanupOldLogfiles(); + const logger = await this.logManager.createLogWriter(); + if (!this.cliOptions.quiet) { + this.output.write(`Current Mongosh Log ID:\t${logger.logId}\n`); + } + this.logWriter = logger; + setupLoggerAndTelemetry( - this.logId, + logger.logId, this.bus, - () => pino({ name: 'mongosh' }, logStream), + () => logger, () => { if (process.env.IS_MONGOSH_EVERGREEN_CI && !this.analyticsOptions?.alwaysEnable) { // This error will be in the log file, but otherwise not visible to users @@ -276,55 +284,8 @@ class CliRepl { } } - get logFilePath(): string { - return this.shellHomeDirectory.localPath(`${this.logId}_log`); - } - - /** - * Open a writable stream for the current log file. - */ - async openLogStream(): Promise { - await this.cleanupOldLogfiles(); - try { - const stream = createWriteStream(this.logFilePath, { mode: 0o600 }); - await once(stream, 'ready'); - return stream; - } catch (err) { - this.warnAboutInaccessibleFile(err, this.logFilePath); - return new Writable({ - write(chunk, enc, cb) { - // Just ignore log data if there was an error. - cb(); - } - }); - } - } - - async cleanupOldLogfiles(): Promise { - const dir = this.shellHomeDirectory.localPath(''); - let dirHandle; - try { - dirHandle = await fs.opendir(dir); - } catch { - return; - } - for await (const dirent of dirHandle) { - if (!dirent.isFile()) continue; - const { id } = dirent.name.match(/^(?[a-f0-9]{24})_log$/i)?.groups ?? {}; - if (!id) continue; - // Delete files older than 30 days - if (new bson.ObjectId(id).generationTime < (Date.now() / 1000) - 30 * 86400) { - try { - await fs.unlink(path.join(dir, dirent.name)); - } catch (err) { - this.bus.emit('mongosh:error', err); - } - } - } - } - warnAboutInaccessibleFile(err: Error, path?: string): void { - this.bus.emit('mongosh:error', err); + this.bus.emit('mongosh:error', err, 'config'); if (this.warnedAboutInaccessibleFiles) { // If one of the files mongosh tries to access, it's also likely that // the others are as well. In that case, there is no point in spamming the @@ -443,7 +404,7 @@ class CliRepl { } private async _fatalError(error: any): Promise { - this.bus.emit('mongosh:error', error); + this.bus.emit('mongosh:error', error, 'fatal'); this.output.write(this.mongoshRepl.formatError(error) + '\n'); return this.exit(1); @@ -461,6 +422,7 @@ class CliRepl { } catch { /* ignore */ } } this.mongocryptdManager.close(); + await this.logWriter?.flush?.(); this.bus.emit('mongosh:closed'); } @@ -469,7 +431,7 @@ class CliRepl { await this.onExit(code); // onExit never returns. If it does, that's a bug. const error = new MongoshInternalError('onExit() unexpectedly returned'); - this.bus.emit('mongosh:error', error); + this.bus.emit('mongosh:error', error, 'fatal'); throw error; } @@ -497,7 +459,7 @@ class CliRepl { } bugReportErrorMessageInfo(): string { - return `Please include the log file for this session (${this.logFilePath}).`; + return `Please include the log file for this session (${this.logWriter?.logFilePath}).`; } } diff --git a/packages/cli-repl/src/log-writer.spec.ts b/packages/cli-repl/src/log-writer.spec.ts new file mode 100644 index 000000000..560e55256 --- /dev/null +++ b/packages/cli-repl/src/log-writer.spec.ts @@ -0,0 +1,203 @@ +import { MongoLogWriter, MongoLogManager, mongoLogId } from './log-writer'; +import { once } from 'events'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import stream from 'stream'; +import { inspect } from 'util'; +import sinon from 'ts-sinon'; +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +chai.use(sinonChai); + +describe('MongoLogWriter', () => { + it('allows writing log messages to a stream', async() => { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + writer.info('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.warn('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.error('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.fatal('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.write({ + t: now, + s: 'E', + c: 'x', + id: mongoLogId(0), + ctx: 'y', + msg: 'z' + }); + await writer.flush(); + const log = target.read() + .split('\n') + .filter(Boolean) + .map(line => JSON.parse(line)); + expect(log).to.deep.equal([ + { + t: { '$date': '2021-08-10T10:39:25.386Z' }, + s: 'I', c: 'component', id: 12345, ctx: 'context', msg: 'message', + attr: { foo: 'bar' } + }, + { + t: { '$date': '2021-08-10T10:39:25.386Z' }, + s: 'W', c: 'component', id: 12345, ctx: 'context', msg: 'message', + attr: { foo: 'bar' } + }, + { + t: { '$date': '2021-08-10T10:39:25.386Z' }, + s: 'E', c: 'component', id: 12345, ctx: 'context', msg: 'message', + attr: { foo: 'bar' } + }, + { + t: { '$date': '2021-08-10T10:39:25.386Z' }, + s: 'F', c: 'component', id: 12345, ctx: 'context', msg: 'message', + attr: { foo: 'bar' } + }, + { + t: { '$date': '2021-08-10T10:39:25.386Z' }, + s: 'E', c: 'x', id: 0, ctx: 'y', msg: 'z' + } + ]); + }); + + it('can log error object as data as-is', async() => { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + writer.error('component', mongoLogId(12345), 'context', 'message', new Error('foo')); + await writer.flush(); + const log = target.read() + .split('\n') + .filter(Boolean) + .map(line => JSON.parse(line)); + log[0].attr.stack = ''; + expect(log[0].attr).to.deep.equal({ + code: null, + message: 'foo', + name: 'Error', + stack: '' + }); + }); + + it('can log non-trivial data', async() => { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + + const cyclic: any = {}; + cyclic.cyclic = cyclic; + writer.error('component', mongoLogId(12345), 'context', 'message', cyclic); + + await writer.flush(); + const log = target.read() + .split('\n') + .filter(Boolean) + .map(line => JSON.parse(line).attr); + expect(log).to.deep.equal([{ + _inspected: inspect(cyclic) + }]); + }); + + it('rejects invalid messages', async() => { + const errors = []; + function tryWrite(input: any) { + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target); + writer.on('error', (err) => errors.push(err)); + writer.write(input); + } + tryWrite({}); + tryWrite({ s: 'E' }); + tryWrite({ s: 'E', c: '' }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0) }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0), ctx: '' }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0), ctx: '', msg: '' }); + + await new Promise(setImmediate); + expect(errors).to.have.lengthOf(5); + expect(new Set([...errors.map(err => err.name)])).to.deep.equal(new Set(['TypeError'])); + }); +}); + +describe('MongoLogManager', () => { + let directory: string; + let onwarn: any; + let onerror: any; + let retentionDays: number; + + beforeEach(async() => { + retentionDays = 30; + onwarn = sinon.stub(); + onerror = sinon.stub(); + directory = path.join(os.tmpdir(), `log-writer-test-${Math.random()}-${Date.now()}`); + await fs.mkdir(directory, { recursive: true }); + }); + afterEach(async() => { + await fs.rmdir(directory, { recursive: true }); + }); + + it('allows creating and writing to log files', async() => { + const manager = new MongoLogManager({ + directory, retentionDays, onwarn, onerror + }); + + const writer = await manager.createLogWriter(); + expect(path.relative(directory, writer.logFilePath)[0]).to.not.equal('.'); + expect(writer.logFilePath.includes(writer.logId)).to.equal(true); + + writer.info('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.end(); + await once(writer, 'finish'); + + const log = (await fs.readFile(writer.logFilePath, 'utf8')) + .split('\n') + .filter(Boolean) + .map(line => JSON.parse(line)); + expect(log).to.have.lengthOf(1); + expect(log[0].t.$date).to.be.a('string'); + }); + + it('cleans up old log files when requested', async() => { + retentionDays = 0.000001; // 86.4 ms + const manager = new MongoLogManager({ + directory, retentionDays, onwarn, onerror + }); + + const writer = await manager.createLogWriter(); + writer.info('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.end(); + await once(writer, 'finish'); + + await fs.stat(writer.logFilePath); + await new Promise(resolve => setTimeout(resolve, 100)); + await manager.cleanupOldLogfiles(); + try { + await fs.stat(writer.logFilePath); + expect.fail('missed exception'); + } catch (err) { + expect(err.code).to.equal('ENOENT'); + } + }); + + it('cleaning up old log files is a no-op by default', async() => { + const manager = new MongoLogManager({ + directory: path.join('directory', 'nonexistent'), retentionDays, onwarn, onerror + }); + + await manager.cleanupOldLogfiles(); + }); + + it('creates no-op write streams as a fallback', async() => { + const manager = new MongoLogManager({ + directory: path.join('directory', 'nonexistent'), retentionDays, onwarn, onerror + }); + + const writer = await manager.createLogWriter(); + expect(onwarn).to.have.been.calledOnce; + expect(writer.logFilePath).to.equal(null); + + writer.info('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); + writer.end(); + await once(writer, 'finish'); + }); +}); diff --git a/packages/cli-repl/src/log-writer.ts b/packages/cli-repl/src/log-writer.ts new file mode 100644 index 000000000..5afe7665f --- /dev/null +++ b/packages/cli-repl/src/log-writer.ts @@ -0,0 +1,245 @@ +// Note: This file should be kept as self-contained as possible, so that we can +// split it into a separate package and then re-use that in Compass. +import { ObjectId, EJSON } from 'bson'; +import { once } from 'events'; +import { createWriteStream, promises as fs } from 'fs'; +import path from 'path'; +import { Writable } from 'stream'; +import { inspect } from 'util'; +import v8 from 'v8'; + +export interface MongoLogId { + /** Internal value -- always use mongoLogId() to create MongoLogId objects */ + __value: number; +} + +export function mongoLogId(id: number): MongoLogId { + return { __value: id }; +} + +export interface MongoLogEntry { + /** Timestamp at which the log event occurred */ + t?: Date; + /** Severity field */ + s: 'F' | 'E' | 'W' | 'I' | 'D1' | 'D2' | 'D3' | 'D4' | 'D5'; + /** Component field */ + c: string; + /** The message id field */ + id: MongoLogId; + /** The context field */ + ctx: string; + /** The message string field */ + msg: string; + /** Additional information about the event in question */ + attr?: any; +} + +function validateLogEntry(info: MongoLogEntry): Error | null { + if (typeof info.s !== 'string') { + return new TypeError('Cannot log messages without a severity field'); + } + if (typeof info.c !== 'string') { + return new TypeError('Cannot log messages without a component field'); + } + if (typeof info.id?.__value !== 'number') { + return new TypeError('Cannot log messages without an id field'); + } + if (typeof info.ctx !== 'string') { + return new TypeError('Cannot log messages without a context field'); + } + if (typeof info.msg !== 'string') { + return new TypeError('Cannot log messages without a message field'); + } + return null; +} + +export class MongoLogWriter extends Writable { + _logId: string; + _logFilePath: string | null; + _target: Pick; + _now: () => Date; + + constructor(logId: string, logFilePath: string | null, target: Pick, now?: () => Date) { + super({ objectMode: true }); + this._logId = logId; + this._logFilePath = logFilePath; + this._target = target; + this._now = now ?? (() => new Date()); + } + + get logId(): string { + return this._logId; + } + + get logFilePath(): string | null { + return this._logFilePath; + } + + _write(info: MongoLogEntry, encoding: unknown, callback: (err?: Error | null | undefined) => void): void { + const validationError = validateLogEntry(info); + if (validationError) { + callback(validationError); + return; + } + + // Copy the object to ensure the order of properties. + const fullInfo: Omit & { id: number } = { + t: info.t ?? this._now(), + s: info.s, + c: info.c, + id: info.id.__value, + ctx: info.ctx, + msg: info.msg + }; + if (info.attr) { + if (Object.prototype.toString.call(info.attr) === '[object Error]') { + fullInfo.attr = { + stack: info.attr.stack, + name: info.attr.name, + message: info.attr.message, + code: info.attr.code, + ...info.attr + }; + } else { + fullInfo.attr = info.attr; + } + } + + // The attr field may contain arbitrary data. If we cannot serialize it, + // we fall back to increasingly less faithful representations of it. + try { + EJSON.stringify(fullInfo.attr); + } catch { + try { + const cloned = v8.deserialize(v8.serialize(fullInfo.attr)); + EJSON.stringify(cloned); + fullInfo.attr = cloned; + } catch { + try { + const cloned = JSON.parse(JSON.stringify(fullInfo.attr)); + EJSON.stringify(cloned); + fullInfo.attr = cloned; + } catch { + fullInfo.attr = { _inspected: inspect(fullInfo.attr) }; + } + } + } + this._target.write(EJSON.stringify(fullInfo, { relaxed: true }) + '\n', callback); + } + + _final(callback: (err?: Error | null | undefined) => void): void { + this._target.end(callback); + } + + async flush(): Promise { + await new Promise(resolve => this._target.write('', resolve)); + } + + info(component: string, id: MongoLogId, context: string, message: string, attr?: any): void { + const logEntry: MongoLogEntry = { + s: 'I', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr + }; + this.write(logEntry); + } + + warn(component: string, id: MongoLogId, context: string, message: string, attr?: any): void { + const logEntry: MongoLogEntry = { + s: 'W', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr + }; + this.write(logEntry); + } + + error(component: string, id: MongoLogId, context: string, message: string, attr?: any): void { + const logEntry: MongoLogEntry = { + s: 'E', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr + }; + this.write(logEntry); + } + + fatal(component: string, id: MongoLogId, context: string, message: string, attr?: any): void { + const logEntry: MongoLogEntry = { + s: 'F', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr + }; + this.write(logEntry); + } +} + +interface MongoLogOptions { + directory: string; + retentionDays: number; + onerror: (err: Error, path: string) => unknown | Promise; + onwarn: (err: Error, path: string) => unknown | Promise; +} + +export class MongoLogManager { + _options: MongoLogOptions; + + constructor(options: MongoLogOptions) { + this._options = options; + } + + async cleanupOldLogfiles(): Promise { + const dir = this._options.directory; + let dirHandle; + try { + dirHandle = await fs.opendir(dir); + } catch { + return; + } + for await (const dirent of dirHandle) { + if (!dirent.isFile()) continue; + const { id } = dirent.name.match(/^(?[a-f0-9]{24})_log$/i)?.groups ?? {}; + if (!id) continue; + // Delete files older than n days + if (new ObjectId(id).generationTime < (Date.now() / 1000) - this._options.retentionDays * 86400) { + const toUnlink = path.join(dir, dirent.name); + try { + await fs.unlink(toUnlink); + } catch (err) { + this._options.onerror(err, toUnlink); + } + } + } + } + + async createLogWriter(): Promise { + const logId = new ObjectId().toString(); + const logFilePath = path.join(this._options.directory, `${logId}_log`); + + let stream: Writable; + try { + stream = createWriteStream(logFilePath, { mode: 0o600 }); + await once(stream, 'ready'); + } catch (err) { + this._options.onwarn(err, logFilePath); + stream = new Writable({ + write(chunk, enc, cb) { + // Just ignore log data if there was an error. + cb(); + } + }); + return new MongoLogWriter(logId, null, stream); + } + return new MongoLogWriter(logId, logFilePath, stream); + } +} diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 092793f54..ec18b197d 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -379,7 +379,7 @@ class MongoshNodeRepl implements EvaluationListener { throw new MongoshCommandFailed('adminCommand getLog unexpectedly returned no result'); } } catch (error) { - this.bus.emit('mongosh:error', error); + this.bus.emit('mongosh:error', error, 'repl'); return; } @@ -542,7 +542,7 @@ class MongoshNodeRepl implements EvaluationListener { name: result.name || 'MongoshInternalError', stack: result.stack }; - this.bus.emit('mongosh:error', output); + this.bus.emit('mongosh:error', output, 'repl'); return this.formatError(output); } diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index d29d2a65d..c4f6b8503 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -100,7 +100,7 @@ import stream from 'stream'; } catch (e) { console.error(`${e.name}: ${e.message}`); if (repl !== undefined) { - repl.bus.emit('mongosh:error', e); + repl.bus.emit('mongosh:error', e, 'startup'); } if (isSingleConsoleProcess) { // In single-process-console mode, it's confusing for the window to be diff --git a/packages/cli-repl/src/setup-logger-and-telemetry.spec.ts b/packages/cli-repl/src/setup-logger-and-telemetry.spec.ts index cb1655284..9e125f4cb 100644 --- a/packages/cli-repl/src/setup-logger-and-telemetry.spec.ts +++ b/packages/cli-repl/src/setup-logger-and-telemetry.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable camelcase */ import { expect } from 'chai'; +import { MongoLogWriter } from './log-writer'; import setupLoggerAndTelemetry from './setup-logger-and-telemetry'; import { EventEmitter } from 'events'; -import pino from 'pino'; import { MongoshInvalidInputError } from '@mongosh/errors'; import { MongoshBus } from '@mongosh/types'; @@ -11,17 +11,18 @@ describe('setupLoggerAndTelemetry', () => { let analyticsOutput: ['identify'|'track'|'log', any][]; let bus: MongoshBus; - const logger = pino({ name: 'mongosh' }, { - write(chunk: string) { logOutput.push(JSON.parse(chunk)); } - }); + const userId = '53defe995fa47e6c13102d9d'; + const logId = '5fb3c20ee1507e894e5340f3'; + + const logger = new MongoLogWriter(logId, `/tmp/${logId}_log`, { + write(chunk: string, cb: () => void) { logOutput.push(JSON.parse(chunk)); cb(); }, + end(cb: () => void) { cb(); } + } as any); const analytics = { identify(info: any) { analyticsOutput.push(['identify', info]); }, track(info: any) { analyticsOutput.push(['track', info]); } }; - const userId = '53defe995fa47e6c13102d9d'; - const logId = '5fb3c20ee1507e894e5340f3'; - beforeEach(() => { logOutput = []; analyticsOutput = []; @@ -45,7 +46,7 @@ describe('setupLoggerAndTelemetry', () => { is_atlas: false, node_version: 'v12.19.0' } as any); - bus.emit('mongosh:error', new MongoshInvalidInputError('meow', 'CLIREPL-1005', { cause: 'x' })); + bus.emit('mongosh:error', new MongoshInvalidInputError('meow', 'CLIREPL-1005', { cause: 'x' }), 'repl'); bus.emit('mongosh:use', { db: 'admin' }); bus.emit('mongosh:show', { method: 'dbs' }); } @@ -94,69 +95,93 @@ describe('setupLoggerAndTelemetry', () => { bus.emit('mongosh-sp:missing-optional-dependency', { name: 'kerberos', error: new Error('no kerberos') }); let i = 0; - expect(logOutput[i++].msg).to.match(/^mongosh:start-logging \{"execPath":".+","version":".+","distributionKind":.+\}$/); - expect(logOutput[i++].msg).to.equal('mongosh:update-user {"enableTelemetry":false}'); - expect(logOutput[i].msg).to.match(/^mongosh:connect/); - expect(logOutput[i].msg).to.match(/"session_id":"5fb3c20ee1507e894e5340f3"/); - expect(logOutput[i].msg).to.match(/"userId":"53defe995fa47e6c13102d9d"/); - expect(logOutput[i].msg).to.match(/"connectionUri":"mongodb:\/\/localhost\/"/); - expect(logOutput[i].msg).to.match(/"is_localhost":true/); - expect(logOutput[i].msg).to.match(/"is_atlas":false/); - expect(logOutput[i++].msg).to.match(/"node_version":"v12\.19\.0"/); - expect(logOutput[i].type).to.equal('Error'); - expect(logOutput[i++].msg).to.match(/meow/); - expect(logOutput[i++].msg).to.equal('mongosh:use {"db":"admin"}'); - expect(logOutput[i++].msg).to.equal('mongosh:show {"method":"dbs"}'); - expect(logOutput[i++].msg).to.equal('mongosh:update-user {"enableTelemetry":true}'); - expect(logOutput[i++].msg).to.match(/^mongosh:connect/); - expect(logOutput[i].type).to.equal('Error'); - expect(logOutput[i++].msg).to.match(/meow/); - expect(logOutput[i++].msg).to.equal('mongosh:use {"db":"admin"}'); - expect(logOutput[i++].msg).to.equal('mongosh:show {"method":"dbs"}'); - expect(logOutput[i++].msg).to.equal('mongosh:setCtx {"method":"setCtx"}'); - expect(logOutput[i].msg).to.match(/^mongosh:api-call/); - expect(logOutput[i++].msg).to.match(/"db":"test-1603986682000"/); - expect(logOutput[i].msg).to.match(/^mongosh:api-call/); - expect(logOutput[i++].msg).to.match(/"email":""/); - expect(logOutput[i].msg).to.match(/^mongosh:evaluate-input/); - expect(logOutput[i++].msg).to.match(/"input":"1\+1"/); - expect(logOutput[i++].msg).to.match(/"version":"3.6.1"/); - expect(logOutput[i++].msg).to.match(/^mongosh:api-call \{"_inspected":"\{.+circular:.+\}/); - expect(logOutput[i++].msg).to.equal('mongosh:start-loading-cli-scripts'); - expect(logOutput[i].msg).to.match(/^mongosh:api-load-file/); - expect(logOutput[i].msg).to.match(/"nested":true/); - expect(logOutput[i++].msg).to.match(/"filename":"foobar.js"/); - expect(logOutput[i++].msg).to.equal('mongosh:start-mongosh-repl {"version":"1.0.0"}'); - expect(logOutput[i].msg).to.match(/"nested":false/); - expect(logOutput[i++].msg).to.match(/"filename":"foobar.js"/); - expect(logOutput[i++].msg).to.equal('mongosh:mongoshrc-load'); - expect(logOutput[i++].msg).to.equal('mongosh:mongoshrc-mongorc-warn'); - expect(logOutput[i++].msg).to.equal('mongosh:eval-cli-script'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:loaded {"installdir":"/"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:npm-lookup {"existingVersion":"v1.2.3"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:npm-lookup-stopped'); - expect(logOutput[i].msg).to.match(/^mongosh-snippets:npm-download-failed/); - expect(logOutput[i++].msg).to.match(/"npmMetadataURL":"https:\/\/example.com"/); - expect(logOutput[i].msg).to.match(/^mongosh-snippets:npm-download-active/); - expect(logOutput[i].msg).to.match(/"npmMetadataURL":"https:\/\/example.com"/); - expect(logOutput[i++].msg).to.match(/"npmTarballURL":"https:\/\/example.net"/); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:fetch-index {"refreshMode":"always"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:fetch-cache-invalid'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:fetch-index-error {"action":"fetch","url":"https://localhost"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:fetch-index-done'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:package-json-edit-error {"error":"failed"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:spawn-child {"args":["npm","install"]}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:load-snippet {"source":"load-all","name":"foo"}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:snippet-command {"args":["install","foo"]}'); - expect(logOutput[i++].msg).to.equal('mongosh-snippets:transform-error {"error":"failed","addition":"oh no","name":"foo"}'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:connect-heartbeat-failure {"connectionId":"localhost","failure":"cause","isFailFast":true,"isKnownServer":true}'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:connect-heartbeat-succeeded {"connectionId":"localhost"}'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:connect-fail-early'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:connect-attempt-finished'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:resolve-srv-error {"from":"mongodb+srv://@hello.world/","error":"failed","duringLoad":false}'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:resolve-srv-succeeded {"from":"mongodb+srv://@hello.world/","to":"mongodb://@db.hello.world/"}'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:reset-connection-options'); - expect(logOutput[i++].msg).to.equal('mongosh-sp:missing-optional-dependency {"name":"kerberos","error":"no kerberos"}'); + expect(logOutput[i].msg).to.equal('Starting log'); + expect(logOutput[i++].attr).to.include.keys('execPath', 'version', 'distributionKind'); + expect(logOutput[i].msg).to.equal('User updated'); + expect(logOutput[i++].attr).to.deep.equal({ enableTelemetry: false }); + expect(logOutput[i].msg).to.equal('Connecting to server'); + expect(logOutput[i].attr.session_id).to.equal('5fb3c20ee1507e894e5340f3'); + expect(logOutput[i].attr.userId).to.equal('53defe995fa47e6c13102d9d'); + expect(logOutput[i].attr.connectionUri).to.equal('mongodb://localhost/'); + expect(logOutput[i].attr.is_localhost).to.equal(true); + expect(logOutput[i].attr.is_atlas).to.equal(false); + expect(logOutput[i++].attr.node_version).to.equal('v12.19.0'); + expect(logOutput[i].s).to.equal('E'); + expect(logOutput[i++].attr.message).to.match(/meow/); + expect(logOutput[i].msg).to.equal('Used "use" command'); + expect(logOutput[i++].attr).to.deep.equal({ db: 'admin' }); + expect(logOutput[i].msg).to.equal('Used "show" command'); + expect(logOutput[i++].attr).to.deep.equal({ method: 'dbs' }); + expect(logOutput[i].msg).to.equal('User updated'); + expect(logOutput[i++].attr).to.deep.equal({ enableTelemetry: true }); + expect(logOutput[i++].msg).to.equal('Connecting to server'); + expect(logOutput[i].s).to.equal('E'); + expect(logOutput[i++].attr.message).to.match(/meow/); + expect(logOutput[i].msg).to.equal('Used "use" command'); + expect(logOutput[i++].attr).to.deep.equal({ db: 'admin' }); + expect(logOutput[i].msg).to.equal('Used "show" command'); + expect(logOutput[i++].attr).to.deep.equal({ method: 'dbs' }); + expect(logOutput[i++].msg).to.equal('Initialized context'); + expect(logOutput[i].msg).to.equal('Performed API call'); + expect(logOutput[i++].attr.db).to.equal('test-1603986682000'); + expect(logOutput[i].msg).to.equal('Performed API call'); + expect(logOutput[i++].attr.arguments.filter.email).to.equal(''); + expect(logOutput[i].msg).to.equal('Evaluating input'); + expect(logOutput[i++].attr.input).to.equal('1+1'); + expect(logOutput[i].msg).to.equal('Driver initialized'); + expect(logOutput[i++].attr.driver.version).to.equal('3.6.1'); + expect(logOutput[i].msg).to.equal('Performed API call'); + expect(logOutput[i++].attr._inspected).to.match(/circular/); + expect(logOutput[i++].msg).to.equal('Start loading CLI scripts'); + expect(logOutput[i].msg).to.equal('Loading file via load()'); + expect(logOutput[i].attr.nested).to.equal(true); + expect(logOutput[i++].attr.filename).to.equal('foobar.js'); + expect(logOutput[i].msg).to.equal('Started REPL'); + expect(logOutput[i++].attr.version).to.equal('1.0.0'); + expect(logOutput[i].attr.nested).to.equal(false); + expect(logOutput[i++].attr.filename).to.equal('foobar.js'); + expect(logOutput[i++].msg).to.equal('Loading .mongoshrc.js'); + expect(logOutput[i++].msg).to.equal('Warning about .mongorc.js/.mongoshrc.js mismatch'); + expect(logOutput[i++].msg).to.equal('Evaluating script passed on the command line'); + expect(logOutput[i].msg).to.equal('Loaded snippets'); + expect(logOutput[i++].attr).to.deep.equal({ installdir: '/' }); + expect(logOutput[i].msg).to.equal('Performing npm lookup'); + expect(logOutput[i++].attr).to.deep.equal({ existingVersion: 'v1.2.3' }); + expect(logOutput[i++].msg).to.equal('npm lookup stopped'); + expect(logOutput[i].msg).to.equal('npm download failed'); + expect(logOutput[i++].attr.npmMetadataURL).to.equal('https://example.com'); + expect(logOutput[i].msg).to.equal('npm download active'); + expect(logOutput[i].attr.npmMetadataURL).to.equal('https://example.com'); + expect(logOutput[i++].attr.npmTarballURL).to.equal('https://example.net'); + expect(logOutput[i].msg).to.equal('Fetching snippet index'); + expect(logOutput[i++].attr.refreshMode).to.equal('always'); + expect(logOutput[i++].msg).to.equal('Snippet cache invalid'); + expect(logOutput[i].msg).to.equal('Fetching snippet index failed'); + expect(logOutput[i++].attr).to.deep.equal({ action: 'fetch', url: 'https://localhost' }); + expect(logOutput[i++].msg).to.equal('Fetching snippet index done'); + expect(logOutput[i].msg).to.equal('Modifying snippets package.json failed'); + expect(logOutput[i++].attr).to.deep.equal({ error: 'failed' }); + expect(logOutput[i].msg).to.equal('Spawning helper'); + expect(logOutput[i++].attr).to.deep.equal({ args: ['npm', 'install'] }); + expect(logOutput[i].msg).to.equal('Loading snippet'); + expect(logOutput[i++].attr).to.deep.equal({ source: 'load-all', name: 'foo' }); + expect(logOutput[i].msg).to.equal('Running snippet command'); + expect(logOutput[i++].attr).to.deep.equal({ args: ['install', 'foo'] }); + expect(logOutput[i].msg).to.equal('Rewrote error message'); + expect(logOutput[i++].attr).to.deep.equal({ error: 'failed', addition: 'oh no', name: 'foo' }); + expect(logOutput[i].msg).to.equal('Server heartbeat failure'); + expect(logOutput[i++].attr).to.deep.equal({ connectionId: 'localhost', failure: 'cause', isFailFast: true, isKnownServer: true }); + expect(logOutput[i].msg).to.equal('Server heartbeat succeeded'); + expect(logOutput[i++].attr).to.deep.equal({ connectionId: 'localhost' }); + expect(logOutput[i++].msg).to.equal('Aborting connection attempt as irrecoverable'); + expect(logOutput[i++].msg).to.equal('Connection attempt finished'); + expect(logOutput[i].msg).to.equal('Resolving SRV record failed'); + expect(logOutput[i++].attr).to.deep.equal({ from: 'mongodb+srv://@hello.world/', error: 'failed', duringLoad: false }); + expect(logOutput[i].msg).to.equal('Resolving SRV record succeeded'); + expect(logOutput[i++].attr).to.deep.equal({ from: 'mongodb+srv://@hello.world/', to: 'mongodb://@db.hello.world/' }); + expect(logOutput[i++].msg).to.equal('Reconnect because of changed connection options'); + expect(logOutput[i].msg).to.equal('Missing optional dependency'); + expect(logOutput[i++].attr).to.deep.equal({ name: 'kerberos', error: 'no kerberos' }); expect(i).to.equal(logOutput.length); const mongosh_version = require('../package.json').version; @@ -318,8 +343,10 @@ describe('setupLoggerAndTelemetry', () => { expect(logOutput).to.have.length(2); expect(analyticsOutput).to.have.length(2); - expect(logOutput[0].msg).to.equal('mongosh:deprecated-api-call {"class":"Database","method":"cloneDatabase"}'); - expect(logOutput[1].msg).to.equal('mongosh:deprecated-api-call {"class":"Database","method":"copyDatabase"}'); + expect(logOutput[0].msg).to.equal('Deprecated API call'); + expect(logOutput[0].attr).to.deep.equal({ class: 'Database', method: 'cloneDatabase' }); + expect(logOutput[1].msg).to.equal('Deprecated API call'); + expect(logOutput[1].attr).to.deep.equal({ class: 'Database', method: 'copyDatabase' }); expect(analyticsOutput).to.deep.equal([ [ 'track', @@ -358,7 +385,8 @@ describe('setupLoggerAndTelemetry', () => { bus.emit('mongosh:evaluate-finished'); expect(logOutput).to.have.length(1); - expect(logOutput[0].msg).to.equal('mongosh:deprecated-api-call {"class":"Database","method":"cloneDatabase"}'); + expect(logOutput[0].msg).to.equal('Deprecated API call'); + expect(logOutput[0].attr).to.deep.equal({ class: 'Database', method: 'cloneDatabase' }); expect(analyticsOutput).to.be.empty; }); @@ -367,7 +395,6 @@ describe('setupLoggerAndTelemetry', () => { bus.emit('mongosh:new-user', userId, true); expect(analyticsOutput).to.be.empty; expect(logOutput).to.have.lengthOf(2); - expect(logOutput[1].type).to.equal('Error'); - expect(logOutput[1].name).to.equal('mongosh'); + expect(logOutput[1].s).to.equal('E'); }); }); diff --git a/packages/cli-repl/src/setup-logger-and-telemetry.ts b/packages/cli-repl/src/setup-logger-and-telemetry.ts index 87a6f471c..e320516a5 100644 --- a/packages/cli-repl/src/setup-logger-and-telemetry.ts +++ b/packages/cli-repl/src/setup-logger-and-telemetry.ts @@ -1,7 +1,6 @@ /* eslint-disable camelcase */ import redactInfo from 'mongodb-redact'; import { redactURICredentials } from '@mongosh/history'; -import type { Logger } from 'pino'; import type { MongoshBus, ApiEvent, @@ -34,6 +33,7 @@ import type { } from '@mongosh/types'; import { inspect } from 'util'; import { buildInfo } from './build-info'; +import { MongoLogWriter, mongoLogId } from './log-writer'; interface MongoshAnalytics { identify(message: { @@ -51,22 +51,6 @@ interface MongoshAnalytics { }): void; } -// The default serializer for pino replaces circular properties it finds -// anywhere in the object graph. In extreme cases, e.g. when a mongosh cursor -// is passed to it, it follows the graph all the way to the network socket and -// replaces random properties of its internal state with the string -// '"[Circular"]': -// https://github.com/davidmarkclements/fast-safe-stringify/blob/0e011f068962e8f8974133a47afcabfc003f2183/index.js#L36 -// As a workaround, we do a JSON-based clone for any input that can contain -// arbitrary values. -function jsonClone(input: T): T | { _inspected: string } { - try { - return JSON.parse(JSON.stringify(input)); - } catch (error) { - return { _inspected: inspect(input) }; - } -} - // set up a noop, in case we are not able to connect to segment. class NoopAnalytics implements MongoshAnalytics { identify(_info: any): void {} // eslint-disable-line @typescript-eslint/no-unused-vars @@ -76,7 +60,7 @@ class NoopAnalytics implements MongoshAnalytics { export default function setupLoggerAndTelemetry( logId: string, bus: MongoshBus, - makeLogger: () => Logger, + makeLogger: () => MongoLogWriter, makeAnalytics: () => MongoshAnalytics): void { const log = makeLogger(); const mongosh_version = require('../package.json').version; @@ -87,7 +71,7 @@ export default function setupLoggerAndTelemetry( arch: process.arch }; - log.info('mongosh:start-logging', { + log.info('MONGOSH', mongoLogId(1_000_000_000), 'log', 'Starting log', { execPath: process.execPath, ...buildInfo() }); @@ -96,7 +80,7 @@ export default function setupLoggerAndTelemetry( try { analytics = makeAnalytics(); } catch (e) { - log.error(e); + log.error('MONGOSH', mongoLogId(1_000_000_001), 'analytics', 'Failed to instantiate analytics provider', e); } // We emit different analytics events for loading files and evaluating scripts @@ -105,13 +89,13 @@ export default function setupLoggerAndTelemetry( // be aware of this distinction. let hasStartedMongoshRepl = false; bus.on('mongosh:start-mongosh-repl', (ev: StartMongoshReplEvent) => { - log.info('mongosh:start-mongosh-repl', ev); + log.info('MONGOSH', mongoLogId(1_000_000_002), 'repl', 'Started REPL', ev); hasStartedMongoshRepl = true; }); let usesShellOption = false; bus.on('mongosh:start-loading-cli-scripts', (event: StartLoadingCliScriptsEvent) => { - log.info('mongosh:start-loading-cli-scripts'); + log.info('MONGOSH', mongoLogId(1_000_000_003), 'repl', 'Start loading CLI scripts'); usesShellOption = event.usesShellOption; }); @@ -119,7 +103,7 @@ export default function setupLoggerAndTelemetry( const connectionUri = redactURICredentials(args.uri); const { uri: _uri, ...argsWithoutUri } = args; // eslint-disable-line @typescript-eslint/no-unused-vars const params = { session_id: logId, userId, connectionUri, ...argsWithoutUri }; - log.info('mongosh:connect', jsonClone(params)); + log.info('MONGOSH', mongoLogId(1_000_000_004), 'connect', 'Connecting to server', params); if (telemetry) { analytics.track({ @@ -135,7 +119,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:driver-initialized', function(driverMetadata: any) { - log.info('mongosh:driver-initialized', jsonClone(driverMetadata)); + log.info('MONGOSH', mongoLogId(1_000_000_004), 'connect', 'Driver initialized', driverMetadata); }); bus.on('mongosh:new-user', function(id: string, enableTelemetry: boolean) { @@ -148,11 +132,15 @@ export default function setupLoggerAndTelemetry( userId = id; telemetry = enableTelemetry; if (telemetry) analytics.identify({ userId, traits: userTraits }); - log.info('mongosh:update-user', { enableTelemetry }); + log.info('MONGOSH', mongoLogId(1_000_000_005), 'config', 'User updated', { enableTelemetry }); }); - bus.on('mongosh:error', function(error: any) { - log.error(error); + bus.on('mongosh:error', function(error: any, context: string) { + if (context === 'fatal') { + log.fatal('MONGOSH', mongoLogId(1_000_000_006), context, `${error.name}: ${error.message}`, error); + } else { + log.error('MONGOSH', mongoLogId(1_000_000_006), context, `${error.name}: ${error.message}`, error); + } if (telemetry && error.name.includes('Mongosh')) { analytics.track({ @@ -170,11 +158,11 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:evaluate-input', function(args: EvaluateInputEvent) { - log.info('mongosh:evaluate-input', args); + log.info('MONGOSH', mongoLogId(1_000_000_007), 'repl', 'Evaluating input', args); }); bus.on('mongosh:use', function(args: UseEvent) { - log.info('mongosh:use', args); + log.info('MONGOSH', mongoLogId(1_000_000_008), 'shell-api', 'Used "use" command', args); if (telemetry) { analytics.track({ @@ -188,7 +176,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:show', function(args: ShowEvent) { - log.info('mongosh:show', args); + log.info('MONGOSH', mongoLogId(1_000_000_009), 'shell-api', 'Used "show" command', args); if (telemetry) { analytics.track({ @@ -203,15 +191,22 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:setCtx', function(args: ApiEvent) { - log.info('mongosh:setCtx', jsonClone(args)); + log.info('MONGOSH', mongoLogId(1_000_000_010), 'shell-api', 'Initialized context', args); }); bus.on('mongosh:api-call', function(args: ApiEvent) { - log.info('mongosh:api-call', redactInfo(jsonClone(args))); + // TODO: redactInfo cannot handle circular or otherwise nontrivial input + let arg; + try { + arg = JSON.parse(JSON.stringify(args)); + } catch { + arg = { _inspected: inspect(args) }; + } + log.info('MONGOSH', mongoLogId(1_000_000_011), 'shell-api', 'Performed API call', redactInfo(arg)); }); bus.on('mongosh:api-load-file', function(args: ScriptLoadFileEvent) { - log.info('mongosh:api-load-file', args); + log.info('MONGOSH', mongoLogId(1_000_000_012), 'shell-api', 'Loading file via load()', args); if (telemetry) { analytics.track({ @@ -227,7 +222,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:eval-cli-script', function() { - log.info('mongosh:eval-cli-script'); + log.info('MONGOSH', mongoLogId(1_000_000_013), 'repl', 'Evaluating script passed on the command line'); if (telemetry) { analytics.track({ @@ -242,7 +237,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:mongoshrc-load', function() { - log.info('mongosh:mongoshrc-load'); + log.info('MONGOSH', mongoLogId(1_000_000_014), 'repl', 'Loading .mongoshrc.js'); if (telemetry) { analytics.track({ @@ -256,7 +251,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:mongoshrc-mongorc-warn', function() { - log.info('mongosh:mongoshrc-mongorc-warn'); + log.info('MONGOSH', mongoLogId(1_000_000_015), 'repl', 'Warning about .mongorc.js/.mongoshrc.js mismatch'); if (telemetry) { analytics.track({ @@ -270,67 +265,67 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh:mongocryptd-tryspawn', function(ev: MongocryptdTrySpawnEvent) { - log.info('mongosh:mongocryptd-tryspawn', ev); + log.info('MONGOCRYPTD', mongoLogId(1_000_000_016), 'mongocryptd', 'Trying to spawn mongocryptd', ev); }); bus.on('mongosh:mongocryptd-error', function(ev: MongocryptdErrorEvent) { - log.error('mongosh:mongocryptd-error', ev); + log.warn('MONGOCRYPTD', mongoLogId(1_000_000_017), 'mongocryptd', 'Error running mongocryptd', ev); }); bus.on('mongosh:mongocryptd-log', function(ev: MongocryptdLogEvent) { - log.info('mongosh:mongocryptd-log', ev); + log.info('MONGOCRYPTD', mongoLogId(1_000_000_018), 'mongocryptd', 'mongocryptd log message', ev); }); bus.on('mongosh-snippets:loaded', function(ev: SnippetsLoadedEvent) { - log.info('mongosh-snippets:loaded', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_019), 'snippets', 'Loaded snippets', ev); }); bus.on('mongosh-snippets:npm-lookup', function(ev: SnippetsNpmLookupEvent) { - log.info('mongosh-snippets:npm-lookup', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_020), 'snippets', 'Performing npm lookup', ev); }); bus.on('mongosh-snippets:npm-lookup-stopped', function() { - log.info('mongosh-snippets:npm-lookup-stopped'); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_021), 'snippets', 'npm lookup stopped'); }); bus.on('mongosh-snippets:npm-download-failed', function(ev: SnippetsNpmDownloadFailedEvent) { - log.error('mongosh-snippets:npm-download-failed', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_022), 'snippets', 'npm download failed', ev); }); bus.on('mongosh-snippets:npm-download-active', function(ev: SnippetsNpmDownloadActiveEvent) { - log.info('mongosh-snippets:npm-download-active', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_023), 'snippets', 'npm download active', ev); }); bus.on('mongosh-snippets:fetch-index', function(ev: SnippetsFetchIndexEvent) { - log.info('mongosh-snippets:fetch-index', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_024), 'snippets', 'Fetching snippet index', ev); }); bus.on('mongosh-snippets:fetch-cache-invalid', function() { - log.info('mongosh-snippets:fetch-cache-invalid'); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_025), 'snippets', 'Snippet cache invalid'); }); bus.on('mongosh-snippets:fetch-index-error', function(ev: SnippetsFetchIndexErrorEvent) { - log.error('mongosh-snippets:fetch-index-error', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_026), 'snippets', 'Fetching snippet index failed', ev); }); bus.on('mongosh-snippets:fetch-index-done', function() { - log.info('mongosh-snippets:fetch-index-done'); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_027), 'snippets', 'Fetching snippet index done'); }); bus.on('mongosh-snippets:package-json-edit-error', function(ev: SnippetsErrorEvent) { - log.error('mongosh-snippets:package-json-edit-error', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_028), 'snippets', 'Modifying snippets package.json failed', ev); }); bus.on('mongosh-snippets:spawn-child', function(ev: SnippetsRunNpmEvent) { - log.info('mongosh-snippets:spawn-child', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_029), 'snippets', 'Spawning helper', ev); }); bus.on('mongosh-snippets:load-snippet', function(ev: SnippetsLoadSnippetEvent) { - log.info('mongosh-snippets:load-snippet', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_030), 'snippets', 'Loading snippet', ev); }); bus.on('mongosh-snippets:snippet-command', function(ev: SnippetsCommandEvent) { - log.info('mongosh-snippets:snippet-command', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_031), 'snippets', 'Running snippet command', ev); if (telemetry && ev.args[0] === 'install') { analytics.track({ @@ -344,7 +339,7 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh-snippets:transform-error', function(ev: SnippetsTransformErrorEvent) { - log.info('mongosh-snippets:transform-error', ev); + log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_032), 'snippets', 'Rewrote error message', ev); }); const deprecatedApiCalls = new Set(); @@ -354,7 +349,7 @@ export default function setupLoggerAndTelemetry( bus.on('mongosh:evaluate-finished', function() { deprecatedApiCalls.forEach(e => { const [clazz, method] = e.split('#'); - log.info('mongosh:deprecated-api-call', { class: clazz, method }); + log.warn('MONGOSH', mongoLogId(1_000_000_033), 'shell-api', 'Deprecated API call', { class: clazz, method }); if (telemetry) { analytics.track({ @@ -372,26 +367,26 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh-sp:connect-heartbeat-failure', function(ev: SpConnectHeartbeatFailureEvent) { - log.info('mongosh-sp:connect-heartbeat-failure', { + log.warn('MONGOSH-SP', mongoLogId(1_000_000_034), 'connect', 'Server heartbeat failure', { ...ev, failure: ev.failure?.message }); }); bus.on('mongosh-sp:connect-heartbeat-succeeded', function(ev: SpConnectHeartbeatSucceededEvent) { - log.info('mongosh-sp:connect-heartbeat-succeeded', ev); + log.info('MONGOSH-SP', mongoLogId(1_000_000_035), 'connect', 'Server heartbeat succeeded', ev); }); bus.on('mongosh-sp:connect-fail-early', function() { - log.info('mongosh-sp:connect-fail-early'); + log.warn('MONGOSH-SP', mongoLogId(1_000_000_036), 'connect', 'Aborting connection attempt as irrecoverable'); }); bus.on('mongosh-sp:connect-attempt-finished', function() { - log.info('mongosh-sp:connect-attempt-finished'); + log.info('MONGOSH-SP', mongoLogId(1_000_000_037), 'connect', 'Connection attempt finished'); }); bus.on('mongosh-sp:resolve-srv-error', function(ev: SpResolveSrvErrorEvent) { - log.info('mongosh-sp:resolve-srv-error', { + log.error('MONGOSH-SP', mongoLogId(1_000_000_038), 'connect', 'Resolving SRV record failed', { from: redactURICredentials(ev.from), error: ev.error?.message, duringLoad: ev.duringLoad @@ -399,17 +394,20 @@ export default function setupLoggerAndTelemetry( }); bus.on('mongosh-sp:resolve-srv-succeeded', function(ev: SpResolveSrvSucceededEvent) { - log.info('mongosh-sp:resolve-srv-succeeded', { + log.info('MONGOSH-SP', mongoLogId(1_000_000_039), 'connect', 'Resolving SRV record succeeded', { from: redactURICredentials(ev.from), to: redactURICredentials(ev.to) }); }); bus.on('mongosh-sp:reset-connection-options', function() { - log.info('mongosh-sp:reset-connection-options'); + log.info('MONGOSH-SP', mongoLogId(1_000_000_040), 'connect', 'Reconnect because of changed connection options'); }); bus.on('mongosh-sp:missing-optional-dependency', function(ev: SpMissingOptionalDependencyEvent) { - log.info('mongosh-sp:missing-optional-dependency', { name: ev.name, error: ev?.error.message }); + log.error('MONGOSH-SP', mongoLogId(1_000_000_041), 'deps', 'Missing optional dependency', { + name: ev.name, + error: ev?.error.message + }); }); } diff --git a/packages/cli-repl/test/e2e.spec.ts b/packages/cli-repl/test/e2e.spec.ts index fae63b5f3..e785f253f 100644 --- a/packages/cli-repl/test/e2e.spec.ts +++ b/packages/cli-repl/test/e2e.spec.ts @@ -853,7 +853,7 @@ describe('e2e', function() { expect(await shell.executeLine('print(123 + 456)')).to.include('579'); await eventually(async() => { const log = await readLogfile(); - expect(log.filter(logEntry => /evaluate-input/.test(logEntry.msg))) + expect(log.filter(logEntry => /Evaluating input/.test(logEntry.msg))) .to.have.lengthOf(1); }); }); @@ -861,7 +861,7 @@ describe('e2e', function() { it('includes information about the driver version', async() => { await eventually(async() => { const log = await readLogfile(); - expect(log.filter(logEntry => /driver-initialized/.test(logEntry.msg))) + expect(log.filter(logEntry => /Driver initialized/.test(logEntry.msg))) .to.have.lengthOf(1); }); }); diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 9414446dc..d382741cb 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -244,7 +244,7 @@ export default class Mongo extends ShellApiClass { const result = await this._serviceProvider.listDatabases('admin', { ...opts }); if (!('databases' in result)) { const err = new MongoshRuntimeError('Got invalid result from "listDatabases"', CommonErrors.CommandFailed); - this._internalState.messageBus.emit('mongosh:error', err); + this._internalState.messageBus.emit('mongosh:error', err, 'shell-api'); throw err; } this._cachedDatabaseNames = result.databases.map((db: any) => db.name); @@ -320,7 +320,7 @@ export default class Mongo extends ShellApiClass { `'${cmd}' is not a valid argument for "show".`, CommonErrors.InvalidArgument ); - this._internalState.messageBus.emit('mongosh:error', err); + this._internalState.messageBus.emit('mongosh:error', err, 'shell-api'); throw err; } } diff --git a/packages/shell-api/src/shell-internal-state.ts b/packages/shell-api/src/shell-internal-state.ts index 3414bf2db..40f387a64 100644 --- a/packages/shell-api/src/shell-internal-state.ts +++ b/packages/shell-api/src/shell-internal-state.ts @@ -170,10 +170,10 @@ export default class ShellInternalState { this.currentDb = newDb; this.context.rs = new ReplicaSet(this.currentDb); this.context.sh = new Shard(this.currentDb); - this.fetchConnectionInfo().catch(err => this.messageBus.emit('mongosh:error', err)); + this.fetchConnectionInfo().catch(err => this.messageBus.emit('mongosh:error', err, 'shell-api')); // Pre-fetch for autocompletion. - this.currentDb._getCollectionNamesForCompletion().catch(err => this.messageBus.emit('mongosh:error', err)); - this.currentDb._mongo._getDatabaseNamesForCompletion().catch(err => this.messageBus.emit('mongosh:error', err)); + this.currentDb._getCollectionNamesForCompletion().catch(err => this.messageBus.emit('mongosh:error', err, 'shell-api')); + this.currentDb._mongo._getDatabaseNamesForCompletion().catch(err => this.messageBus.emit('mongosh:error', err, 'shell-api')); this.currentCursor = null; return newDb; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 683b18b10..67c487f4e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -182,7 +182,7 @@ export interface MongoshBusEventsMap { /** * Signals an error that should be logged or potentially tracked by analytics. */ - 'mongosh:error': (error: Error) => void; + 'mongosh:error': (error: Error, component: string) => void; /** * Signals the start of the evaluation of user code inside Shellevaluator. */