Skip to content

Commit

Permalink
feat: Added support for Mongo v5+ (#2085)
Browse files Browse the repository at this point in the history
  • Loading branch information
bizob2828 committed Mar 20, 2024
1 parent 07d7e7d commit 00f6feb
Show file tree
Hide file tree
Showing 18 changed files with 1,637 additions and 1,157 deletions.
22 changes: 17 additions & 5 deletions lib/instrumentation/mongodb/common.js
Expand Up @@ -72,9 +72,13 @@ common.instrumentBulkOperation = function instrumentBulkOperation(shim, BulkOper
common.instrumentDb = function instrumentDb(shim, Db) {
if (Db && Db.prototype) {
const proto = Db.prototype
shim.recordOperation(proto, DB_OPS, new OperationSpec({ callback: shim.LAST, opaque: true }))
shim.recordOperation(
proto,
DB_OPS,
new OperationSpec({ callback: shim.LAST, opaque: true, promise: true })
)
// link to client.connect(removed in v4.0)
shim.recordOperation(Db, 'connect', new OperationSpec({ callback: shim.LAST }))
shim.recordOperation(Db, 'connect', new OperationSpec({ callback: shim.LAST, promise: true }))
}
}

Expand Down Expand Up @@ -133,7 +137,7 @@ common.makeBulkDescFunc = function makeBulkDescFunc(shim) {
*
* @param {Shim} shim instance of shim
* @param {object} instrumenter instance of mongo APM class
* @param {object} [options={}] provide command names to skip updating host/port as they are unrelated to the active query. This is only in v3 because after every command is runs `endSessions` which runs on the admin database
* @param {object} [options] provide command names to skip updating host/port as they are unrelated to the active query. This is only in v3 because after every command is runs `endSessions` which runs on the admin database
*/
common.captureAttributesOnStarted = function captureAttributesOnStarted(
shim,
Expand Down Expand Up @@ -214,7 +218,6 @@ function setHostPort(shim, connStr, db, client) {
*/
function getInstanceAttributeParameters(shim, mongo) {
let params

if (mongo?.s?.topology) {
shim.logger.trace('Adding datastore instance attributes from mongo.s.db + mongo.s.topology')
const databaseName = mongo?.s?.db?.databaseName || mongo?.s?.namespace?.db || null
Expand All @@ -224,6 +227,10 @@ function getInstanceAttributeParameters(shim, mongo) {
const databaseName = mongo?.s?.db?.databaseName || null
const hosts = mongo.s.db.s.client.s.options.hosts
params = getParametersFromHosts(hosts, databaseName)
} else if (mongo?.s?.db?.client?.topology) {
const databaseName = mongo?.s?.namespace?.db
const topology = mongo.s.db.client.topology
params = getParametersFromTopology(topology, databaseName)
} else {
shim.logger.trace('Could not find datastore instance attributes.')
params = new DatastoreParameters()
Expand Down Expand Up @@ -272,6 +279,11 @@ function getParametersFromTopology(conf, database) {
;[{ host, port }] = conf.s.options.servers
}

// hosts is an array but we will always pull the first for consistency
if (conf?.s?.options?.hosts?.length) {
;[{ host, port }] = conf.s.options.hosts
}

// host is a domain socket. set host as localhost and use the domain
// socket host as the port
if (host && host.endsWith('.sock')) {
Expand All @@ -280,7 +292,7 @@ function getParametersFromTopology(conf, database) {
}

return new DatastoreParameters({
host: host,
host,
port_path_or_id: port,
database_name: database
})
Expand Down
159 changes: 63 additions & 96 deletions test/versioned/mongodb/collection-common.js
Expand Up @@ -14,16 +14,12 @@ const { version: pkgVersion } = require('mongodb/package')
let METRIC_HOST_NAME = null
let METRIC_HOST_PORT = null

exports.MONGO_SEGMENT_RE = common.MONGO_SEGMENT_RE
exports.TRANSACTION_NAME = common.TRANSACTION_NAME
exports.DB_NAME = common.DB_NAME

exports.connect = common.connect
exports.close = common.close
exports.populate = populate
exports.test = collectionTest

exports.dropTestCollections = dropTestCollections
exports.populate = populate

const { COLLECTIONS } = common

function collectionTest(name, run) {
Expand All @@ -36,30 +32,25 @@ function collectionTest(name, run) {

t.test('remote connection', function (t) {
t.autoend()
t.beforeEach(function () {
t.beforeEach(async function () {
agent = helper.instrumentMockedAgent()

const mongodb = require('mongodb')

return dropTestCollections(mongodb)
.then(() => {
METRIC_HOST_NAME = common.getHostName(agent)
METRIC_HOST_PORT = common.getPort()
return common.connect(mongodb)
})
.then((res) => {
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
return populate(db, collection)
})
await dropTestCollections(mongodb)
METRIC_HOST_NAME = common.getHostName(agent)
METRIC_HOST_PORT = common.getPort()
const res = await common.connect(mongodb)
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
await populate(collection)
})

t.afterEach(function () {
return common.close(client, db).then(() => {
helper.unloadAgent(agent)
agent = null
})
t.afterEach(async function () {
await common.close(client, db)
helper.unloadAgent(agent)
agent = null
})

t.test('should not error outside of a transaction', function (t) {
Expand Down Expand Up @@ -200,30 +191,25 @@ function collectionTest(name, run) {
const shouldTestDomain = domainPath
t.test('domain socket', { skip: !shouldTestDomain }, function (t) {
t.autoend()
t.beforeEach(function () {
t.beforeEach(async function () {
agent = helper.instrumentMockedAgent()
METRIC_HOST_NAME = agent.config.getHostnameSafe()
METRIC_HOST_PORT = domainPath

const mongodb = require('mongodb')

return dropTestCollections(mongodb)
.then(() => {
return common.connect(mongodb, domainPath)
})
.then((res) => {
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
return populate(db, collection)
})
await dropTestCollections(mongodb)
const res = await common.connect(mongodb, domainPath)
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
await populate(collection)
})

t.afterEach(function () {
return common.close(client, db).then(() => {
helper.unloadAgent(agent)
agent = null
})
t.afterEach(async function () {
await common.close(client, db)
helper.unloadAgent(agent)
agent = null
})

t.test('should have domain socket in metrics', function (t) {
Expand All @@ -247,30 +233,25 @@ function collectionTest(name, run) {

t.test('domain socket replica set', { skip: !shouldTestDomain }, function (t) {
t.autoend()
t.beforeEach(function () {
t.beforeEach(async function () {
agent = helper.instrumentMockedAgent()
METRIC_HOST_NAME = agent.config.getHostnameSafe()
METRIC_HOST_PORT = domainPath

const mongodb = require('mongodb')

return dropTestCollections(mongodb)
.then(() => {
return common.connect(mongodb, domainPath, true)
})
.then((res) => {
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
return populate(db, collection)
})
await dropTestCollections(mongodb)
const res = await common.connect(mongodb, domainPath)
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
await populate(collection)
})

t.afterEach(function () {
return common.close(client, db).then(() => {
helper.unloadAgent(agent)
agent = null
})
t.afterEach(async function () {
await common.close(client, db)
helper.unloadAgent(agent)
agent = null
})

t.test('should have domain socket in metrics', function (t) {
Expand All @@ -297,30 +278,25 @@ function collectionTest(name, run) {
if (semver.satisfies(pkgVersion, '>=3.6.0')) {
t.test('replica set string remote connection', function (t) {
t.autoend()
t.beforeEach(function () {
t.beforeEach(async function () {
agent = helper.instrumentMockedAgent()

const mongodb = require('mongodb')

return dropTestCollections(mongodb)
.then(() => {
METRIC_HOST_NAME = common.getHostName(agent)
METRIC_HOST_PORT = common.getPort()
return common.connect(mongodb, null, true)
})
.then((res) => {
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
return populate(db, collection)
})
await dropTestCollections(mongodb)
METRIC_HOST_NAME = common.getHostName(agent)
METRIC_HOST_PORT = common.getPort()
const res = await common.connect(mongodb, null, true)
client = res.client
db = res.db
collection = db.collection(COLLECTIONS.collection1)
await populate(collection)
})

t.afterEach(function () {
return common.close(client, db).then(() => {
helper.unloadAgent(agent)
agent = null
})
t.afterEach(async function () {
await common.close(client, db)
helper.unloadAgent(agent)
agent = null
})

t.test('should generate the correct metrics and segments', function (t) {
Expand Down Expand Up @@ -410,29 +386,20 @@ function checkSegmentParams(t, segment) {
t.equal(attributes.port_path_or_id, METRIC_HOST_PORT, 'should have correct port')
}

function populate(db, collection) {
return new Promise((resolve, reject) => {
const items = []
for (let i = 0; i < 30; ++i) {
items.push({
i: i,
next3: [i + 1, i + 2, i + 3],
data: Math.random().toString(36).slice(2),
mod10: i % 10,
// spiral out
loc: [i % 4 && (i + 1) % 4 ? i : -i, (i + 1) % 4 && (i + 2) % 4 ? i : -i]
})
}

db.collection(COLLECTIONS.collection2).drop(function () {
collection.deleteMany({}, function (err) {
if (err) {
reject(err)
}
collection.insert(items, resolve)
})
async function populate(collection) {
const items = []
for (let i = 0; i < 30; ++i) {
items.push({
i: i,
next3: [i + 1, i + 2, i + 3],
data: Math.random().toString(36).slice(2),
mod10: i % 10,
// spiral out
loc: [i % 4 && (i + 1) % 4 ? i : -i, (i + 1) % 4 && (i + 2) % 4 ? i : -i]
})
})
}

await collection.insertMany(items)
}

/**
Expand Down
83 changes: 21 additions & 62 deletions test/versioned/mongodb/collection-find.tap.js
Expand Up @@ -5,73 +5,32 @@

'use strict'
const common = require('./collection-common')
const semver = require('semver')
const { pkgVersion, STATEMENT_PREFIX } = require('./common')
const { STATEMENT_PREFIX } = require('./common')
const findOpt = { returnDocument: 'after' }

let findOpt = { returnOriginal: false }
// 4.0.0 changed this opt https://github.com/mongodb/node-mongodb-native/pull/2803/files
if (semver.satisfies(pkgVersion, '>=4')) {
findOpt = { returnDocument: 'after' }
}

if (semver.satisfies(pkgVersion, '<4')) {
common.test('findAndModify', function findAndModifyTest(t, collection, verify) {
collection.findAndModify({ i: 1 }, [['i', 1]], { $set: { a: 15 } }, { new: true }, done)

function done(err, data) {
t.error(err)
t.equal(data.value.a, 15)
t.equal(data.value.i, 1)
t.equal(data.ok, 1)
verify(null, [`${STATEMENT_PREFIX}/findAndModify`, 'Callback: done'], ['findAndModify'])
}
})

common.test('findAndRemove', function findAndRemoveTest(t, collection, verify) {
collection.findAndRemove({ i: 1 }, [['i', 1]], function done(err, data) {
t.error(err)
t.equal(data.value.i, 1)
t.equal(data.ok, 1)
verify(null, [`${STATEMENT_PREFIX}/findAndRemove`, 'Callback: done'], ['findAndRemove'])
})
})
}

common.test('findOne', function findOneTest(t, collection, verify) {
collection.findOne({ i: 15 }, function done(err, data) {
t.error(err)
t.equal(data.i, 15)
verify(null, [`${STATEMENT_PREFIX}/findOne`, 'Callback: done'], ['findOne'])
})
common.test('findOne', async function findOneTest(t, collection, verify) {
const data = await collection.findOne({ i: 15 })
t.equal(data.i, 15)
verify(null, [`${STATEMENT_PREFIX}/findOne`], ['findOne'], { strict: false })
})

common.test('findOneAndDelete', function findOneAndDeleteTest(t, collection, verify) {
collection.findOneAndDelete({ i: 15 }, function done(err, data) {
t.error(err)
t.equal(data.ok, 1)
t.equal(data.value.i, 15)
verify(null, [`${STATEMENT_PREFIX}/findOneAndDelete`, 'Callback: done'], ['findOneAndDelete'])
})
common.test('findOneAndDelete', async function findOneAndDeleteTest(t, collection, verify) {
const data = await collection.findOneAndDelete({ i: 15 })
const response = data?.value?.i || data.i
t.equal(response, 15)
verify(null, [`${STATEMENT_PREFIX}/findOneAndDelete`], ['findOneAndDelete'], { strict: false })
})

common.test('findOneAndReplace', function findAndReplaceTest(t, collection, verify) {
collection.findOneAndReplace({ i: 15 }, { b: 15 }, findOpt, done)

function done(err, data) {
t.error(err)
t.equal(data.value.b, 15)
t.equal(data.ok, 1)
verify(null, [`${STATEMENT_PREFIX}/findOneAndReplace`, 'Callback: done'], ['findOneAndReplace'])
}
common.test('findOneAndReplace', async function findAndReplaceTest(t, collection, verify) {
const data = await collection.findOneAndReplace({ i: 15 }, { b: 15 }, findOpt)
const response = data?.value?.b || data.b
t.equal(response, 15)
verify(null, [`${STATEMENT_PREFIX}/findOneAndReplace`], ['findOneAndReplace'], { strict: false })
})

common.test('findOneAndUpdate', function findOneAndUpdateTest(t, collection, verify) {
collection.findOneAndUpdate({ i: 15 }, { $set: { a: 15 } }, findOpt, done)

function done(err, data) {
t.error(err)
t.equal(data.value.a, 15)
t.equal(data.ok, 1)
verify(null, [`${STATEMENT_PREFIX}/findOneAndUpdate`, 'Callback: done'], ['findOneAndUpdate'])
}
common.test('findOneAndUpdate', async function findOneAndUpdateTest(t, collection, verify) {
const data = await collection.findOneAndUpdate({ i: 15 }, { $set: { a: 15 } }, findOpt)
const response = data?.value?.a || data.a
t.equal(response, 15)
verify(null, [`${STATEMENT_PREFIX}/findOneAndUpdate`], ['findOneAndUpdate'], { strict: false })
})

0 comments on commit 00f6feb

Please sign in to comment.