apm.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. 'use strict';
  2. const Msg = require('../connection/msg').Msg;
  3. const KillCursor = require('../connection/commands').KillCursor;
  4. const GetMore = require('../connection/commands').GetMore;
  5. const calculateDurationInMs = require('../utils').calculateDurationInMs;
  6. /** Commands that we want to redact because of the sensitive nature of their contents */
  7. const SENSITIVE_COMMANDS = new Set([
  8. 'authenticate',
  9. 'saslStart',
  10. 'saslContinue',
  11. 'getnonce',
  12. 'createUser',
  13. 'updateUser',
  14. 'copydbgetnonce',
  15. 'copydbsaslstart',
  16. 'copydb'
  17. ]);
  18. // helper methods
  19. const extractCommandName = commandDoc => Object.keys(commandDoc)[0];
  20. const namespace = command => command.ns;
  21. const databaseName = command => command.ns.split('.')[0];
  22. const collectionName = command => command.ns.split('.')[1];
  23. const generateConnectionId = pool => `${pool.options.host}:${pool.options.port}`;
  24. const maybeRedact = (commandName, result) => (SENSITIVE_COMMANDS.has(commandName) ? {} : result);
  25. const LEGACY_FIND_QUERY_MAP = {
  26. $query: 'filter',
  27. $orderby: 'sort',
  28. $hint: 'hint',
  29. $comment: 'comment',
  30. $maxScan: 'maxScan',
  31. $max: 'max',
  32. $min: 'min',
  33. $returnKey: 'returnKey',
  34. $showDiskLoc: 'showRecordId',
  35. $maxTimeMS: 'maxTimeMS',
  36. $snapshot: 'snapshot'
  37. };
  38. const LEGACY_FIND_OPTIONS_MAP = {
  39. numberToSkip: 'skip',
  40. numberToReturn: 'batchSize',
  41. returnFieldsSelector: 'projection'
  42. };
  43. const OP_QUERY_KEYS = [
  44. 'tailable',
  45. 'oplogReplay',
  46. 'noCursorTimeout',
  47. 'awaitData',
  48. 'partial',
  49. 'exhaust'
  50. ];
  51. /**
  52. * Extract the actual command from the query, possibly upconverting if it's a legacy
  53. * format
  54. *
  55. * @param {Object} command the command
  56. */
  57. const extractCommand = command => {
  58. if (command instanceof GetMore) {
  59. return {
  60. getMore: command.cursorId,
  61. collection: collectionName(command),
  62. batchSize: command.numberToReturn
  63. };
  64. }
  65. if (command instanceof KillCursor) {
  66. return {
  67. killCursors: collectionName(command),
  68. cursors: command.cursorIds
  69. };
  70. }
  71. if (command instanceof Msg) {
  72. return command.command;
  73. }
  74. if (command.query && command.query.$query) {
  75. let result;
  76. if (command.ns === 'admin.$cmd') {
  77. // upconvert legacy command
  78. result = Object.assign({}, command.query.$query);
  79. } else {
  80. // upconvert legacy find command
  81. result = { find: collectionName(command) };
  82. Object.keys(LEGACY_FIND_QUERY_MAP).forEach(key => {
  83. if (typeof command.query[key] !== 'undefined')
  84. result[LEGACY_FIND_QUERY_MAP[key]] = command.query[key];
  85. });
  86. }
  87. Object.keys(LEGACY_FIND_OPTIONS_MAP).forEach(key => {
  88. if (typeof command[key] !== 'undefined') result[LEGACY_FIND_OPTIONS_MAP[key]] = command[key];
  89. });
  90. OP_QUERY_KEYS.forEach(key => {
  91. if (command[key]) result[key] = command[key];
  92. });
  93. if (typeof command.pre32Limit !== 'undefined') {
  94. result.limit = command.pre32Limit;
  95. }
  96. if (command.query.$explain) {
  97. return { explain: result };
  98. }
  99. return result;
  100. }
  101. return command.query ? command.query : command;
  102. };
  103. const extractReply = (command, reply) => {
  104. if (command instanceof GetMore) {
  105. return {
  106. ok: 1,
  107. cursor: {
  108. id: reply.message.cursorId,
  109. ns: namespace(command),
  110. nextBatch: reply.message.documents
  111. }
  112. };
  113. }
  114. if (command instanceof KillCursor) {
  115. return {
  116. ok: 1,
  117. cursorsUnknown: command.cursorIds
  118. };
  119. }
  120. // is this a legacy find command?
  121. if (command.query && typeof command.query.$query !== 'undefined') {
  122. return {
  123. ok: 1,
  124. cursor: {
  125. id: reply.message.cursorId,
  126. ns: namespace(command),
  127. firstBatch: reply.message.documents
  128. }
  129. };
  130. }
  131. // in the event of a `noResponse` command, just return
  132. if (reply === null) return reply;
  133. return reply.result;
  134. };
  135. /** An event indicating the start of a given command */
  136. class CommandStartedEvent {
  137. /**
  138. * Create a started event
  139. *
  140. * @param {Pool} pool the pool that originated the command
  141. * @param {Object} command the command
  142. */
  143. constructor(pool, command) {
  144. const cmd = extractCommand(command);
  145. const commandName = extractCommandName(cmd);
  146. // NOTE: remove in major revision, this is not spec behavior
  147. if (SENSITIVE_COMMANDS.has(commandName)) {
  148. this.commandObj = {};
  149. this.commandObj[commandName] = true;
  150. }
  151. Object.assign(this, {
  152. command: cmd,
  153. databaseName: databaseName(command),
  154. commandName,
  155. requestId: command.requestId,
  156. connectionId: generateConnectionId(pool)
  157. });
  158. }
  159. }
  160. /** An event indicating the success of a given command */
  161. class CommandSucceededEvent {
  162. /**
  163. * Create a succeeded event
  164. *
  165. * @param {Pool} pool the pool that originated the command
  166. * @param {Object} command the command
  167. * @param {Object} reply the reply for this command from the server
  168. * @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
  169. */
  170. constructor(pool, command, reply, started) {
  171. const cmd = extractCommand(command);
  172. const commandName = extractCommandName(cmd);
  173. Object.assign(this, {
  174. duration: calculateDurationInMs(started),
  175. commandName,
  176. reply: maybeRedact(commandName, extractReply(command, reply)),
  177. requestId: command.requestId,
  178. connectionId: generateConnectionId(pool)
  179. });
  180. }
  181. }
  182. /** An event indicating the failure of a given command */
  183. class CommandFailedEvent {
  184. /**
  185. * Create a failure event
  186. *
  187. * @param {Pool} pool the pool that originated the command
  188. * @param {Object} command the command
  189. * @param {MongoError|Object} error the generated error or a server error response
  190. * @param {Array} started a high resolution tuple timestamp of when the command was first sent, to calculate duration
  191. */
  192. constructor(pool, command, error, started) {
  193. const cmd = extractCommand(command);
  194. const commandName = extractCommandName(cmd);
  195. Object.assign(this, {
  196. duration: calculateDurationInMs(started),
  197. commandName,
  198. failure: maybeRedact(commandName, error),
  199. requestId: command.requestId,
  200. connectionId: generateConnectionId(pool)
  201. });
  202. }
  203. }
  204. module.exports = {
  205. CommandStartedEvent,
  206. CommandSucceededEvent,
  207. CommandFailedEvent
  208. };