Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cmap/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
MongoRuntimeError,
needsRetryableWriteLabel
} from '../error';
import { Callback, ClientMetadata, HostAddress, ns } from '../utils';
import { Callback, HostAddress, ns } from '../utils';
import { AuthContext, AuthProvider } from './auth/auth_provider';
import { GSSAPI } from './auth/gssapi';
import { MongoCR } from './auth/mongocr';
Expand All @@ -28,6 +28,7 @@ import { AuthMechanism } from './auth/providers';
import { ScramSHA1, ScramSHA256 } from './auth/scram';
import { X509 } from './auth/x509';
import { CommandOptions, Connection, ConnectionOptions, CryptoConnection } from './connection';
import type { ClientMetadata } from './handshake/client_metadata';
import {
MAX_SUPPORTED_SERVER_VERSION,
MAX_SUPPORTED_WIRE_VERSION,
Expand Down
2 changes: 1 addition & 1 deletion src/cmap/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { applySession, ClientSession, updateSessionFromResponse } from '../sessi
import {
calculateDurationInMs,
Callback,
ClientMetadata,
HostAddress,
maxWireVersion,
MongoDBNamespace,
Expand All @@ -46,6 +45,7 @@ import {
} from './command_monitoring_events';
import { BinMsg, Msg, Query, Response, WriteProtocolMessageType } from './commands';
import type { Stream } from './connect';
import type { ClientMetadata } from './handshake/client_metadata';
import { MessageStream, OperationDescription } from './message_stream';
import { StreamDescription, StreamDescriptionOptions } from './stream_description';
import { getReadPreference, isSharded } from './wire_protocol/shared';
Expand Down
112 changes: 112 additions & 0 deletions src/cmap/handshake/client_metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { calculateObjectSize, Int32 } from 'bson';
import * as os from 'os';

import type { MongoOptions } from '../../mongo_client';
import { applyFaasEnvMetadata } from './faas_provider';

/**
* @public
* @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#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;
};

/** Data containing information about the environment, if the driver is running in a FAAS environment. */
env?: {
name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel';
timeout_sec?: Int32;
memory_mb?: Int32;
region?: string;
url?: string;
};
}

/**
* @internal
* truncates the client metadata according to the priority outlined here
* https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#limitations
*/
export function truncateClientMetadata(metadata: ClientMetadata): ClientMetadata {
if (calculateObjectSize(metadata) <= 512) {
return metadata;
}
// 1. Truncate ``platform``.
// no-op - we don't truncate because the `platform` field is essentially a fixed length in Node
// and there isn't anything we can truncate that without removing useful information.

// 2. Omit fields from ``env`` except ``env.name``.
if (metadata.env) {
metadata.env = { name: metadata.env.name };
}
if (calculateObjectSize(metadata) <= 512) {
return metadata;
}

// 3. Omit the ``env`` document entirely.
delete metadata.env;

return metadata;
}

/** @public */
export interface ClientMetadataOptions {
driverInfo?: {
name?: string;
version?: string;
platform?: string;
};
appName?: string;
}

// eslint-disable-next-line @typescript-eslint/no-var-requires
const NODE_DRIVER_VERSION = require('../../../package.json').version;

export function makeClientMetadata(
options: Pick<MongoOptions, 'appName' | 'driverInfo'>
): ClientMetadata {
const name = options.driverInfo.name ? `nodejs|${options.driverInfo.name}` : 'nodejs';
const version = options.driverInfo.version
? `${NODE_DRIVER_VERSION}|${options.driverInfo.version}`
: NODE_DRIVER_VERSION;
const platform = options.driverInfo.platform
? `Node.js ${process.version}, ${os.endianness()}|${options.driverInfo.platform}`
: `Node.js ${process.version}, ${os.endianness()}`;

const metadata: ClientMetadata = {
driver: {
name,
version
},
os: {
type: os.type(),
name: process.platform,
architecture: process.arch,
version: os.release()
},
platform
};

if (options.appName) {
// MongoDB requires the appName not exceed a byte length of 128
const name =
Buffer.byteLength(options.appName, 'utf8') <= 128
? options.appName
: Buffer.from(options.appName, 'utf8').subarray(0, 128).toString('utf8');
metadata.application = { name };
}

return truncateClientMetadata(applyFaasEnvMetadata(metadata));
}
93 changes: 93 additions & 0 deletions src/cmap/handshake/faas_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Int32 } from 'bson';

import { identity } from '../../utils';
import type { ClientMetadata } from './client_metadata';

export type FAASProvider = 'aws' | 'gcp' | 'azure' | 'vercel' | 'none';

function isNonEmptyString(s: string | undefined): s is string {
return typeof s === 'string' && s.length > 0;
}

export function determineFAASProvider(): FAASProvider {
const awsPresent =
isNonEmptyString(process.env.AWS_EXECUTION_ENV) ||
isNonEmptyString(process.env.AWS_LAMBDA_RUNTIME_API);
const azurePresent = isNonEmptyString(process.env.FUNCTIONS_WORKER_RUNTIME);
const gcpPresent =
isNonEmptyString(process.env.K_SERVICE) || isNonEmptyString(process.env.FUNCTION_NAME);
const vercelPresent = isNonEmptyString(process.env.VERCEL);

const numberOfProvidersPresent = [awsPresent, azurePresent, gcpPresent, vercelPresent].filter(
identity
).length;
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be an ifelse sequence instead?, we have the order encoded below with the early returns, I'd just add an else case to the end of that and return none.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, look closely at the logic. It purposefully returns none when there's more than one faas provider present


if (numberOfProvidersPresent !== 1) {
return 'none';
}

if (awsPresent) return 'aws';
if (azurePresent) return 'azure';
if (gcpPresent) return 'gcp';
return 'vercel';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should vercel come before aws? IIRC vercel runs on AWS lambda is it possible aws env vars are also defined in vercel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above – they will never both be set. If they are, we explicitly determine none

}

function applyAzureMetadata(m: ClientMetadata): ClientMetadata {
m.env = { name: 'azure.func' };
return m;
}

function applyGCPMetadata(m: ClientMetadata): ClientMetadata {
m.env = { name: 'gcp.func' };

const memory_mb = Number(process.env.FUNCTION_MEMORY_MB);
if (Number.isInteger(memory_mb)) {
m.env.memory_mb = new Int32(memory_mb);
}
const timeout_sec = Number(process.env.FUNCTION_TIMEOUT_SEC);
if (Number.isInteger(timeout_sec)) {
m.env.timeout_sec = new Int32(timeout_sec);
}
if (isNonEmptyString(process.env.FUNCTION_REGION)) {
m.env.region = process.env.FUNCTION_REGION;
}

return m;
}

function applyAWSMetadata(m: ClientMetadata): ClientMetadata {
m.env = { name: 'aws.lambda' };
if (isNonEmptyString(process.env.AWS_REGION)) {
m.env.region = process.env.AWS_REGION;
}
const memory_mb = Number(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE);
if (Number.isInteger(memory_mb)) {
m.env.memory_mb = new Int32(memory_mb);
}
return m;
}

function applyVercelMetadata(m: ClientMetadata): ClientMetadata {
m.env = { name: 'vercel' };
if (isNonEmptyString(process.env.VERCEL_URL)) {
m.env.url = process.env.VERCEL_URL;
}
if (isNonEmptyString(process.env.VERCEL_REGION)) {
m.env.region = process.env.VERCEL_REGION;
}
return m;
}

export function applyFaasEnvMetadata(metadata: ClientMetadata): ClientMetadata {
const handlerMap: Record<FAASProvider, (m: ClientMetadata) => ClientMetadata> = {
aws: applyAWSMetadata,
gcp: applyGCPMetadata,
azure: applyAzureMetadata,
vercel: applyVercelMetadata,
none: identity
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding a step for none, can't none do nothing to the input? maybe none could instead be represented by a null determineFAASProvider result that leads to returning the metadata input.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none does do nothing to the input, that's what identity is. I chose this over using null for simplicity in typing and edge casing. It flows everything through the same path logically

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an early return based on null should narrow the key to be one of the acceptable strings, this way we can write less handling for the obvious case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That still requires handling for the obvious case, no? it's just that you've put it in a null check. I like this approach because there's no special casing values (or a "lack" of value here, indicated by none or null)

};
const faasProvider = determineFAASProvider();

const faasMetadataProvider = handlerMap[faasProvider];
return faasMetadataProvider(metadata);
}
2 changes: 1 addition & 1 deletion src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { URLSearchParams } from 'url';
import type { Document } from './bson';
import { MongoCredentials } from './cmap/auth/mongo_credentials';
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';
import { makeClientMetadata } from './cmap/handshake/client_metadata';
import { Compressor, CompressorName } from './cmap/wire_protocol/compression';
import { Encrypter } from './encrypter';
import {
Expand All @@ -32,7 +33,6 @@ import {
emitWarningOnce,
HostAddress,
isRecord,
makeClientMetadata,
parseInteger,
setDifference
} from './utils';
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export type {
WaitQueueMember,
WithConnectionCallback
} from './cmap/connection_pool';
export type { ClientMetadata, ClientMetadataOptions } from './cmap/handshake/client_metadata';
export type {
MessageStream,
MessageStreamOptions,
Expand Down Expand Up @@ -463,8 +464,6 @@ export type { Transaction, TransactionOptions, TxnState } from './transactions';
export type {
BufferPool,
Callback,
ClientMetadata,
ClientMetadataOptions,
EventEmitterWithState,
HostAddress,
List,
Expand Down
4 changes: 3 additions & 1 deletion src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mong
import type { AuthMechanism } from './cmap/auth/providers';
import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect';
import type { Connection } from './cmap/connection';
import type { ClientMetadata } from './cmap/handshake/client_metadata';
import type { CompressorName } from './cmap/wire_protocol/compression';
import { parseOptions, resolveSRVRecord } from './connection_string';
import { MONGO_CLIENT_EVENTS } from './constants';
Expand All @@ -24,7 +25,7 @@ import { readPreferenceServerSelector } from './sdam/server_selection';
import type { SrvPoller } from './sdam/srv_polling';
import { Topology, TopologyEvents } from './sdam/topology';
import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions';
import { ClientMetadata, HostAddress, MongoDBNamespace, ns, resolveOptions } from './utils';
import { HostAddress, MongoDBNamespace, ns, resolveOptions } from './utils';
import type { W, WriteConcern, WriteConcernSettings } from './write_concern';

/** @public */
Expand Down Expand Up @@ -717,6 +718,7 @@ export interface MongoOptions
proxyPort?: number;
proxyUsername?: string;
proxyPassword?: string;

/** @internal */
connectionType?: typeof Connection;

Expand Down
4 changes: 1 addition & 3 deletions src/sdam/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BSONSerializeOptions, Document } from '../bson';
import type { MongoCredentials } from '../cmap/auth/mongo_credentials';
import type { ConnectionEvents, DestroyOptions } from '../cmap/connection';
import type { CloseOptions, ConnectionPoolEvents } from '../cmap/connection_pool';
import type { ClientMetadata } from '../cmap/handshake/client_metadata';
import { DEFAULT_OPTIONS, FEATURE_FLAGS } from '../connection_string';
import {
CLOSE,
Expand Down Expand Up @@ -37,7 +38,6 @@ import type { ClientSession } from '../sessions';
import type { Transaction } from '../transactions';
import {
Callback,
ClientMetadata,
EventEmitterWithState,
HostAddress,
List,
Expand Down Expand Up @@ -138,15 +138,13 @@ export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
/** The name of the replica set to connect to */
replicaSet?: string;
srvHost?: string;
/** @internal */
srvPoller?: SrvPoller;
/** Indicates that a client should directly connect to a node without attempting to discover its topology type */
directConnection: boolean;
loadBalanced: boolean;
metadata: ClientMetadata;
/** MongoDB server API version */
serverApi?: ServerApi;
/** @internal */
[featureFlag: symbol]: any;
}

Expand Down
Loading