client_metadata.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.LimitedSizeDocument = void 0;
  4. exports.isDriverInfoEqual = isDriverInfoEqual;
  5. exports.makeClientMetadata = makeClientMetadata;
  6. exports.addContainerMetadata = addContainerMetadata;
  7. exports.getFAASEnv = getFAASEnv;
  8. const os = require("os");
  9. const process = require("process");
  10. const bson_1 = require("../../bson");
  11. const error_1 = require("../../error");
  12. const utils_1 = require("../../utils");
  13. // eslint-disable-next-line @typescript-eslint/no-require-imports
  14. const NODE_DRIVER_VERSION = require('../../../package.json').version;
  15. /** @internal */
  16. function isDriverInfoEqual(info1, info2) {
  17. /** for equality comparison, we consider "" as unset */
  18. const nonEmptyCmp = (s1, s2) => {
  19. s1 ||= undefined;
  20. s2 ||= undefined;
  21. return s1 === s2;
  22. };
  23. return (nonEmptyCmp(info1.name, info2.name) &&
  24. nonEmptyCmp(info1.platform, info2.platform) &&
  25. nonEmptyCmp(info1.version, info2.version));
  26. }
  27. /** @internal */
  28. class LimitedSizeDocument {
  29. constructor(maxSize) {
  30. this.document = new Map();
  31. /** BSON overhead: Int32 + Null byte */
  32. this.documentSize = 5;
  33. this.maxSize = maxSize;
  34. }
  35. /** Only adds key/value if the bsonByteLength is less than MAX_SIZE */
  36. ifItFitsItSits(key, value) {
  37. // The BSON byteLength of the new element is the same as serializing it to its own document
  38. // subtracting the document size int32 and the null terminator.
  39. const newElementSize = bson_1.BSON.serialize(new Map().set(key, value)).byteLength - 5;
  40. if (newElementSize + this.documentSize > this.maxSize) {
  41. return false;
  42. }
  43. this.documentSize += newElementSize;
  44. this.document.set(key, value);
  45. return true;
  46. }
  47. toObject() {
  48. return bson_1.BSON.deserialize(bson_1.BSON.serialize(this.document), {
  49. promoteLongs: false,
  50. promoteBuffers: false,
  51. promoteValues: false,
  52. useBigInt64: false
  53. });
  54. }
  55. }
  56. exports.LimitedSizeDocument = LimitedSizeDocument;
  57. /**
  58. * From the specs:
  59. * Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit:
  60. * 1. Omit fields from `env` except `env.name`.
  61. * 2. Omit fields from `os` except `os.type`.
  62. * 3. Omit the `env` document entirely.
  63. * 4. Truncate `platform`. -- special we do not truncate this field
  64. */
  65. function makeClientMetadata(driverInfoList, { appName = '' }) {
  66. const metadataDocument = new LimitedSizeDocument(512);
  67. // Add app name first, it must be sent
  68. if (appName.length > 0) {
  69. const name = Buffer.byteLength(appName, 'utf8') <= 128
  70. ? appName
  71. : Buffer.from(appName, 'utf8').subarray(0, 128).toString('utf8');
  72. metadataDocument.ifItFitsItSits('application', { name });
  73. }
  74. const driverInfo = {
  75. name: 'nodejs',
  76. version: NODE_DRIVER_VERSION
  77. };
  78. // This is where we handle additional driver info added after client construction.
  79. for (const { name: n = '', version: v = '' } of driverInfoList) {
  80. if (n.length > 0) {
  81. driverInfo.name = `${driverInfo.name}|${n}`;
  82. }
  83. if (v.length > 0) {
  84. driverInfo.version = `${driverInfo.version}|${v}`;
  85. }
  86. }
  87. if (!metadataDocument.ifItFitsItSits('driver', driverInfo)) {
  88. throw new error_1.MongoInvalidArgumentError('Unable to include driverInfo name and version, metadata cannot exceed 512 bytes');
  89. }
  90. let runtimeInfo = getRuntimeInfo();
  91. // This is where we handle additional driver info added after client construction.
  92. for (const { platform = '' } of driverInfoList) {
  93. if (platform.length > 0) {
  94. runtimeInfo = `${runtimeInfo}|${platform}`;
  95. }
  96. }
  97. if (!metadataDocument.ifItFitsItSits('platform', runtimeInfo)) {
  98. throw new error_1.MongoInvalidArgumentError('Unable to include driverInfo platform, metadata cannot exceed 512 bytes');
  99. }
  100. // Note: order matters, os.type is last so it will be removed last if we're at maxSize
  101. const osInfo = new Map()
  102. .set('name', process.platform)
  103. .set('architecture', process.arch)
  104. .set('version', os.release())
  105. .set('type', os.type());
  106. if (!metadataDocument.ifItFitsItSits('os', osInfo)) {
  107. for (const key of osInfo.keys()) {
  108. osInfo.delete(key);
  109. if (osInfo.size === 0)
  110. break;
  111. if (metadataDocument.ifItFitsItSits('os', osInfo))
  112. break;
  113. }
  114. }
  115. const faasEnv = getFAASEnv();
  116. if (faasEnv != null) {
  117. if (!metadataDocument.ifItFitsItSits('env', faasEnv)) {
  118. for (const key of faasEnv.keys()) {
  119. faasEnv.delete(key);
  120. if (faasEnv.size === 0)
  121. break;
  122. if (metadataDocument.ifItFitsItSits('env', faasEnv))
  123. break;
  124. }
  125. }
  126. }
  127. return metadataDocument.toObject();
  128. }
  129. let dockerPromise;
  130. /** @internal */
  131. async function getContainerMetadata() {
  132. const containerMetadata = {};
  133. dockerPromise ??= (0, utils_1.fileIsAccessible)('/.dockerenv');
  134. const isDocker = await dockerPromise;
  135. const { KUBERNETES_SERVICE_HOST = '' } = process.env;
  136. const isKubernetes = KUBERNETES_SERVICE_HOST.length > 0 ? true : false;
  137. if (isDocker)
  138. containerMetadata.runtime = 'docker';
  139. if (isKubernetes)
  140. containerMetadata.orchestrator = 'kubernetes';
  141. return containerMetadata;
  142. }
  143. /**
  144. * @internal
  145. * Re-add each metadata value.
  146. * Attempt to add new env container metadata, but keep old data if it does not fit.
  147. */
  148. async function addContainerMetadata(originalMetadata) {
  149. const containerMetadata = await getContainerMetadata();
  150. if (Object.keys(containerMetadata).length === 0)
  151. return originalMetadata;
  152. const extendedMetadata = new LimitedSizeDocument(512);
  153. const extendedEnvMetadata = { ...originalMetadata?.env, container: containerMetadata };
  154. for (const [key, val] of Object.entries(originalMetadata)) {
  155. if (key !== 'env') {
  156. extendedMetadata.ifItFitsItSits(key, val);
  157. }
  158. else {
  159. if (!extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata)) {
  160. // add in old data if newer / extended metadata does not fit
  161. extendedMetadata.ifItFitsItSits('env', val);
  162. }
  163. }
  164. }
  165. if (!('env' in originalMetadata)) {
  166. extendedMetadata.ifItFitsItSits('env', extendedEnvMetadata);
  167. }
  168. return extendedMetadata.toObject();
  169. }
  170. /**
  171. * Collects FaaS metadata.
  172. * - `name` MUST be the last key in the Map returned.
  173. */
  174. function getFAASEnv() {
  175. 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;
  176. const isAWSFaaS = AWS_EXECUTION_ENV.startsWith('AWS_Lambda_') || AWS_LAMBDA_RUNTIME_API.length > 0;
  177. const isAzureFaaS = FUNCTIONS_WORKER_RUNTIME.length > 0;
  178. const isGCPFaaS = K_SERVICE.length > 0 || FUNCTION_NAME.length > 0;
  179. const isVercelFaaS = VERCEL.length > 0;
  180. // Note: order matters, name must always be the last key
  181. const faasEnv = new Map();
  182. // When isVercelFaaS is true so is isAWSFaaS; Vercel inherits the AWS env
  183. if (isVercelFaaS && !(isAzureFaaS || isGCPFaaS)) {
  184. if (VERCEL_REGION.length > 0) {
  185. faasEnv.set('region', VERCEL_REGION);
  186. }
  187. faasEnv.set('name', 'vercel');
  188. return faasEnv;
  189. }
  190. if (isAWSFaaS && !(isAzureFaaS || isGCPFaaS || isVercelFaaS)) {
  191. if (AWS_REGION.length > 0) {
  192. faasEnv.set('region', AWS_REGION);
  193. }
  194. if (AWS_LAMBDA_FUNCTION_MEMORY_SIZE.length > 0 &&
  195. Number.isInteger(+AWS_LAMBDA_FUNCTION_MEMORY_SIZE)) {
  196. faasEnv.set('memory_mb', new bson_1.Int32(AWS_LAMBDA_FUNCTION_MEMORY_SIZE));
  197. }
  198. faasEnv.set('name', 'aws.lambda');
  199. return faasEnv;
  200. }
  201. if (isAzureFaaS && !(isGCPFaaS || isAWSFaaS || isVercelFaaS)) {
  202. faasEnv.set('name', 'azure.func');
  203. return faasEnv;
  204. }
  205. if (isGCPFaaS && !(isAzureFaaS || isAWSFaaS || isVercelFaaS)) {
  206. if (FUNCTION_REGION.length > 0) {
  207. faasEnv.set('region', FUNCTION_REGION);
  208. }
  209. if (FUNCTION_MEMORY_MB.length > 0 && Number.isInteger(+FUNCTION_MEMORY_MB)) {
  210. faasEnv.set('memory_mb', new bson_1.Int32(FUNCTION_MEMORY_MB));
  211. }
  212. if (FUNCTION_TIMEOUT_SEC.length > 0 && Number.isInteger(+FUNCTION_TIMEOUT_SEC)) {
  213. faasEnv.set('timeout_sec', new bson_1.Int32(FUNCTION_TIMEOUT_SEC));
  214. }
  215. faasEnv.set('name', 'gcp.func');
  216. return faasEnv;
  217. }
  218. return null;
  219. }
  220. /**
  221. * @internal
  222. * Get current JavaScript runtime platform
  223. *
  224. * NOTE: The version information fetching is intentionally written defensively
  225. * to avoid having a released driver version that becomes incompatible
  226. * with a future change to these global objects.
  227. */
  228. function getRuntimeInfo() {
  229. if ('Deno' in globalThis) {
  230. const version = typeof Deno?.version?.deno === 'string' ? Deno?.version?.deno : '0.0.0-unknown';
  231. return `Deno v${version}, ${os.endianness()}`;
  232. }
  233. if ('Bun' in globalThis) {
  234. const version = typeof Bun?.version === 'string' ? Bun?.version : '0.0.0-unknown';
  235. return `Bun v${version}, ${os.endianness()}`;
  236. }
  237. return `Node.js ${process.version}, ${os.endianness()}`;
  238. }
  239. //# sourceMappingURL=client_metadata.js.map