UNPKG

mongodb

Version:
354 lines (297 loc) • 10.6 kB
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}`; }