Skip to content

Commit 5e503f8

Browse files
committed
feat(cli-repl): add option to disable logging MONGOSH-1988
Some tests still need to be fixed
1 parent e85c465 commit 5e503f8

File tree

7 files changed

+406
-174
lines changed

7 files changed

+406
-174
lines changed

packages/cli-repl/src/cli-repl.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import { CliRepl } from './cli-repl';
3131
import { CliReplErrors } from './error-codes';
3232
import type { DevtoolsConnectOptions } from '@mongosh/service-provider-node-driver';
3333
import type { AddressInfo } from 'net';
34+
import sinon from 'sinon';
35+
import type { CliUserConfig } from '@mongosh/types';
36+
import { MongoLogWriter } from 'mongodb-log-writer';
3437
const { EJSON } = bson;
3538

3639
const delay = promisify(setTimeout);
@@ -297,7 +300,8 @@ describe('CliRepl', function () {
297300
'oidcTrustedEndpoints',
298301
'browser',
299302
'updateURL',
300-
]);
303+
'disableLogging',
304+
] satisfies (keyof CliUserConfig)[]);
301305
});
302306

303307
it('fails when trying to overwrite mongosh-owned config settings', async function () {
@@ -1310,6 +1314,36 @@ describe('CliRepl', function () {
13101314
hasDatabaseNames: true,
13111315
});
13121316

1317+
context('logging configuration', function () {
1318+
afterEach(function () {
1319+
sinon.restore();
1320+
});
1321+
1322+
it('start logging when it is not disabled', async function () {
1323+
const emitSpy = sinon.spy(cliRepl.bus, 'emit');
1324+
1325+
await cliRepl.start(await testServer.connectionString(), {});
1326+
1327+
expect(cliRepl.getConfig('disableLogging')).is.false;
1328+
1329+
expect(emitSpy).calledWith('mongosh:log-initialized');
1330+
expect(cliRepl.logWriter).is.instanceOf(MongoLogWriter);
1331+
});
1332+
1333+
it('does not start logging when it is disabled', async function () {
1334+
const emitSpy = sinon.spy(cliRepl.bus, 'emit');
1335+
cliRepl.config.disableLogging = true;
1336+
1337+
await cliRepl.start(await testServer.connectionString(), {});
1338+
1339+
expect(cliRepl.getConfig('disableLogging')).is.true;
1340+
1341+
expect(emitSpy).called;
1342+
expect(emitSpy).not.calledWith('mongosh:log-initialized');
1343+
expect(cliRepl.logWriter).is.undefined;
1344+
});
1345+
});
1346+
13131347
context('analytics integration', function () {
13141348
context('with network connectivity', function () {
13151349
let srv: http.Server;
@@ -1333,6 +1367,7 @@ describe('CliRepl', function () {
13331367
.on('data', (chunk) => {
13341368
body += chunk;
13351369
})
1370+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
13361371
.on('end', async () => {
13371372
requests.push({ req, body });
13381373
totalEventsTracked += JSON.parse(body).batch.length;
@@ -1343,7 +1378,7 @@ describe('CliRepl', function () {
13431378
})
13441379
.listen(0);
13451380
await once(srv, 'listening');
1346-
host = `http://localhost:${(srv.address() as any).port}`;
1381+
host = `http://localhost:${(srv.address() as AddressInfo).port}`;
13471382
cliReplOptions.analyticsOptions = {
13481383
host,
13491384
apiKey: '🔑',

packages/cli-repl/src/cli-repl.ts

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import type {
5353
DevtoolsProxyOptions,
5454
} from '@mongodb-js/devtools-proxy-support';
5555
import { useOrCreateAgent } from '@mongodb-js/devtools-proxy-support';
56+
import { setupMongoLogWriter } from '@mongosh/logging/lib/setup-logger-and-telemetry';
5657

5758
/**
5859
* Connecting text key.
@@ -256,6 +257,32 @@ export class CliRepl implements MongoshIOProvider {
256257
);
257258
}
258259

260+
/** Setup log writer and start logging. */
261+
private async startLogging() {
262+
await this.logManager.cleanupOldLogFiles();
263+
markTime(TimingCategories.Logging, 'cleaned up log files');
264+
const logger = await this.logManager.createLogWriter();
265+
const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
266+
if (!quiet) {
267+
this.output.write(`Current Mongosh Log ID:\t${logger.logId}\n`);
268+
}
269+
270+
this.logWriter = logger;
271+
this.logWriter = logger;
272+
setupMongoLogWriter(logger);
273+
this.logWriter = logger;
274+
setupMongoLogWriter(logger);
275+
markTime(TimingCategories.Logging, 'instantiated log writer');
276+
setupMongoLogWriter(logger);
277+
this.bus.emit('mongosh:log-initialized');
278+
logger.info('MONGOSH', mongoLogId(1_000_000_000), 'log', 'Starting log', {
279+
execPath: process.execPath,
280+
envInfo: redactSensitiveData(this.getLoggedEnvironmentVariables()),
281+
...(await buildInfo()),
282+
});
283+
markTime(TimingCategories.Logging, 'logged initial message');
284+
}
285+
259286
/**
260287
* Setup CLI environment: serviceProvider, ShellEvaluator, log connection
261288
* information, external editor, and finally start the repl.
@@ -267,7 +294,8 @@ export class CliRepl implements MongoshIOProvider {
267294
driverUri: string,
268295
driverOptions: DevtoolsConnectOptions
269296
): Promise<void> {
270-
const { version } = require('../package.json');
297+
// eslint-disable-next-line @typescript-eslint/no-var-requires
298+
const { version }: { version: string } = require('../package.json');
271299
await this.verifyNodeVersion();
272300
markTime(TimingCategories.REPLInstantiation, 'verified node version');
273301

@@ -302,41 +330,24 @@ export class CliRepl implements MongoshIOProvider {
302330

303331
try {
304332
await this.shellHomeDirectory.ensureExists();
305-
} catch (err: any) {
306-
this.warnAboutInaccessibleFile(err);
333+
} catch (err: unknown) {
334+
this.warnAboutInaccessibleFile(err as Error);
307335
}
308336
markTime(TimingCategories.REPLInstantiation, 'ensured shell homedir');
309337

310-
await this.logManager.cleanupOldLogFiles();
311-
markTime(TimingCategories.Logging, 'cleaned up log files');
312-
const logger = await this.logManager.createLogWriter();
313-
const { quiet } = CliRepl.getFileAndEvalInfo(this.cliOptions);
314-
if (!quiet) {
315-
this.output.write(`Current Mongosh Log ID:\t${logger.logId}\n`);
316-
}
317-
this.logWriter = logger;
318-
markTime(TimingCategories.Logging, 'instantiated log writer');
319-
320-
logger.info('MONGOSH', mongoLogId(1_000_000_000), 'log', 'Starting log', {
321-
execPath: process.execPath,
322-
envInfo: redactSensitiveData(this.getLoggedEnvironmentVariables()),
323-
...(await buildInfo()),
324-
});
325-
markTime(TimingCategories.Logging, 'logged initial message');
326-
327338
let analyticsSetupError: Error | null = null;
328339
try {
329340
await this.setupAnalytics();
330-
} catch (err: any) {
341+
} catch (err: unknown) {
331342
// Need to delay emitting the error on the bus so that logging is in place
332343
// as well
333-
analyticsSetupError = err;
344+
analyticsSetupError = err as Error;
334345
}
335346

336347
markTime(TimingCategories.Telemetry, 'created analytics instance');
348+
337349
setupLoggerAndTelemetry(
338350
this.bus,
339-
logger,
340351
this.toggleableAnalytics,
341352
{
342353
platform: process.platform,
@@ -352,17 +363,23 @@ export class CliRepl implements MongoshIOProvider {
352363
this.bus.emit('mongosh:error', analyticsSetupError, 'analytics');
353364
}
354365

366+
// Read local and global configuration
355367
try {
356368
this.config = await this.configDirectory.generateOrReadConfig(
357369
this.config
358370
);
359-
} catch (err: any) {
360-
this.warnAboutInaccessibleFile(err);
371+
} catch (err: unknown) {
372+
this.warnAboutInaccessibleFile(err as Error);
361373
}
362374

363375
this.globalConfig = await this.loadGlobalConfigFile();
364376
markTime(TimingCategories.UserConfigLoading, 'read global config files');
365377

378+
const disableLogging = this.getConfig('disableLogging');
379+
if (disableLogging !== true) {
380+
await this.startLogging();
381+
}
382+
366383
// Needs to happen after loading the mongosh config file(s)
367384
void this.fetchMongoshUpdateUrl();
368385

@@ -483,7 +500,7 @@ export class CliRepl implements MongoshIOProvider {
483500
if (!this.cliOptions.shell) {
484501
// We flush the telemetry data as part of exiting. Make sure we have
485502
// the right config value.
486-
this.setTelemetryEnabled(await this.getConfig('enableTelemetry'));
503+
this.setTelemetryEnabled(this.getConfig('enableTelemetry'));
487504
await this.exit(0);
488505
return;
489506
}
@@ -516,10 +533,11 @@ export class CliRepl implements MongoshIOProvider {
516533

517534
// We only enable/disable here, since the rc file/command line scripts
518535
// can disable the telemetry setting.
519-
this.setTelemetryEnabled(await this.getConfig('enableTelemetry'));
536+
this.setTelemetryEnabled(this.getConfig('enableTelemetry'));
520537
this.bus.emit('mongosh:start-mongosh-repl', { version });
521538
markTime(TimingCategories.REPLInstantiation, 'starting repl');
522539
await this.mongoshRepl.startRepl(initialized);
540+
523541
this.bus.emit('mongosh:start-session', {
524542
isInteractive: true,
525543
jsContext: this.mongoshRepl.jsContext(),
@@ -624,7 +642,7 @@ export class CliRepl implements MongoshIOProvider {
624642
files: string[],
625643
evalScripts: string[]
626644
): Promise<number> {
627-
let lastEvalResult: any;
645+
let lastEvalResult: unknown;
628646
let exitCode = 0;
629647
try {
630648
markTime(TimingCategories.Eval, 'start eval scripts');
@@ -856,9 +874,7 @@ export class CliRepl implements MongoshIOProvider {
856874
* Implements getConfig from the {@link ConfigProvider} interface.
857875
*/
858876
// eslint-disable-next-line @typescript-eslint/require-await
859-
async getConfig<K extends keyof CliUserConfig>(
860-
key: K
861-
): Promise<CliUserConfig[K]> {
877+
getConfig<K extends keyof CliUserConfig>(key: K): CliUserConfig[K] {
862878
return (
863879
(this.config as CliUserConfig)[key] ??
864880
(this.globalConfig as CliUserConfig)?.[key] ??
@@ -1259,7 +1275,7 @@ export class CliRepl implements MongoshIOProvider {
12591275
}
12601276

12611277
try {
1262-
const updateURL = (await this.getConfig('updateURL')).trim();
1278+
const updateURL = this.getConfig('updateURL').trim();
12631279
if (!updateURL) return;
12641280

12651281
const localFilePath = this.shellHomeDirectory.localPath(
@@ -1284,7 +1300,8 @@ export class CliRepl implements MongoshIOProvider {
12841300
}
12851301

12861302
async getMoreRecentMongoshVersion(): Promise<string | null> {
1287-
const { version } = require('../package.json');
1303+
// eslint-disable-next-line @typescript-eslint/no-var-requires
1304+
const { version }: { version: string } = require('../package.json');
12881305
return await this.updateNotificationManager.getLatestVersionIfMoreRecent(
12891306
process.env
12901307
.MONGOSH_ASSUME_DIFFERENT_VERSION_FOR_UPDATE_NOTIFICATION_TEST ||

packages/logging/src/analytics-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export type MongoshAnalyticsIdentity =
1111
anonymousId: string;
1212
};
1313

14-
type AnalyticsIdentifyMessage = MongoshAnalyticsIdentity & {
14+
export type AnalyticsIdentifyMessage = MongoshAnalyticsIdentity & {
1515
traits: { platform: string; session_id: string };
1616
timestamp?: Date;
1717
};

packages/logging/src/multi-set.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* A helper class for keeping track of how often specific events occurred.
3+
*/
4+
export class MultiSet<T extends Record<string, unknown>> {
5+
_entries: Map<string, number> = new Map();
6+
7+
add(entry: T): void {
8+
const key = JSON.stringify(Object.entries(entry).sort());
9+
this._entries.set(key, (this._entries.get(key) ?? 0) + 1);
10+
}
11+
12+
clear(): void {
13+
this._entries.clear();
14+
}
15+
16+
*[Symbol.iterator](): Iterator<[T, number]> {
17+
for (const [key, count] of this._entries) {
18+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
19+
yield [Object.fromEntries(JSON.parse(key)) as T, count];
20+
}
21+
}
22+
}
23+
24+
/**
25+
* It transforms a random string into snake case. Snake case is completely
26+
* lowercase and uses '_' to separate words. For example:
27+
*
28+
* This function defines a "word" as a sequence of characters until the next `.` or capital letter.
29+
*
30+
* 'Random String' => 'random_string'
31+
*
32+
* It will also remove any non alphanumeric characters to ensure the string
33+
* is compatible with Segment. For example:
34+
*
35+
* 'Node.js REPL Instantiation' => 'node_js_repl_instantiation'
36+
*
37+
* @param str Any non snake-case formatted string
38+
* @returns The snake-case formatted string
39+
*/
40+
export function toSnakeCase(str: string): string {
41+
const matches = str.match(
42+
/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g
43+
);
44+
if (!matches) {
45+
return str;
46+
}
47+
48+
return matches.map((x) => x.toLowerCase()).join('_');
49+
}

0 commit comments

Comments
 (0)