mongodb
Version:
The official MongoDB driver for Node.js
354 lines (297 loc) • 10.6 kB
text/typescript
import * as os from 'os';
import * as process from 'process';
import { BSON, type Document, Int32, NumberUtils } from '../../bson';
import { MongoInvalidArgumentError } from '../../error';
import type { DriverInfo, MongoOptions } from '../../mongo_client';
import { fileIsAccessible } from '../../utils';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const NODE_DRIVER_VERSION = require('../../../package.json').version;
/** @internal */
export function isDriverInfoEqual(info1: DriverInfo, info2: DriverInfo): boolean {
/** for equality comparison, we consider "" as unset */
const nonEmptyCmp = (s1: string | undefined, s2: string | undefined): boolean => {
s1 ||= undefined;
s2 ||= undefined;
return s1 === s2;
};
return (
nonEmptyCmp(info1.name, info2.name) &&
nonEmptyCmp(info1.platform, info2.platform) &&
nonEmptyCmp(info1.version, info2.version)
);
}
/**
* @internal
* @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.md#hello-command
*/
export interface ClientMetadata {
driver: {
name: string;
version: string;
};
os: {
type: string;
name?: NodeJS.Platform;
architecture?: string;
version?: string;
};
platform: string;
application?: {
name: string;
};
/** FaaS environment information */
env?: {
name?: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel';
timeout_sec?: Int32;
memory_mb?: Int32;
region?: string;
container?: {
runtime?: string;
orchestrator?: string;
};
};
}
/** @internal */
export class LimitedSizeDocument {
private document = new Map();
/** BSON overhead: Int32 + Null byte */
private documentSize = 5;
private maxSize: number;
constructor(maxSize: number) {
this.maxSize = maxSize;
}
/** Only adds key/value if the bsonByteLength is less than MAX_SIZE */
public ifItFitsItSits(key: string, value: Record<string, any> | string): boolean {
// The BSON byteLength of the new element is the same as serializing it to its own document
// subtracting the document size int32 and the null terminator.
const newElementSize = BSON.serialize(new Map().set(key, value)).byteLength - 5;
if (newElementSize + this.documentSize > this.maxSize) {
return false;
}
this.documentSize += newElementSize;
this.document.set(key, value);
return true;
}
toObject(): Document {
return BSON.deserialize(BSON.serialize(this.document), {
promoteLongs: false,
promoteBuffers: false,
promoteValues: false,
useBigInt64: false
});
}
}
type MakeClientMetadataOptions = Pick<MongoOptions, 'appName'>;
/**
* From the specs:
* Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit:
* 1. Omit fields from `env` except `env.name`.
* 2. Omit fields from `os` except `os.type`.
* 3. Omit the `env` document entirely.
* 4. Truncate `platform`. -- special we do not truncate this field
*/
export async function makeClientMetadata(
driverInfoList: DriverInfo[],
{ appName = '' }: MakeClientMetadataOptions
): Promise<ClientMetadata> {
const metadataDocument = new LimitedSizeDocument(512);
// Add app name first, it must be sent
if (appName.length > 0) {
const name =
Buffer.byteLength(appName, 'utf8') <= 128
? appName
: Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8');
metadataDocument.ifItFitsItSits('application', { name });
}
const driverInfo = {
name: 'nodejs',
version: NODE_DRIVER_VERSION
};
// This is where we handle additional driver info added after client construction.
for (const { name: n = '', version: v = '' } of driverInfoList) {
if (n.length > 0) {
driverInfo.name = `${driverInfo.name}|${n}`;
}
if (v.length > 0) {
driverInfo.version = `${driverInfo.version}|${v}`;
}
}
if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) {
throw new MongoInvalidArgumentError(
'Unable to include driverInfo name and version, metadata cannot exceed 512 bytes'
);
}
let runtimeInfo = getRuntimeInfo();
// This is where we handle additional driver info added after client construction.
for (const { platform = '' } of driverInfoList) {
if (platform.length > 0) {
runtimeInfo = `${runtimeInfo}|${platform}`;
}
}
if (!metadataDocument.ifItFitsItSits('platform', runtimeInfo)) {
throw new MongoInvalidArgumentError(
'Unable to include driverInfo platform, metadata cannot exceed 512 bytes'
);
}
// Note: order matters, os.type is last so it will be removed last if we're at maxSize
const osInfo = new Map()
.set('name', os.platform())
.set('architecture', os.arch())
.set('version', os.release())
.set('type', os.type());
if (!metadataDocument.ifItFitsItSits('os', osInfo)) {
for (const key of osInfo.keys()) {
osInfo.delete(key);
if (osInfo.size === 0) break;
if (metadataDocument.ifItFitsItSits('os', osInfo)) break;
}
}
const faasEnv = getFAASEnv();
if (faasEnv != null) {
if (!metadataDocument.ifItFitsItSits('env', faasEnv)) {
for (const key of faasEnv.keys()) {
faasEnv.delete(key);
if (faasEnv.size === 0) break;
if (metadataDocument.ifItFitsItSits('env', faasEnv)) break;
}
}
}
return await addContainerMetadata(metadataDocument.toObject() as ClientMetadata);
}
let dockerPromise: Promise<boolean>;
type ContainerMetadata = NonNullable<NonNullable<ClientMetadata['env']>['container']>;
/** @internal */
async function getContainerMetadata(): Promise<ContainerMetadata> {
dockerPromise ??= fileIsAccessible('/.dockerenv');
const isDocker = await dockerPromise;
const { KUBERNETES_SERVICE_HOST = '' } = process.env;
const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false;
const containerMetadata: ContainerMetadata = {};
if (isDocker) containerMetadata.runtime = 'docker';
if (isKubernetes) containerMetadata.orchestrator = 'kubernetes';
return containerMetadata;
}
/**
* @internal
* Re-add each metadata value.
* Attempt to add new env container metadata, but keep old data if it does not fit.
*/
async function addContainerMetadata(originalMetadata: ClientMetadata): Promise<ClientMetadata> {
const containerMetadata = await getContainerMetadata();
if (Object.keys(containerMetadata).length === 0) return originalMetadata;
const extendedMetadata = new LimitedSizeDocument(512);
const extendedEnvMetadata: NonNullable<ClientMetadata['env']> = {
...originalMetadata?.env,
container: containerMetadata
};
for (const [key, val] of Object.entries(originalMetadata)) {
if (key !== 'env') {
extendedMetadata.ifItFitsItSits(key, val);
} else {
if (!extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata)) {
// add in old data if newer / extended metadata does not fit
extendedMetadata.ifItFitsItSits('env', val);
}
}
}
if (!('env' in originalMetadata)) {
extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
}
return extendedMetadata.toObject() as ClientMetadata;
}
/**
* Collects FaaS metadata.
* - `name` MUST be the last key in the Map returned.
*/
export function getFAASEnv(): Map<string, string | Int32> | null {
const {
AWS_EXECUTION_ENV = '',
AWS_LAMBDA_RUNTIME_API = '',
FUNCTIONS_WORKER_RUNTIME = '',
K_SERVICE = '',
FUNCTION_NAME = '',
VERCEL = '',
AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '',
AWS_REGION = '',
FUNCTION_MEMORY_MB = '',
FUNCTION_REGION = '',
FUNCTION_TIMEOUT_SEC = '',
VERCEL_REGION = ''
} = process.env;
const isAWSFaaS =
AWS_EXECUTION_ENV.startsWith('AWS_Lambda_') || AWS_LAMBDA_RUNTIME_API.length > 0;
const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0;
const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0;
const isVercelFaaS = VERCEL.length > 0;
// Note: order matters, name must always be the last key
const faasEnv = new Map();
// When isVercelFaaS is true so is isAWSFaaS; Vercel inherits the AWS env
if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) {
if (VERCEL_REGION.length > 0) {
faasEnv.set('region', VERCEL_REGION);
}
faasEnv.set('name', 'vercel');
return faasEnv;
}
if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) {
if (AWS_REGION.length > 0) {
faasEnv.set('region', AWS_REGION);
}
if (
AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 &&
Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE)
) {
faasEnv.set('memory_mb', new Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE));
}
faasEnv.set('name', 'aws.lambda');
return faasEnv;
}
if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) {
faasEnv.set('name', 'azure.func');
return faasEnv;
}
if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) {
if (FUNCTION_REGION.length > 0) {
faasEnv.set('region', FUNCTION_REGION);
}
if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) {
faasEnv.set('memory_mb', new Int32(FUNCTION_MEMORY_MB));
}
if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) {
faasEnv.set('timeout_sec', new Int32(FUNCTION_TIMEOUT_SEC));
}
faasEnv.set('name', 'gcp.func');
return faasEnv;
}
return null;
}
/**
* @internal
* This type represents the global Deno object and the minimal type contract we expect it to satisfy.
*/
declare const Deno: { version?: { deno?: string } } | undefined;
/**
* @internal
* This type represents the global Bun object and the minimal type contract we expect it to satisfy.
*/
declare const Bun: { (): void; version?: string } | undefined;
/**
* @internal
* Get current JavaScript runtime platform
*
* NOTE: The version information fetching is intentionally written defensively
* to avoid having a released driver version that becomes incompatible
* with a future change to these global objects.
*/
function getRuntimeInfo(): string {
const endianness = NumberUtils.isBigEndian ? 'BE' : 'LE';
if ('Deno' in globalThis) {
const version = typeof Deno?.version?.deno === 'string' ? Deno?.version?.deno : '0.0.0-unknown';
return `Deno v${version}, ${endianness}`;
}
if ('Bun' in globalThis) {
const version = typeof Bun?.version === 'string' ? Bun?.version : '0.0.0-unknown';
return `Bun v${version}, ${endianness}`;
}
return `Node.js ${process.version}, ${endianness}`;
}