| /*! |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| import SwPlugin, { wrapEmit, wrapPromise } from '../core/SwPlugin'; |
| import ContextManager from '../trace/context/ContextManager'; |
| import { Component } from '../trace/Component'; |
| import Tag from '../Tag'; |
| import { SpanLayer } from '../proto/language-agent/Tracing_pb'; |
| import PluginInstaller from '../core/PluginInstaller'; |
| import agentConfig from '../config/AgentConfig'; |
| |
| class MongoDBPlugin implements SwPlugin { |
| readonly module = 'mongodb'; |
| readonly versions = '*'; |
| |
| Collection: any; |
| Cursor: any; |
| Db: any; |
| |
| hookCursorMaybe(span: any, cursor: any): boolean { |
| if (!(cursor instanceof this.Cursor)) return false; |
| |
| wrapEmit(span, cursor, true, 'close'); |
| |
| return true; |
| } |
| |
| install(installer: PluginInstaller): void { |
| const plugin = this; |
| this.Collection = installer.require?.('mongodb/lib/collection') ?? require('mongodb/lib/collection'); |
| this.Cursor = installer.require?.('mongodb/lib/cursor') ?? require('mongodb/lib/cursor'); |
| this.Db = installer.require?.('mongodb/lib/db') ?? require('mongodb/lib/db'); |
| |
| const wrapCallbackWithCursorMaybe = (span: any, args: any[], idx: number): boolean => { |
| const callback = args.length > idx && typeof args[(idx = args.length - 1)] === 'function' ? args[idx] : null; |
| |
| if (!callback) return false; |
| |
| args[idx] = function (this: any) { |
| // arguments = [error: any, result: any] |
| span.mongodbInCall = false; // we do this because some operations may call callback immediately which would not create a new span for any operations in that callback (db.collection()) |
| |
| if (arguments[0]) span.error(arguments[0]); |
| |
| if (arguments[0] || !plugin.hookCursorMaybe(span, arguments[1])) span.stop(); |
| |
| return callback.apply(this, arguments); |
| }; |
| |
| return true; |
| }; |
| |
| const stringify = (params: any) => { |
| if (params === undefined) return ''; |
| else if (typeof params === 'function') return `${params}`; |
| |
| let str = JSON.stringify(params); |
| |
| if (str.length > agentConfig.mongoParametersMaxLength) |
| str = str.slice(0, agentConfig.mongoParametersMaxLength) + ' ...'; |
| |
| return str; |
| }; |
| |
| const collInsertFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [doc(s), options, callback] |
| span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}()`)); |
| |
| if (agentConfig.mongoTraceParameters) span.tag(Tag.dbMongoParameters(stringify(args[0]))); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| const collDeleteFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [filter, options, callback] |
| span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])})`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| const collUpdateFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [filter, update, options, callback] |
| span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])})`)); |
| |
| if (agentConfig.mongoTraceParameters) span.tag(Tag.dbMongoParameters(stringify(args[1]))); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 2); |
| }; |
| |
| const collFindOneFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [query, options, callback] |
| span.tag( |
| Tag.dbStatement( |
| `${this.s.namespace.collection}.${operation}(${typeof args[0] !== 'function' ? stringify(args[0]) : ''})`, |
| ), |
| ); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 0); |
| }; |
| |
| const collFindAndRemoveFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [query, sort, options, callback] |
| span.tag( |
| Tag.dbStatement( |
| `${this.s.namespace.collection}.${operation}(${stringify(args[0])}${ |
| typeof args[1] !== 'function' && args[1] !== undefined ? ', ' + stringify(args[1]) : '' |
| })`, |
| ), |
| ); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| const collFindAndModifyFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [query, sort, doc, options, callback] |
| let params = stringify(args[0]); |
| |
| if (typeof args[1] !== 'function' && args[1] !== undefined) { |
| params += ', ' + stringify(args[1]); |
| |
| if (typeof args[2] !== 'function' && args[2] !== undefined) { |
| if (agentConfig.mongoTraceParameters) span.tag(Tag.dbMongoParameters(stringify(args[2]))); |
| } |
| } |
| |
| span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${params})`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| const collMapReduceFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [map, reduce, options, callback] |
| span.tag( |
| Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])}, ${stringify(args[1])})`), |
| ); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 2); |
| }; |
| |
| const collDropFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [options, callback] |
| span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}()`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 0); |
| }; |
| |
| const dbAddUserFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [username, password, options, callback] |
| span.tag(Tag.dbStatement(`${operation}(${stringify(args[0])})`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 2); |
| }; |
| |
| const dbRemoveUserFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [username, options, callback] |
| span.tag(Tag.dbStatement(`${operation}(${stringify(args[0])})`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| const dbRenameCollectionFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [fromCollection, toCollection, options, callback] |
| span.tag(Tag.dbStatement(`${operation}(${stringify(args[0])}, ${stringify(args[1])})`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 2); |
| }; |
| |
| const dbCollectionsFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [options, callback] |
| span.tag(Tag.dbStatement(`${operation}()`)); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 0); |
| }; |
| |
| const dbEvalFunc = function (this: any, operation: string, span: any, args: any[]): boolean { |
| // args = [code, parameters, options, callback] |
| span.tag( |
| Tag.dbStatement( |
| `${operation}(${stringify(args[0])}${ |
| typeof args[1] !== 'function' && args[1] !== undefined ? ', ' + stringify(args[1]) : '' |
| })`, |
| ), |
| ); |
| |
| return wrapCallbackWithCursorMaybe(span, args, 1); |
| }; |
| |
| this.interceptOperation(this.Collection, 'insert', collInsertFunc); |
| this.interceptOperation(this.Collection, 'insertOne', collInsertFunc); |
| this.interceptOperation(this.Collection, 'insertMany', collInsertFunc); |
| this.interceptOperation(this.Collection, 'save', collInsertFunc); |
| this.interceptOperation(this.Collection, 'deleteOne', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'deleteMany', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'remove', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'removeOne', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'removeMany', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'update', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'updateOne', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'updateMany', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'replaceOne', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'find', collFindOneFunc); // cursor |
| this.interceptOperation(this.Collection, 'findOne', collFindOneFunc); |
| this.interceptOperation(this.Collection, 'findOneAndDelete', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'findOneAndReplace', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'findOneAndUpdate', collUpdateFunc); |
| this.interceptOperation(this.Collection, 'findAndRemove', collFindAndRemoveFunc); |
| this.interceptOperation(this.Collection, 'findAndModify', collFindAndModifyFunc); |
| |
| this.interceptOperation(this.Collection, 'bulkWrite', collInsertFunc); |
| this.interceptOperation(this.Collection, 'mapReduce', collMapReduceFunc); |
| this.interceptOperation(this.Collection, 'aggregate', collDeleteFunc); // cursor |
| this.interceptOperation(this.Collection, 'distinct', collFindAndRemoveFunc); |
| this.interceptOperation(this.Collection, 'count', collFindOneFunc); |
| this.interceptOperation(this.Collection, 'estimatedDocumentCount', collDropFunc); |
| this.interceptOperation(this.Collection, 'countDocuments', collFindOneFunc); |
| |
| this.interceptOperation(this.Collection, 'createIndex', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'createIndexes', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'ensureIndex', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'dropIndex', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'dropIndexes', collDropFunc); |
| this.interceptOperation(this.Collection, 'dropAllIndexes', collDropFunc); |
| this.interceptOperation(this.Collection, 'reIndex', collDropFunc); |
| |
| this.interceptOperation(this.Collection, 'indexes', collDropFunc); |
| this.interceptOperation(this.Collection, 'indexExists', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'indexInformation', collDropFunc); |
| this.interceptOperation(this.Collection, 'listIndexes', collDropFunc); // cursor |
| this.interceptOperation(this.Collection, 'stats', collDropFunc); |
| |
| this.interceptOperation(this.Collection, 'rename', collDeleteFunc); |
| this.interceptOperation(this.Collection, 'drop', collDropFunc); |
| this.interceptOperation(this.Collection, 'options', collDropFunc); |
| this.interceptOperation(this.Collection, 'isCapped', collDropFunc); |
| |
| this.interceptOperation(this.Db, 'aggregate', dbAddUserFunc); // cursor |
| |
| this.interceptOperation(this.Db, 'addUser', dbAddUserFunc); |
| this.interceptOperation(this.Db, 'removeUser', dbRemoveUserFunc); |
| |
| this.interceptOperation(this.Db, 'collection', dbRemoveUserFunc); |
| this.interceptOperation(this.Db, 'createCollection', dbRemoveUserFunc); |
| this.interceptOperation(this.Db, 'renameCollection', dbRenameCollectionFunc); |
| this.interceptOperation(this.Db, 'dropCollection', dbRemoveUserFunc); |
| this.interceptOperation(this.Db, 'collections', dbCollectionsFunc); |
| this.interceptOperation(this.Db, 'listCollections', dbAddUserFunc); // cursor |
| |
| this.interceptOperation(this.Db, 'createIndex', dbRenameCollectionFunc); |
| this.interceptOperation(this.Db, 'ensureIndex', dbRenameCollectionFunc); |
| this.interceptOperation(this.Db, 'indexInformation', dbRemoveUserFunc); |
| this.interceptOperation(this.Db, 'stats', dbCollectionsFunc); |
| |
| this.interceptOperation(this.Db, 'command', dbRemoveUserFunc); |
| this.interceptOperation(this.Db, 'eval', dbEvalFunc); |
| this.interceptOperation(this.Db, 'executeDbAdminCommand', dbRemoveUserFunc); |
| |
| this.interceptOperation(this.Db, 'dropDatabase', dbCollectionsFunc); |
| |
| // TODO collection? |
| // group |
| // parallelCollectionScan |
| // geoHaystackSearch |
| |
| // NODO collection: |
| // initializeUnorderedBulkOp |
| // initializeOrderedBulkOp |
| // watch |
| |
| // NODO db: |
| // admin |
| // profilingLevel |
| // setProfilingLevel |
| // unref |
| // watch |
| } |
| |
| interceptOperation(Cls: any, operation: string, operationFunc: any): void { |
| const plugin = this; |
| const _original = Cls.prototype[operation]; |
| |
| if (!_original) return; |
| |
| Cls.prototype[operation] = function (...args: any[]) { |
| let span = ContextManager.currentSpan; |
| |
| // XXX: mongodb calls back into itself at this level in several places, for this reason we just do a normal call |
| // if this is detected instead of opening a new span. This should not affect secondary db calls being recorded |
| // from a cursor since this span is kept async until the cursor is closed, at which point it is stoppped. |
| |
| if ((span as any)?.mongodbInCall) |
| // mongodb has called into itself internally |
| return _original.apply(this, args); |
| |
| let host = '???'; |
| |
| try { |
| const db = this instanceof plugin.Collection ? this.s.db : this; |
| host = db.serverConfig.s.options.servers.map((s: any) => `${s.host}:${s.port}`).join(','); // will this work for non-NativeTopology? |
| } catch { |
| /* nop */ |
| } |
| |
| span = ContextManager.current.newExitSpan('MongoDB/' + operation, Component.MONGODB); |
| |
| span.start(); |
| |
| try { |
| if (span.component === Component.UNKNOWN) |
| // in case mongoose sitting on top |
| span.component = Component.MONGODB; |
| |
| span.layer = SpanLayer.DATABASE; |
| span.peer = host; |
| |
| span.tag(Tag.dbType('MongoDB')); |
| span.tag(Tag.dbInstance(`${this.s.namespace.db}`)); |
| |
| const hasCB = operationFunc.call(this, operation, span, args); |
| |
| (span as any).mongodbInCall = true; |
| let ret = _original.apply(this, args); |
| (span as any).mongodbInCall = false; |
| |
| if (!hasCB) { |
| if (plugin.hookCursorMaybe(span, ret)) { |
| // NOOP |
| } else if (ret && typeof ret.then === 'function') { |
| // generic Promise check |
| ret = wrapPromise(span, ret); |
| } else { |
| // no callback passed in and no Promise or Cursor returned, play it safe |
| span.stop(); |
| |
| return ret; |
| } |
| } |
| |
| span.async(); |
| |
| return ret; |
| } catch (e) { |
| span.error(e); |
| span.stop(); |
| |
| throw e; |
| } |
| }; |
| } |
| } |
| |
| // noinspection JSUnusedGlobalSymbols |
| export default new MongoDBPlugin(); |