mongo_logger.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.MongoLogger = exports.stringifyWithMaxLen = exports.createStdioLogger = exports.MongoLoggableComponent = exports.SEVERITY_LEVEL_MAP = exports.DEFAULT_MAX_DOCUMENT_LENGTH = exports.SeverityLevel = void 0;
  4. const bson_1 = require("bson");
  5. const util_1 = require("util");
  6. const constants_1 = require("./constants");
  7. const utils_1 = require("./utils");
  8. /** @internal */
  9. exports.SeverityLevel = Object.freeze({
  10. EMERGENCY: 'emergency',
  11. ALERT: 'alert',
  12. CRITICAL: 'critical',
  13. ERROR: 'error',
  14. WARNING: 'warn',
  15. NOTICE: 'notice',
  16. INFORMATIONAL: 'info',
  17. DEBUG: 'debug',
  18. TRACE: 'trace',
  19. OFF: 'off'
  20. });
  21. /** @internal */
  22. exports.DEFAULT_MAX_DOCUMENT_LENGTH = 1000;
  23. /** @internal */
  24. class SeverityLevelMap extends Map {
  25. constructor(entries) {
  26. const newEntries = [];
  27. for (const [level, value] of entries) {
  28. newEntries.push([value, level]);
  29. }
  30. newEntries.push(...entries);
  31. super(newEntries);
  32. }
  33. getNumericSeverityLevel(severity) {
  34. return this.get(severity);
  35. }
  36. getSeverityLevelName(level) {
  37. return this.get(level);
  38. }
  39. }
  40. /** @internal */
  41. exports.SEVERITY_LEVEL_MAP = new SeverityLevelMap([
  42. [exports.SeverityLevel.OFF, -Infinity],
  43. [exports.SeverityLevel.EMERGENCY, 0],
  44. [exports.SeverityLevel.ALERT, 1],
  45. [exports.SeverityLevel.CRITICAL, 2],
  46. [exports.SeverityLevel.ERROR, 3],
  47. [exports.SeverityLevel.WARNING, 4],
  48. [exports.SeverityLevel.NOTICE, 5],
  49. [exports.SeverityLevel.INFORMATIONAL, 6],
  50. [exports.SeverityLevel.DEBUG, 7],
  51. [exports.SeverityLevel.TRACE, 8]
  52. ]);
  53. /** @internal */
  54. exports.MongoLoggableComponent = Object.freeze({
  55. COMMAND: 'command',
  56. TOPOLOGY: 'topology',
  57. SERVER_SELECTION: 'serverSelection',
  58. CONNECTION: 'connection'
  59. });
  60. /**
  61. * Parses a string as one of SeverityLevel
  62. *
  63. * @param s - the value to be parsed
  64. * @returns one of SeverityLevel if value can be parsed as such, otherwise null
  65. */
  66. function parseSeverityFromString(s) {
  67. const validSeverities = Object.values(exports.SeverityLevel);
  68. const lowerSeverity = s?.toLowerCase();
  69. if (lowerSeverity != null && validSeverities.includes(lowerSeverity)) {
  70. return lowerSeverity;
  71. }
  72. return null;
  73. }
  74. /** @internal */
  75. function createStdioLogger(stream) {
  76. return {
  77. write: (log) => {
  78. stream.write((0, util_1.inspect)(log, { compact: true, breakLength: Infinity }), 'utf-8');
  79. return;
  80. }
  81. };
  82. }
  83. exports.createStdioLogger = createStdioLogger;
  84. /**
  85. * resolves the MONGODB_LOG_PATH and mongodbLogPath options from the environment and the
  86. * mongo client options respectively. The mongodbLogPath can be either 'stdout', 'stderr', a NodeJS
  87. * Writable or an object which has a `write` method with the signature:
  88. * ```ts
  89. * write(log: Log): void
  90. * ```
  91. *
  92. * @returns the MongoDBLogWritable object to write logs to
  93. */
  94. function resolveLogPath({ MONGODB_LOG_PATH }, { mongodbLogPath }) {
  95. if (typeof mongodbLogPath === 'string' && /^stderr$/i.test(mongodbLogPath)) {
  96. return createStdioLogger(process.stderr);
  97. }
  98. if (typeof mongodbLogPath === 'string' && /^stdout$/i.test(mongodbLogPath)) {
  99. return createStdioLogger(process.stdout);
  100. }
  101. if (typeof mongodbLogPath === 'object' && typeof mongodbLogPath?.write === 'function') {
  102. return mongodbLogPath;
  103. }
  104. if (MONGODB_LOG_PATH && /^stderr$/i.test(MONGODB_LOG_PATH)) {
  105. return createStdioLogger(process.stderr);
  106. }
  107. if (MONGODB_LOG_PATH && /^stdout$/i.test(MONGODB_LOG_PATH)) {
  108. return createStdioLogger(process.stdout);
  109. }
  110. return createStdioLogger(process.stderr);
  111. }
  112. function compareSeverity(s0, s1) {
  113. const s0Num = exports.SEVERITY_LEVEL_MAP.getNumericSeverityLevel(s0);
  114. const s1Num = exports.SEVERITY_LEVEL_MAP.getNumericSeverityLevel(s1);
  115. return s0Num < s1Num ? -1 : s0Num > s1Num ? 1 : 0;
  116. }
  117. /** @internal */
  118. function stringifyWithMaxLen(value, maxDocumentLength) {
  119. const ejson = bson_1.EJSON.stringify(value);
  120. return maxDocumentLength !== 0 && ejson.length > maxDocumentLength
  121. ? `${ejson.slice(0, maxDocumentLength)}...`
  122. : ejson;
  123. }
  124. exports.stringifyWithMaxLen = stringifyWithMaxLen;
  125. function isLogConvertible(obj) {
  126. const objAsLogConvertible = obj;
  127. // eslint-disable-next-line no-restricted-syntax
  128. return objAsLogConvertible.toLog !== undefined && typeof objAsLogConvertible.toLog === 'function';
  129. }
  130. function attachCommandFields(log, commandEvent) {
  131. log.commandName = commandEvent.commandName;
  132. log.requestId = commandEvent.requestId;
  133. log.driverConnectionId = commandEvent?.connectionId;
  134. const { host, port } = utils_1.HostAddress.fromString(commandEvent.address).toHostPort();
  135. log.serverHost = host;
  136. log.serverPort = port;
  137. if (commandEvent?.serviceId) {
  138. log.serviceId = commandEvent.serviceId.toHexString();
  139. }
  140. return log;
  141. }
  142. function attachConnectionFields(log, connectionPoolEvent) {
  143. const { host, port } = utils_1.HostAddress.fromString(connectionPoolEvent.address).toHostPort();
  144. log.serverHost = host;
  145. log.serverPort = port;
  146. return log;
  147. }
  148. function defaultLogTransform(logObject, maxDocumentLength = exports.DEFAULT_MAX_DOCUMENT_LENGTH) {
  149. let log = Object.create(null);
  150. switch (logObject.name) {
  151. case constants_1.COMMAND_STARTED:
  152. log = attachCommandFields(log, logObject);
  153. log.message = 'Command started';
  154. log.command = stringifyWithMaxLen(logObject.command, maxDocumentLength);
  155. log.databaseName = logObject.databaseName;
  156. return log;
  157. case constants_1.COMMAND_SUCCEEDED:
  158. log = attachCommandFields(log, logObject);
  159. log.message = 'Command succeeded';
  160. log.durationMS = logObject.duration;
  161. log.reply = stringifyWithMaxLen(logObject.reply, maxDocumentLength);
  162. return log;
  163. case constants_1.COMMAND_FAILED:
  164. log = attachCommandFields(log, logObject);
  165. log.message = 'Command failed';
  166. log.durationMS = logObject.duration;
  167. log.failure = logObject.failure;
  168. return log;
  169. case constants_1.CONNECTION_POOL_CREATED:
  170. log = attachConnectionFields(log, logObject);
  171. log.message = 'Connection pool created';
  172. if (logObject.options) {
  173. const { maxIdleTimeMS, minPoolSize, maxPoolSize, maxConnecting, waitQueueTimeoutMS } = logObject.options;
  174. log = {
  175. ...log,
  176. maxIdleTimeMS,
  177. minPoolSize,
  178. maxPoolSize,
  179. maxConnecting,
  180. waitQueueTimeoutMS
  181. };
  182. }
  183. return log;
  184. case constants_1.CONNECTION_POOL_READY:
  185. log = attachConnectionFields(log, logObject);
  186. log.message = 'Connection pool ready';
  187. return log;
  188. case constants_1.CONNECTION_POOL_CLEARED:
  189. log = attachConnectionFields(log, logObject);
  190. log.message = 'Connection pool cleared';
  191. if (logObject.serviceId?._bsontype === 'ObjectId') {
  192. log.serviceId = logObject.serviceId.toHexString();
  193. }
  194. return log;
  195. case constants_1.CONNECTION_POOL_CLOSED:
  196. log = attachConnectionFields(log, logObject);
  197. log.message = 'Connection pool closed';
  198. return log;
  199. case constants_1.CONNECTION_CREATED:
  200. log = attachConnectionFields(log, logObject);
  201. log.message = 'Connection created';
  202. log.driverConnectionId = logObject.connectionId;
  203. return log;
  204. case constants_1.CONNECTION_READY:
  205. log = attachConnectionFields(log, logObject);
  206. log.message = 'Connection ready';
  207. log.driverConnectionId = logObject.connectionId;
  208. return log;
  209. case constants_1.CONNECTION_CLOSED:
  210. log = attachConnectionFields(log, logObject);
  211. log.message = 'Connection closed';
  212. log.driverConnectionId = logObject.connectionId;
  213. switch (logObject.reason) {
  214. case 'stale':
  215. log.reason = 'Connection became stale because the pool was cleared';
  216. break;
  217. case 'idle':
  218. log.reason =
  219. 'Connection has been available but unused for longer than the configured max idle time';
  220. break;
  221. case 'error':
  222. log.reason = 'An error occurred while using the connection';
  223. if (logObject.error) {
  224. log.error = logObject.error;
  225. }
  226. break;
  227. case 'poolClosed':
  228. log.reason = 'Connection pool was closed';
  229. break;
  230. default:
  231. log.reason = `Unknown close reason: ${logObject.reason}`;
  232. }
  233. return log;
  234. case constants_1.CONNECTION_CHECK_OUT_STARTED:
  235. log = attachConnectionFields(log, logObject);
  236. log.message = 'Connection checkout started';
  237. return log;
  238. case constants_1.CONNECTION_CHECK_OUT_FAILED:
  239. log = attachConnectionFields(log, logObject);
  240. log.message = 'Connection checkout failed';
  241. switch (logObject.reason) {
  242. case 'poolClosed':
  243. log.reason = 'Connection pool was closed';
  244. break;
  245. case 'timeout':
  246. log.reason = 'Wait queue timeout elapsed without a connection becoming available';
  247. break;
  248. case 'connectionError':
  249. log.reason = 'An error occurred while trying to establish a new connection';
  250. if (logObject.error) {
  251. log.error = logObject.error;
  252. }
  253. break;
  254. default:
  255. log.reason = `Unknown close reason: ${logObject.reason}`;
  256. }
  257. return log;
  258. case constants_1.CONNECTION_CHECKED_OUT:
  259. log = attachConnectionFields(log, logObject);
  260. log.message = 'Connection checked out';
  261. log.driverConnectionId = logObject.connectionId;
  262. return log;
  263. case constants_1.CONNECTION_CHECKED_IN:
  264. log = attachConnectionFields(log, logObject);
  265. log.message = 'Connection checked in';
  266. log.driverConnectionId = logObject.connectionId;
  267. return log;
  268. default:
  269. for (const [key, value] of Object.entries(logObject)) {
  270. if (value != null)
  271. log[key] = value;
  272. }
  273. }
  274. return log;
  275. }
  276. /** @internal */
  277. class MongoLogger {
  278. constructor(options) {
  279. /**
  280. * This method should be used when logging errors that do not have a public driver API for
  281. * reporting errors.
  282. */
  283. this.error = this.log.bind(this, 'error');
  284. /**
  285. * This method should be used to log situations where undesirable application behaviour might
  286. * occur. For example, failing to end sessions on `MongoClient.close`.
  287. */
  288. this.warn = this.log.bind(this, 'warn');
  289. /**
  290. * This method should be used to report high-level information about normal driver behaviour.
  291. * For example, the creation of a `MongoClient`.
  292. */
  293. this.info = this.log.bind(this, 'info');
  294. /**
  295. * This method should be used to report information that would be helpful when debugging an
  296. * application. For example, a command starting, succeeding or failing.
  297. */
  298. this.debug = this.log.bind(this, 'debug');
  299. /**
  300. * This method should be used to report fine-grained details related to logic flow. For example,
  301. * entering and exiting a function body.
  302. */
  303. this.trace = this.log.bind(this, 'trace');
  304. this.componentSeverities = options.componentSeverities;
  305. this.maxDocumentLength = options.maxDocumentLength;
  306. this.logDestination = options.logDestination;
  307. }
  308. log(severity, component, message) {
  309. if (compareSeverity(severity, this.componentSeverities[component]) > 0)
  310. return;
  311. let logMessage = { t: new Date(), c: component, s: severity };
  312. if (typeof message === 'string') {
  313. logMessage.message = message;
  314. }
  315. else if (typeof message === 'object') {
  316. if (isLogConvertible(message)) {
  317. logMessage = { ...logMessage, ...message.toLog() };
  318. }
  319. else {
  320. logMessage = { ...logMessage, ...defaultLogTransform(message, this.maxDocumentLength) };
  321. }
  322. }
  323. this.logDestination.write(logMessage);
  324. }
  325. /**
  326. * Merges options set through environment variables and the MongoClient, preferring environment
  327. * variables when both are set, and substituting defaults for values not set. Options set in
  328. * constructor take precedence over both environment variables and MongoClient options.
  329. *
  330. * @remarks
  331. * When parsing component severity levels, invalid values are treated as unset and replaced with
  332. * the default severity.
  333. *
  334. * @param envOptions - options set for the logger from the environment
  335. * @param clientOptions - options set for the logger in the MongoClient options
  336. * @returns a MongoLoggerOptions object to be used when instantiating a new MongoLogger
  337. */
  338. static resolveOptions(envOptions, clientOptions) {
  339. // client options take precedence over env options
  340. const combinedOptions = {
  341. ...envOptions,
  342. ...clientOptions,
  343. mongodbLogPath: resolveLogPath(envOptions, clientOptions)
  344. };
  345. const defaultSeverity = parseSeverityFromString(combinedOptions.MONGODB_LOG_ALL) ?? exports.SeverityLevel.OFF;
  346. return {
  347. componentSeverities: {
  348. command: parseSeverityFromString(combinedOptions.MONGODB_LOG_COMMAND) ?? defaultSeverity,
  349. topology: parseSeverityFromString(combinedOptions.MONGODB_LOG_TOPOLOGY) ?? defaultSeverity,
  350. serverSelection: parseSeverityFromString(combinedOptions.MONGODB_LOG_SERVER_SELECTION) ?? defaultSeverity,
  351. connection: parseSeverityFromString(combinedOptions.MONGODB_LOG_CONNECTION) ?? defaultSeverity,
  352. default: defaultSeverity
  353. },
  354. maxDocumentLength: (0, utils_1.parseUnsignedInteger)(combinedOptions.MONGODB_LOG_MAX_DOCUMENT_LENGTH) ?? 1000,
  355. logDestination: combinedOptions.mongodbLogPath
  356. };
  357. }
  358. }
  359. exports.MongoLogger = MongoLogger;
  360. //# sourceMappingURL=mongo_logger.js.map