Skip to content

Instantly share code, notes, and snippets.

@mcasimir
Created October 12, 2021 14:24
Show Gist options
  • Save mcasimir/7f089a2e66018cf12c4cc1e516b08526 to your computer and use it in GitHub Desktop.
Save mcasimir/7f089a2e66018cf12c4cc1e516b08526 to your computer and use it in GitHub Desktop.
instance-detail.ts
import { union, unionBy } from 'lodash';
import { AnyError, MongoClient, ReadPreference } from 'mongodb';
import {
isEnterprise,
getGenuineMongoDB,
getDataLake,
} from 'mongodb-build-info';
import createLogger from '@mongodb-js/compass-logging';
const { debug } = createLogger('COMPASS-CONNECT');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const toNS = require('mongodb-ns');
interface HostInfoDetails {
system_time?: any; // ISODate?
hostname?: string;
os?: string;
os_family?: string;
kernel_version?: string;
kernel_version_string?: string;
memory_bits?: number;
memory_page_size?: number;
arch?: string;
cpu_cores?: number;
cpu_cores_physical?: number;
cpu_scheduler?: string;
cpu_frequency?: number;
cpu_string?: string;
cpu_bits?: number;
machine_model?: string;
feature_numa?: boolean;
feature_always_full_sync?: number;
feature_nfs_async?: number;
}
interface GenuineMongoDBDetails {
isGenuine: boolean;
dbType: string;
}
interface BuildInfoDetails {
version: string;
commit: string;
commit_url: string;
flags_loader: any;
flags_compiler: any;
allocator: string;
javascript_engine: string;
debug: boolean;
for_bits: number;
max_bson_object_size: number;
enterprise_module: boolean;
query_engine: any;
}
interface DataLakeDetails {
isDataLake: boolean;
version: string;
}
interface InstanceDetails {
build: BuildInfoDetails;
dataLake: DataLakeDetails;
genuineMongoDB: GenuineMongoDBDetails;
host: HostInfoDetails;
featureCompatibilityVersion: string;
databases: any[];
}
export async function getInstance(
client: MongoClient
): Promise<InstanceDetails> {
const [
connectionStatus,
getCmdLineOptsResult,
hostInfoResult,
buildInfoResult,
listDatabasesResult,
getParameterResult,
] = await Promise.all([
runAdminCommand(client, { connectionStatus: 1, showPrivileges: true }),
runAdminCommand(client, { getCmdLineOpts: 1 }, null),
runAdminCommand(client, { hostInfo: 1 }),
runAdminCommand(client, { buildInfo: 1 }),
runAdminCommand(client, {
listDatabases: 1,
nameOnly: true,
authorizedDatabases: true,
}),
runAdminCommand(client, {
getParameter: 1,
featureCompatibilityVersion: 1,
}),
]);
const databases = await fetchDatabases(
client,
connectionStatus,
listDatabasesResult
);
return {
build: adaptBuildInfo(buildInfoResult),
dataLake: buildDataLakeInfo(buildInfoResult),
genuineMongoDB: buildGenuineMongoDBInfo(
buildInfoResult,
getCmdLineOptsResult
),
host: adaptHostInfo(hostInfoResult),
databases: databases,
featureCompatibilityVersion:
getParameterResult?.featureCompatibilityVersion?.version,
};
}
function runAdminCommand(
client: MongoClient,
spec: any,
fallback: any = {}
): Promise<any> {
const adminDb = client.db('admin');
const readPreference = client.readPreference || ReadPreference.PRIMARY;
return adminDb
.command(spec, { readPreference })
.catch(ignoreNotAuthorized(fallback));
}
function buildGenuineMongoDBInfo(
buildInfo: any,
cmdLineOpts: any
): GenuineMongoDBDetails {
const { isGenuine, serverName } = getGenuineMongoDB(buildInfo, cmdLineOpts);
return {
isGenuine,
dbType: serverName,
};
}
function buildDataLakeInfo(buildInfo: any): DataLakeDetails {
const { isDataLake, dlVersion } = getDataLake(buildInfo);
return {
isDataLake,
version: dlVersion,
};
}
async function fetchDatabases(
client: MongoClient,
connectionStatus: any,
listDatabaseCommandResult: any
) {
const privileges = extractPrivilegesByDatabaseAndCollection(connectionStatus);
const listedDatabaseNames = (
(listDatabaseCommandResult || {}).databases || []
).map(({ name }: { name: string }) => name);
// we pull in the database names listed among the user privileges.
// this accounts for situations where a user would not have rights to listDatabases
// on the cluster but is authorized to perform actions on specific databases.
const databasesFromPrivileges = Object.keys(privileges);
const databaseNames = union(
listedDatabaseNames,
databasesFromPrivileges
).filter((databaseName) => !!databaseName);
const databases = (
await Promise.all(
databaseNames.map((name) =>
fetchDatabaseWithCollections(client, name, privileges)
)
)
)
.filter(Boolean)
.filter(({ name }) => name);
return databases;
}
function extractPrivilegesByDatabaseAndCollection(connectionStatus: any) {
const privileges =
connectionStatus?.authInfo?.authenticatedUserPrivileges || [];
const databases: Record<string, any> = {};
for (const privilege of privileges) {
const { db, collection } = (privilege || {}).resource || {};
databases[db] = { [collection || '']: privilege.actions || [] };
}
return databases;
}
async function fetchDatabaseWithCollections(
client: MongoClient,
dbName: string,
privileges: Record<string, any> = {}
) {
const db = client.db(dbName);
/**
* @note: Durran: For some reason the listCollections call does not take into
* account the read preference that was set on the db instance - it only looks
* in the passed options: https://github.com/mongodb/node-mongodb-native/blob/2.2/lib/db.js#L671
*/
const readPreference = client.readPreference || ReadPreference.PRIMARY;
const [database, rawCollections] = await Promise.all([
db
.command({ dbStats: 1 })
.catch(
ignoreNotAuthorized({
db: dbName,
})
)
.then(adaptDatabaseInfo),
db
.listCollections(
{ nameOnly: true, authorizedCollections: true },
{ readPreference }
)
.toArray()
.catch(ignoreNotAuthorized([]))
.catch(ignoreMongosLocalException([])),
]);
const listedCollections = rawCollections.map((rawCollection) => ({
db: dbName,
...rawCollection,
}));
const collectionsFromPrivileges = Object.keys(privileges[dbName] || {})
.filter(Boolean)
.filter((name) => !isSystemCollection(name))
.map((name) => ({
db: dbName,
name,
}));
const collections = unionBy(
listedCollections,
collectionsFromPrivileges,
'name'
)
.filter(Boolean)
.filter(({ name, db }) => name && db)
.map(adaptCollectionInfo);
return {
...database,
collections,
};
}
function isSystemCollection(name: string) {
return name.startsWith('system.');
}
function isNotAuthorized(err: AnyError) {
if (!err) {
return false;
}
const msg = err.message || JSON.stringify(err);
return new RegExp('not (authorized|allowed)').test(msg);
}
function isMongosLocalException(err: AnyError) {
if (!err) {
return false;
}
const msg = err.message || JSON.stringify(err);
return new RegExp('database through mongos').test(msg);
}
function ignoreNotAuthorized<T>(fallback: T): (err: AnyError) => Promise<T> {
return (err: AnyError) => {
if (isNotAuthorized(err)) {
debug('ignoring not authorized error and returning fallback value:', {
err,
fallback,
});
return Promise.resolve(fallback);
}
return Promise.reject(err);
};
}
function ignoreMongosLocalException<T>(
fallback: T
): (err: AnyError) => Promise<T> {
return (err: AnyError) => {
if (isMongosLocalException(err)) {
debug(
'ignoring mongos action on local db error and returning fallback value:',
{
err,
fallback,
}
);
return Promise.resolve(fallback);
}
return Promise.reject(err);
};
}
function adaptHostInfo(rawHostInfo: any): HostInfoDetails {
return {
system_time: rawHostInfo?.system?.currentTime,
hostname: rawHostInfo?.system?.hostname || 'unknown',
os: rawHostInfo?.os?.name,
os_family: (rawHostInfo?.os?.type || '').toLowerCase(),
kernel_version: rawHostInfo?.os?.version,
kernel_version_string: rawHostInfo?.extra?.versionString,
memory_bits:
parseInt(rawHostInfo?.system?.memSizeMB || 0, 10) * 1024 * 1024,
memory_page_size: rawHostInfo?.extra?.pageSize,
arch: rawHostInfo?.system?.cpuArch,
cpu_cores: rawHostInfo?.system?.numCores,
cpu_cores_physical: rawHostInfo?.extra?.physicalCores,
cpu_scheduler: rawHostInfo?.extra?.scheduler,
cpu_frequency:
parseInt(rawHostInfo?.extra?.cpuFrequencyMHz || 0, 10) * 1_000_000,
cpu_string: rawHostInfo?.extra?.cpuString,
cpu_bits: rawHostInfo?.system?.cpuAddrSize,
machine_model: rawHostInfo?.extra?.model,
feature_numa: rawHostInfo?.system?.numaEnabled,
feature_always_full_sync: rawHostInfo?.extra?.alwaysFullSync,
feature_nfs_async: rawHostInfo?.extra?.nfsAsync,
};
}
function adaptBuildInfo(rawBuildInfo: any): BuildInfoDetails {
return {
version: rawBuildInfo.version,
commit: rawBuildInfo.gitVersion,
commit_url: rawBuildInfo.gitVersion
? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`https://github.com/mongodb/mongo/commit/${rawBuildInfo.gitVersion}`
: '',
flags_loader: rawBuildInfo.loaderFlags,
flags_compiler: rawBuildInfo.compilerFlags,
allocator: rawBuildInfo.allocator,
javascript_engine: rawBuildInfo.javascriptEngine,
debug: rawBuildInfo.debug,
for_bits: rawBuildInfo.bits,
max_bson_object_size: rawBuildInfo.maxBsonObjectSize,
// Cover both cases of detecting enterprise module, see SERVER-18099.
enterprise_module: isEnterprise(rawBuildInfo),
query_engine: rawBuildInfo.queryEngine ? rawBuildInfo.queryEngine : null,
};
}
function adaptDatabaseInfo(databaseStats: any = {}) {
return {
_id: databaseStats.db,
name: databaseStats.db,
document_count: databaseStats.objects || 0,
storage_size: databaseStats.storageSize || 0,
index_count: databaseStats.indexes || 0,
index_size: databaseStats.indexSize || 0,
};
}
function adaptCollectionInfo(rawCollectionInfo: any) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const ns = toNS(rawCollectionInfo.db + '.' + rawCollectionInfo.name);
return {
_id: ns.toString(),
name: ns.collection,
database: ns.database,
readonly: rawCollectionInfo?.info?.readOnly || false,
collation: rawCollectionInfo?.options?.collation,
type: rawCollectionInfo?.type || 'collection',
view_on: rawCollectionInfo?.options?.viewOn,
pipeline: rawCollectionInfo?.options?.pipeline,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment