Skip to content

Commit e0b20f1

Browse files
feat(NODE-5159): add FaaS env information to client metadata (#3639)
1 parent 4272c43 commit e0b20f1

16 files changed

+884
-197
lines changed

src/cmap/auth/auth_provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { Document } from '../../bson';
22
import { MongoRuntimeError } from '../../error';
3-
import type { Callback, ClientMetadataOptions } from '../../utils';
3+
import type { Callback } from '../../utils';
44
import type { HandshakeDocument } from '../connect';
55
import type { Connection, ConnectionOptions } from '../connection';
6+
import type { ClientMetadataOptions } from '../handshake/client_metadata';
67
import type { MongoCredentials } from './mongo_credentials';
78

89
export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions;

src/cmap/connect.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
MongoServerError,
1919
needsRetryableWriteLabel
2020
} from '../error';
21-
import { Callback, ClientMetadata, HostAddress, ns } from '../utils';
21+
import { Callback, HostAddress, ns } from '../utils';
2222
import { AuthContext, AuthProvider } from './auth/auth_provider';
2323
import { GSSAPI } from './auth/gssapi';
2424
import { MongoCR } from './auth/mongocr';
@@ -28,6 +28,7 @@ import { AuthMechanism } from './auth/providers';
2828
import { ScramSHA1, ScramSHA256 } from './auth/scram';
2929
import { X509 } from './auth/x509';
3030
import { Connection, ConnectionOptions, CryptoConnection } from './connection';
31+
import type { ClientMetadata } from './handshake/client_metadata';
3132
import {
3233
MAX_SUPPORTED_SERVER_VERSION,
3334
MAX_SUPPORTED_WIRE_VERSION,

src/cmap/connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { applySession, ClientSession, updateSessionFromResponse } from '../sessi
2828
import {
2929
calculateDurationInMs,
3030
Callback,
31-
ClientMetadata,
3231
HostAddress,
3332
maxWireVersion,
3433
MongoDBNamespace,
@@ -44,6 +43,7 @@ import {
4443
} from './command_monitoring_events';
4544
import { BinMsg, Msg, Query, Response, WriteProtocolMessageType } from './commands';
4645
import type { Stream } from './connect';
46+
import type { ClientMetadata } from './handshake/client_metadata';
4747
import { MessageStream, OperationDescription } from './message_stream';
4848
import { StreamDescription, StreamDescriptionOptions } from './stream_description';
4949
import { getReadPreference, isSharded } from './wire_protocol/shared';

src/cmap/handshake/client_metadata.ts

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import * as os from 'os';
2+
import * as process from 'process';
3+
4+
import { BSON, Int32 } from '../../bson';
5+
import { MongoInvalidArgumentError } from '../../error';
6+
import type { MongoOptions } from '../../mongo_client';
7+
8+
// eslint-disable-next-line @typescript-eslint/no-var-requires
9+
const NODE_DRIVER_VERSION = require('../../../package.json').version;
10+
11+
/**
12+
* @public
13+
* @see https://github.com/mongodb/specifications/blob/master/source/mongodb-handshake/handshake.rst#hello-command
14+
*/
15+
export interface ClientMetadata {
16+
driver: {
17+
name: string;
18+
version: string;
19+
};
20+
os: {
21+
type: string;
22+
name?: NodeJS.Platform;
23+
architecture?: string;
24+
version?: string;
25+
};
26+
platform: string;
27+
application?: {
28+
name: string;
29+
};
30+
/** FaaS environment information */
31+
env?: {
32+
name: 'aws.lambda' | 'gcp.func' | 'azure.func' | 'vercel';
33+
timeout_sec?: Int32;
34+
memory_mb?: Int32;
35+
region?: string;
36+
url?: string;
37+
};
38+
}
39+
40+
/** @public */
41+
export interface ClientMetadataOptions {
42+
driverInfo?: {
43+
name?: string;
44+
version?: string;
45+
platform?: string;
46+
};
47+
appName?: string;
48+
}
49+
50+
/** @internal */
51+
export class LimitedSizeDocument {
52+
private document = new Map();
53+
/** BSON overhead: Int32 + Null byte */
54+
private documentSize = 5;
55+
constructor(private maxSize: number) {}
56+
57+
/** Only adds key/value if the bsonByteLength is less than MAX_SIZE */
58+
public ifItFitsItSits(key: string, value: Record<string, any> | string): boolean {
59+
// The BSON byteLength of the new element is the same as serializing it to its own document
60+
// subtracting the document size int32 and the null terminator.
61+
const newElementSize = BSON.serialize(new Map().set(key, value)).byteLength - 5;
62+
63+
if (newElementSize + this.documentSize > this.maxSize) {
64+
return false;
65+
}
66+
67+
this.documentSize += newElementSize;
68+
69+
this.document.set(key, value);
70+
71+
return true;
72+
}
73+
74+
toObject(): ClientMetadata {
75+
return BSON.deserialize(BSON.serialize(this.document), {
76+
promoteLongs: false,
77+
promoteBuffers: false,
78+
promoteValues: false,
79+
useBigInt64: false
80+
}) as ClientMetadata;
81+
}
82+
}
83+
84+
type MakeClientMetadataOptions = Pick<MongoOptions, 'appName' | 'driverInfo'>;
85+
/**
86+
* From the specs:
87+
* Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit:
88+
* 1. Omit fields from `env` except `env.name`.
89+
* 2. Omit fields from `os` except `os.type`.
90+
* 3. Omit the `env` document entirely.
91+
* 4. Truncate `platform`. -- special we do not truncate this field
92+
*/
93+
export function makeClientMetadata(options: MakeClientMetadataOptions): ClientMetadata {
94+
const metadataDocument = new LimitedSizeDocument(512);
95+
96+
const { appName = '' } = options;
97+
// Add app name first, it must be sent
98+
if (appName.length > 0) {
99+
const name =
100+
Buffer.byteLength(appName, 'utf8') <= 128
101+
? options.appName
102+
: Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8');
103+
metadataDocument.ifItFitsItSits('application', { name });
104+
}
105+
106+
const { name = '', version = '', platform = '' } = options.driverInfo;
107+
108+
const driverInfo = {
109+
name: name.length > 0 ? `nodejs|${name}` : 'nodejs',
110+
version: version.length > 0 ? `${NODE_DRIVER_VERSION}|${version}` : NODE_DRIVER_VERSION
111+
};
112+
113+
if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) {
114+
throw new MongoInvalidArgumentError(
115+
'Unable to include driverInfo name and version, metadata cannot exceed 512 bytes'
116+
);
117+
}
118+
119+
const platformInfo =
120+
platform.length > 0
121+
? `Node.js ${process.version}, ${os.endianness()}|${platform}`
122+
: `Node.js ${process.version}, ${os.endianness()}`;
123+
124+
if (!metadataDocument.ifItFitsItSits('platform', platformInfo)) {
125+
throw new MongoInvalidArgumentError(
126+
'Unable to include driverInfo platform, metadata cannot exceed 512 bytes'
127+
);
128+
}
129+
130+
// Note: order matters, os.type is last so it will be removed last if we're at maxSize
131+
const osInfo = new Map()
132+
.set('name', process.platform)
133+
.set('architecture', process.arch)
134+
.set('version', os.release())
135+
.set('type', os.type());
136+
137+
if (!metadataDocument.ifItFitsItSits('os', osInfo)) {
138+
for (const key of osInfo.keys()) {
139+
osInfo.delete(key);
140+
if (osInfo.size === 0) break;
141+
if (metadataDocument.ifItFitsItSits('os', osInfo)) break;
142+
}
143+
}
144+
145+
const faasEnv = getFAASEnv();
146+
if (faasEnv != null) {
147+
if (!metadataDocument.ifItFitsItSits('env', faasEnv)) {
148+
for (const key of faasEnv.keys()) {
149+
faasEnv.delete(key);
150+
if (faasEnv.size === 0) break;
151+
if (metadataDocument.ifItFitsItSits('env', faasEnv)) break;
152+
}
153+
}
154+
}
155+
156+
return metadataDocument.toObject();
157+
}
158+
159+
/**
160+
* Collects FaaS metadata.
161+
* - `name` MUST be the last key in the Map returned.
162+
*/
163+
export function getFAASEnv(): Map<string, string | Int32> | null {
164+
const {
165+
AWS_EXECUTION_ENV = '',
166+
AWS_LAMBDA_RUNTIME_API = '',
167+
FUNCTIONS_WORKER_RUNTIME = '',
168+
K_SERVICE = '',
169+
FUNCTION_NAME = '',
170+
VERCEL = '',
171+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '',
172+
AWS_REGION = '',
173+
FUNCTION_MEMORY_MB = '',
174+
FUNCTION_REGION = '',
175+
FUNCTION_TIMEOUT_SEC = '',
176+
VERCEL_REGION = ''
177+
} = process.env;
178+
179+
const isAWSFaaS = AWS_EXECUTION_ENV.length > 0 || AWS_LAMBDA_RUNTIME_API.length > 0;
180+
const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0;
181+
const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0;
182+
const isVercelFaaS = VERCEL.length > 0;
183+
184+
// Note: order matters, name must always be the last key
185+
const faasEnv = new Map();
186+
187+
// When isVercelFaaS is true so is isAWSFaaS; Vercel inherits the AWS env
188+
if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) {
189+
if (VERCEL_REGION.length > 0) {
190+
faasEnv.set('region', VERCEL_REGION);
191+
}
192+
193+
faasEnv.set('name', 'vercel');
194+
return faasEnv;
195+
}
196+
197+
if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) {
198+
if (AWS_REGION.length > 0) {
199+
faasEnv.set('region', AWS_REGION);
200+
}
201+
202+
if (
203+
AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 &&
204+
Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE)
205+
) {
206+
faasEnv.set('memory_mb', new Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE));
207+
}
208+
209+
faasEnv.set('name', 'aws.lambda');
210+
return faasEnv;
211+
}
212+
213+
if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) {
214+
faasEnv.set('name', 'azure.func');
215+
return faasEnv;
216+
}
217+
218+
if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) {
219+
if (FUNCTION_REGION.length > 0) {
220+
faasEnv.set('region', FUNCTION_REGION);
221+
}
222+
223+
if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) {
224+
faasEnv.set('memory_mb', new Int32(FUNCTION_MEMORY_MB));
225+
}
226+
227+
if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) {
228+
faasEnv.set('timeout_sec', new Int32(FUNCTION_TIMEOUT_SEC));
229+
}
230+
231+
faasEnv.set('name', 'gcp.func');
232+
return faasEnv;
233+
}
234+
235+
return null;
236+
}

src/connection_string.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { URLSearchParams } from 'url';
66
import type { Document } from './bson';
77
import { MongoCredentials } from './cmap/auth/mongo_credentials';
88
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './cmap/auth/providers';
9+
import { makeClientMetadata } from './cmap/handshake/client_metadata';
910
import { Compressor, CompressorName } from './cmap/wire_protocol/compression';
1011
import { Encrypter } from './encrypter';
1112
import {
@@ -34,7 +35,6 @@ import {
3435
emitWarningOnce,
3536
HostAddress,
3637
isRecord,
37-
makeClientMetadata,
3838
matchesParentDomain,
3939
parseInteger,
4040
setDifference

src/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export type {
248248
WaitQueueMember,
249249
WithConnectionCallback
250250
} from './cmap/connection_pool';
251+
export type { ClientMetadata, ClientMetadataOptions } from './cmap/handshake/client_metadata';
251252
export type {
252253
MessageStream,
253254
MessageStreamOptions,
@@ -480,8 +481,6 @@ export type { Transaction, TransactionOptions, TxnState } from './transactions';
480481
export type {
481482
BufferPool,
482483
Callback,
483-
ClientMetadata,
484-
ClientMetadataOptions,
485484
EventEmitterWithState,
486485
HostAddress,
487486
List,

src/mongo_client.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mong
88
import type { AuthMechanism } from './cmap/auth/providers';
99
import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect';
1010
import type { Connection } from './cmap/connection';
11+
import type { ClientMetadata } from './cmap/handshake/client_metadata';
1112
import type { CompressorName } from './cmap/wire_protocol/compression';
1213
import { parseOptions, resolveSRVRecord } from './connection_string';
1314
import { MONGO_CLIENT_EVENTS } from './constants';
@@ -27,7 +28,6 @@ import { Topology, TopologyEvents } from './sdam/topology';
2728
import { ClientSession, ClientSessionOptions, ServerSessionPool } from './sessions';
2829
import {
2930
Callback,
30-
ClientMetadata,
3131
HostAddress,
3232
maybeCallback,
3333
MongoDBNamespace,
@@ -389,6 +389,7 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
389389
};
390390
}
391391

392+
/** @see MongoOptions */
392393
get options(): Readonly<MongoOptions> {
393394
return Object.freeze({ ...this[kOptions] });
394395
}
@@ -469,7 +470,7 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
469470
topology.once(Topology.OPEN, () => this.emit('open', this));
470471

471472
for (const event of MONGO_CLIENT_EVENTS) {
472-
topology.on(event, (...args: any[]) => this.emit(event, ...(args as any)));
473+
topology.on(event, (...args: any[]): unknown => this.emit(event, ...(args as any)));
473474
}
474475

475476
const topologyConnect = async () => {
@@ -728,7 +729,22 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> {
728729
}
729730

730731
/**
731-
* Mongo Client Options
732+
* Parsed Mongo Client Options.
733+
*
734+
* User supplied options are documented by `MongoClientOptions`.
735+
*
736+
* **NOTE:** The client's options parsing is subject to change to support new features.
737+
* This type is provided to aid with inspection of options after parsing, it should not be relied upon programmatically.
738+
*
739+
* Options are sourced from:
740+
* - connection string
741+
* - options object passed to the MongoClient constructor
742+
* - file system (ex. tls settings)
743+
* - environment variables
744+
* - DNS SRV records and TXT records
745+
*
746+
* Not all options may be present after client construction as some are obtained from asynchronous operations.
747+
*
732748
* @public
733749
*/
734750
export interface MongoOptions
@@ -787,6 +803,7 @@ export interface MongoOptions
787803
proxyPort?: number;
788804
proxyUsername?: string;
789805
proxyPassword?: string;
806+
790807
/** @internal */
791808
connectionType?: typeof Connection;
792809

src/sdam/topology.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { deserialize, serialize } from '../bson';
66
import type { MongoCredentials } from '../cmap/auth/mongo_credentials';
77
import type { ConnectionEvents, DestroyOptions } from '../cmap/connection';
88
import type { CloseOptions, ConnectionPoolEvents } from '../cmap/connection_pool';
9+
import type { ClientMetadata } from '../cmap/handshake/client_metadata';
910
import { DEFAULT_OPTIONS, FEATURE_FLAGS } from '../connection_string';
1011
import {
1112
CLOSE,
@@ -38,7 +39,6 @@ import type { ClientSession } from '../sessions';
3839
import type { Transaction } from '../transactions';
3940
import {
4041
Callback,
41-
ClientMetadata,
4242
emitWarning,
4343
EventEmitterWithState,
4444
HostAddress,

0 commit comments

Comments
 (0)