Skip to content

Commit 8af1f1a

Browse files
committed
MONGOSH-304 - Support server stats and info methods
1 parent e3f9a9a commit 8af1f1a

File tree

8 files changed

+590
-24
lines changed

8 files changed

+590
-24
lines changed

packages/browser-repl/src/components/shell-output-line.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ export class ShellOutputLine extends Component<ShellOutputLineProps> {
5656
return <ShowDbsOutput value={value} />;
5757
}
5858

59+
if (type === 'StatsResult') {
60+
const res = Object.keys(value).reduce((str, c) => {
61+
return `${str}\n${c}\n${value[c]}\n---\n`;
62+
}, '');
63+
return <pre>{res}</pre>;
64+
}
65+
5966
if (type === 'ShowCollectionsResult') {
6067
return <ShowCollectionsOutput value={value} />;
6168
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export default function formatOutput(evaluationResult: EvaluationResult): string
4545
return formatCollections(value);
4646
}
4747

48+
if (type === 'StatsResult') {
49+
return formatStats(value);
50+
}
51+
4852
if (type === 'Error') {
4953
return formatError(value);
5054
}
@@ -71,6 +75,12 @@ function formatDatabases(output): string {
7175
return textTable(tableEntries, { align: ['l', 'r'] });
7276
}
7377

78+
function formatStats(output): string {
79+
return Object.keys(output).reduce((str, c) => {
80+
return `${str}\n${c}\n${inspect(output[c])}\n---\n`;
81+
}, '');
82+
}
83+
7484
export function formatError(error): string {
7585
let result = '';
7686
if (error.name) result += `\r${clr(error.name, ['bold', 'red'])}: `;

packages/i18n/src/locales/en_US.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,12 @@ const translations = {
522522
getMongo: {
523523
description: 'Returns the Mongo object.',
524524
example: 'db.collection.getMongo()'
525-
}
525+
},
526+
latencyStats: {
527+
link: 'https://docs.mongodb.com/manual/reference/method/db.collection.latencyStats',
528+
description: 'returns the $latencyStats aggregation for the collection. Takes an options document with an optional boolean \'histograms\' field.',
529+
example: 'db.latencyStats({ histograms: true })'
530+
},
526531
}
527532
}
528533
},
@@ -940,6 +945,46 @@ const translations = {
940945
link: 'https://docs.mongodb.com/manual/reference/method/db.fsyncUnlock',
941946
description: 'Calls the fsyncUnlock command. Reduces the lock taken by db.fsyncLock() on a mongod instance by 1.',
942947
example: 'db.fsyncUnlock(<options>)'
948+
},
949+
version: {
950+
link: 'https://docs.mongodb.com/manual/reference/method/db.version',
951+
description: 'returns the db version. uses the buildinfo command',
952+
example: 'db.version()'
953+
},
954+
serverBits: {
955+
link: 'https://docs.mongodb.com/manual/reference/method/db.serverBits',
956+
description: 'returns the db serverBits. uses the buildInfo command',
957+
example: 'db.serverBits()',
958+
},
959+
isMaster: {
960+
link: 'https://docs.mongodb.com/manual/reference/method/db.isMaster',
961+
description: 'Calls the isMaster command',
962+
example: 'db.isMaster()'
963+
},
964+
serverBuildInfo: {
965+
link: 'https://docs.mongodb.com/manual/reference/method/db.serverBuildInfo',
966+
description: 'returns the db serverBuildInfo. uses the buildInfo command',
967+
example: 'db.serverBuildInfo()',
968+
},
969+
stats: {
970+
link: 'https://docs.mongodb.com/manual/reference/method/db.stats',
971+
description: 'returns the db stats. uses the dbStats command',
972+
example: 'db.stats(<scale>)',
973+
},
974+
hostInfo: {
975+
link: 'https://docs.mongodb.com/manual/reference/method/db.hostInfo',
976+
description: 'Calls the hostInfo command',
977+
example: 'db.hostInfo()'
978+
},
979+
serverCmdLineOpts: {
980+
link: 'https://docs.mongodb.com/manual/reference/method/db.serverCmdLineOpts',
981+
description: 'returns the db serverCmdLineOpts. uses the getCmdLineOpts command',
982+
example: 'db.serverCmdLineOpts()',
983+
},
984+
printCollectionStats: {
985+
link: 'https://docs.mongodb.com/manual/reference/method/db.printCollectionStats',
986+
description: 'Prints the collection.stats for each collection in the db.',
987+
example: 'db.printCollectionStats(scale)',
943988
}
944989
}
945990
}

packages/shell-api/src/collection.spec.ts

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/camelcase */
12
import { expect, use } from 'chai';
23
import sinon, { StubbedInstance, stubInterface } from 'ts-sinon';
34
import { EventEmitter } from 'events';
@@ -62,6 +63,7 @@ describe('Collection', () => {
6263
beforeEach(() => {
6364
bus = stubInterface<EventEmitter>();
6465
serviceProvider = stubInterface<ServiceProvider>();
66+
serviceProvider.runCommand.resolves({ ok: 1 });
6567
serviceProvider.initialDb = 'test';
6668
serviceProvider.bsonLibrary = bson;
6769
internalState = new ShellInternalState(serviceProvider, bus);
@@ -514,16 +516,78 @@ describe('Collection', () => {
514516
});
515517

516518
describe('stats', () => {
517-
let result;
519+
it('calls serviceProvider.runCommand on the database with no options', async() => {
520+
await collection.stats();
518521

519-
beforeEach(() => {
520-
result = {};
521-
serviceProvider.stats.resolves(result);
522+
expect(serviceProvider.runCommand).to.have.been.calledWith(
523+
database._name,
524+
{ collStats: 'coll1', scale: 1 } // ensure simple collname
525+
);
526+
});
527+
528+
it('calls serviceProvider.runCommand on the database with scale option', async() => {
529+
await collection.stats({ scale: 2 });
530+
531+
expect(serviceProvider.runCommand).to.have.been.calledWith(
532+
database._name,
533+
{ collStats: collection._name, scale: 2 }
534+
);
535+
});
536+
537+
it('calls serviceProvider.runCommand on the database with legacy scale', async() => {
538+
await collection.stats(2);
539+
540+
expect(serviceProvider.runCommand).to.have.been.calledWith(
541+
database._name,
542+
{ collStats: collection._name, scale: 2 }
543+
);
544+
});
545+
546+
context('indexDetails', () => {
547+
let expectedResult;
548+
let indexesResult;
549+
beforeEach(() => {
550+
expectedResult = { ok: 1, indexDetails: { k1_1: { details: 1 }, k2_1: { details: 2 } } };
551+
indexesResult = [ { v: 2, key: { k1: 1 }, name: 'k1_1' }, { v: 2, key: { k2: 1 }, name: 'k2_1' }];
552+
serviceProvider.runCommand.resolves(expectedResult);
553+
serviceProvider.getIndexes.resolves(indexesResult);
554+
});
555+
it('not returned when no args', async() => {
556+
const result = await collection.stats();
557+
expect(result).to.deep.equal({ ok: 1 });
558+
});
559+
it('not returned when options indexDetails: false', async() => {
560+
const result = await collection.stats({ indexDetails: false });
561+
expect(result).to.deep.equal({ ok: 1 });
562+
});
563+
it('returned all when true, even if no key/name set', async() => {
564+
const result = await collection.stats({ indexDetails: true });
565+
expect(result).to.deep.equal(expectedResult);
566+
});
567+
it('returned only 1 when indexDetailsName set', async() => {
568+
const result = await collection.stats({ indexDetails: true, indexDetailsName: 'k2_1' });
569+
expect(result).to.deep.equal({ ok: 1, indexDetails: { 'k2_1': expectedResult.indexDetails.k2_1 } });
570+
});
571+
it('returned all when indexDetailsName set but not found', async() => {
572+
const result = await collection.stats({ indexDetails: true, indexDetailsName: 'k3_1' });
573+
expect(result).to.deep.equal(expectedResult);
574+
});
575+
it('returned only 1 when indexDetailsKey set', async() => {
576+
const result = await collection.stats({ indexDetails: true, indexDetailsKey: indexesResult[1].key });
577+
expect(result).to.deep.equal({ ok: 1, indexDetails: { 'k2_1': expectedResult.indexDetails.k2_1 } });
578+
});
579+
it('returned all when indexDetailsKey set but not found', async() => {
580+
const result = await collection.stats({ indexDetails: true, indexDetailsKey: { other: 1 } });
581+
expect(result).to.deep.equal(expectedResult);
582+
});
522583
});
523584

524-
it('returns stats', async() => {
525-
expect(await collection.stats({ scale: 1 })).to.equal(result);
526-
expect(serviceProvider.stats).to.have.been.calledOnceWith('db1', 'coll1', { scale: 1 });
585+
it('throws if serviceProvider.runCommand rejects', async() => {
586+
const expectedError = new Error();
587+
serviceProvider.runCommand.rejects(expectedError);
588+
const catchedError = await collection.stats()
589+
.catch(e => e);
590+
expect(catchedError).to.equal(expectedError);
527591
});
528592
});
529593

@@ -801,5 +865,35 @@ describe('Collection', () => {
801865
expect(explainable._verbosity).to.equal('queryPlanner');
802866
});
803867
});
868+
869+
describe('latencyStats', () => {
870+
it('calls serviceProvider.aggregate on the database with options', async() => {
871+
serviceProvider.aggregate.returns({ toArray: async() => ([]) } as any);
872+
await collection.latencyStats({ histograms: true });
873+
874+
expect(serviceProvider.aggregate).to.have.been.calledWith(
875+
database._name,
876+
collection._name,
877+
[{
878+
$collStats: { latencyStats: { histograms: true } }
879+
}],
880+
{}
881+
);
882+
});
883+
884+
it('returns whatever serviceProvider.runCommand returns', async() => {
885+
serviceProvider.aggregate.returns({ toArray: async() => ([{ 1: 'db1' }]) } as any);
886+
const result = await collection.latencyStats();
887+
expect(result).to.deep.equal([{ 1: 'db1' }]);
888+
});
889+
890+
it('throws if serviceProvider.runCommand rejects', async() => {
891+
const expectedError = new Error();
892+
serviceProvider.aggregate.throws(expectedError);
893+
const catchedError = await collection.latencyStats()
894+
.catch(e => e);
895+
expect(catchedError).to.equal(expectedError);
896+
});
897+
});
804898
});
805899
});

packages/shell-api/src/collection.ts

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable complexity */
12
import Mongo from './mongo';
23
import {
34
shellApiClassDefault,
@@ -21,7 +22,7 @@ import {
2122
InsertOneResult,
2223
UpdateResult
2324
} from './index';
24-
import { MongoshInvalidInputError } from '@mongosh/errors';
25+
import { MongoshInvalidInputError, MongoshRuntimeError } from '@mongosh/errors';
2526

2627
@shellApiClassDefault
2728
@hasAsyncChild
@@ -1122,18 +1123,6 @@ export default class Collection extends ShellApiClass {
11221123
return this._mongo;
11231124
}
11241125

1125-
/**
1126-
* Get all the collection statistics.
1127-
*
1128-
* @param {Object} options - The stats options.
1129-
* @return {Promise} returns Promise
1130-
*/
1131-
@returnsPromise
1132-
async stats(options: Document = {}): Promise<any> {
1133-
this._emitCollectionApiCall('stats', { options });
1134-
return await this._mongo._serviceProvider.stats(this._database._name, this._name, options);
1135-
}
1136-
11371126
/**
11381127
* Get the collection dataSize.
11391128
*
@@ -1254,4 +1243,86 @@ export default class Collection extends ShellApiClass {
12541243
this._emitCollectionApiCall('explain', { verbosity });
12551244
return new Explainable(this._mongo, this, verbosity);
12561245
}
1246+
1247+
@returnsPromise
1248+
async stats(options: any = {}): Promise<any> {
1249+
if (typeof options === 'number') {
1250+
options = {
1251+
scale: options
1252+
};
1253+
}
1254+
if (options.indexDetailsKey && options.indexDetailsName) {
1255+
throw new MongoshInvalidInputError('Cannot filter indexDetails on both indexDetailsKey and indexDetailsName');
1256+
}
1257+
if (options.indexDetailsKey && typeof options.indexDetailsKey !== 'object') {
1258+
throw new MongoshInvalidInputError(`Expected options.indexDetailsKey to be a document, got ${typeof options.indexDetailsKey}`);
1259+
}
1260+
if (options.indexDetailsName && typeof options.indexDetailsName !== 'string') {
1261+
throw new MongoshInvalidInputError(`Expected options.indexDetailsName to be a string, got ${typeof options.indexDetailsName}`);
1262+
}
1263+
options.scale = options.scale || 1;
1264+
options.indexDetails = options.indexDetails || false;
1265+
1266+
const result = await this._mongo._serviceProvider.runCommand(
1267+
this._database._name,
1268+
{
1269+
collStats: this._name, scale: options.scale
1270+
}
1271+
);
1272+
if (!result || !result.ok) {
1273+
throw new MongoshRuntimeError(`Error running collStats command ${result ? result.errmsg : ''}`);
1274+
}
1275+
let filterIndexName = options.indexDetailsName;
1276+
if (!filterIndexName && options.indexDetailsKey) {
1277+
const indexes = await this._mongo._serviceProvider.getIndexes(this._database._name, this._name);
1278+
indexes.forEach((spec) => {
1279+
if (JSON.stringify(spec.key) === JSON.stringify(options.indexDetailsKey)) {
1280+
filterIndexName = spec.name;
1281+
}
1282+
});
1283+
}
1284+
1285+
/**
1286+
* Remove indexDetails if options.indexDetails is true. From the old shell code.
1287+
* @param stats
1288+
*/
1289+
const updateStats = (stats): void => {
1290+
if (!stats.indexDetails) {
1291+
return;
1292+
}
1293+
if (!options.indexDetails) {
1294+
delete stats.indexDetails;
1295+
return;
1296+
}
1297+
if (!filterIndexName) {
1298+
return;
1299+
}
1300+
for (const key of Object.keys(stats.indexDetails)) {
1301+
if (key === filterIndexName) {
1302+
continue;
1303+
}
1304+
delete stats.indexDetails[key];
1305+
}
1306+
};
1307+
updateStats(result);
1308+
1309+
if (result.sharded) {
1310+
for (const shardName of result.shards) {
1311+
updateStats(result.shards[shardName]);
1312+
}
1313+
}
1314+
return result;
1315+
}
1316+
1317+
@returnsPromise
1318+
async latencyStats(options = {}): Promise<any> {
1319+
const pipeline = [{ $collStats: { latencyStats: options } }];
1320+
const providerCursor = this._mongo._serviceProvider.aggregate(
1321+
this._database._name,
1322+
this._name,
1323+
pipeline,
1324+
{}
1325+
);
1326+
return await providerCursor.toArray();
1327+
}
12571328
}

0 commit comments

Comments
 (0)