Skip to content

Commit e0b20f1

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

File tree

16 files changed

+884
-197
lines changed

16 files changed

+884
-197
lines changed

src/cmap/auth/auth_provider.ts

Lines changed: 2 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 236 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 2 deletions
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

Lines changed: 20 additions & 3 deletions
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

Lines changed: 1 addition & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy