From c4bbecd2736d60815a6a4465524049a205aa5275 Mon Sep 17 00:00:00 2001 From: Maarten Ackermans <4571935+mackermans@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:58:54 +0200 Subject: [PATCH] feat(plugin-neo4j): support for neo4j-driver up to 5.12.0 --- .github/workflows/plugins.yml | 6 +- docker-compose.yml | 6 +- .../src/helpers/hooks.js | 3 +- .../datadog-instrumentations/src/neo4j.js | 83 +++-- .../datadog-plugin-neo4j/test/index.spec.js | 298 +++++++++--------- packages/dd-trace/src/plugins/index.js | 3 +- 6 files changed, 220 insertions(+), 179 deletions(-) diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 0af70a6baae..11a8bff3d18 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -738,12 +738,12 @@ jobs: runs-on: ubuntu-latest services: neo4j: - image: neo4j:4.2.3 + image: neo4j:5.11.0 env: - NEO4J_AUTH: 'neo4j/test' + NEO4J_AUTH: 'neo4j/test-password' ports: - 7474:7474 - - 11011:7687 + - 7687:7687 env: PLUGINS: neo4j SERVICES: neo4j diff --git a/docker-compose.yml b/docker-compose.yml index eae2d97017e..cf58ab7776f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,12 +83,12 @@ services: ports: - "127.0.0.1:8081:8081" neo4j: - image: neo4j:4.2.3 + image: neo4j:5.11.0 environment: - - NEO4J_AUTH=neo4j/test + - NEO4J_AUTH=neo4j/test-password ports: - "7474:7474" - - "11011:7687" + - "7687:7687" localstack: # TODO: Figure out why SNS doesn't work in >=1.2 # https://github.com/localstack/localstack/issues/7479 diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 59bf4db0414..ead2883fe3d 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -71,7 +71,8 @@ module.exports = { 'mongoose': () => require('../mongoose'), 'mysql': () => require('../mysql'), 'mysql2': () => require('../mysql2'), - 'neo4j': () => require('../neo4j'), + 'neo4j-driver': () => require('../neo4j'), + 'neo4j-driver-core': () => require('../neo4j'), 'net': () => require('../net'), 'next': () => require('../next'), 'oracledb': () => require('../oracledb'), diff --git a/packages/datadog-instrumentations/src/neo4j.js b/packages/datadog-instrumentations/src/neo4j.js index 89b26db3892..b6f09ab397e 100644 --- a/packages/datadog-instrumentations/src/neo4j.js +++ b/packages/datadog-instrumentations/src/neo4j.js @@ -11,29 +11,53 @@ const startCh = channel('apm:neo4j:query:start') const finishCh = channel('apm:neo4j:query:finish') const errorCh = channel('apm:neo4j:query:error') -addHook({ name: 'neo4j-driver-core', file: 'lib/session.js', versions: ['>=4.3.0'] }, exports => { - const Session = exports.default - shimmer.wrap(Session.prototype, 'run', wrapRun) - return Session -}) - -addHook({ name: 'neo4j-driver-core', file: 'lib/transaction.js', versions: ['>=4.3.0'] }, exports => { - const Transaction = exports.default - shimmer.wrap(Transaction.prototype, 'run', wrapRun) - return Transaction -}) - -addHook({ name: 'neo4j-driver', file: 'lib/session.js', versions: ['<4.3.0', '>=4.0.0'] }, exports => { - const Session = exports.default - shimmer.wrap(Session.prototype, 'run', wrapRun) - return Session -}) - -addHook({ name: 'neo4j-driver', file: 'lib/transaction.js', versions: ['<4.3.0', '>=4.0.0'] }, exports => { - const Transaction = exports.default - shimmer.wrap(Transaction.prototype, 'run', wrapRun) - return Transaction -}) +addHook( + { + name: 'neo4j-driver-core', + file: 'lib/session.js', + versions: ['>=4.3.0'] + }, + (exports) => { + shimmer.wrap(exports.default.prototype, 'run', wrapRun) + return exports + } +) + +addHook( + { + name: 'neo4j-driver-core', + file: 'lib/transaction.js', + versions: ['>=4.3.0'] + }, + (exports) => { + shimmer.wrap(exports.default.prototype, 'run', wrapRun) + return exports + } +) + +addHook( + { + name: 'neo4j-driver', + file: 'lib/session.js', + versions: ['>=4.0.0 <4.3.0'] + }, + (exports) => { + shimmer.wrap(exports.default.prototype, 'run', wrapRun) + return exports + } +) + +addHook( + { + name: 'neo4j-driver', + file: 'lib/transaction.js', + versions: ['>=4.0.0 <4.3.0'] + }, + (exports) => { + shimmer.wrap(exports.default.prototype, 'run', wrapRun) + return exports + } +) function wrapRun (run) { return function (statement) { @@ -86,7 +110,6 @@ function getAttributesFromNeo4jSession (session) { // seedRouter is used when connecting to a url that starts with "neo4j", usually aura const address = connectionProvider._address || connectionProvider._seedRouter - const auth = connectionProvider._authToken || {} const attributes = { // "neo4j" is the default database name. When used, "session._database" is an empty string @@ -96,8 +119,20 @@ function getAttributesFromNeo4jSession (session) { attributes.host = address._host attributes.port = address._port } + + // neo4j-driver <5.12.0 + const auth = connectionProvider._authToken || {} if (auth.principal) { attributes.dbUser = auth.principal } + + // neo4j-driver >=5.12.0 + const authProvider = connectionProvider._authenticationProvider || {} + const authTokenManager = authProvider._authTokenManager || {} + const authToken = authTokenManager._authToken || {} + if (authToken.principal) { + attributes.dbUser = authToken.principal + } + return attributes } diff --git a/packages/datadog-plugin-neo4j/test/index.spec.js b/packages/datadog-plugin-neo4j/test/index.spec.js index f2deb169f31..c3019f403c5 100644 --- a/packages/datadog-plugin-neo4j/test/index.spec.js +++ b/packages/datadog-plugin-neo4j/test/index.spec.js @@ -1,208 +1,212 @@ 'use strict' const agent = require('../../dd-trace/test/plugins/agent') +const { ERROR_MESSAGE, ERROR_STACK, ERROR_TYPE } = require('../../dd-trace/src/constants') describe('Plugin', () => { - let neo4j - let tracer - - withVersions('neo4j', ['neo4j-driver'], (version, moduleName) => { + withVersions('neo4j', ['neo4j-driver', 'neo4j-driver-core'], (version, moduleName) => { const metaModule = require(`../../../versions/${moduleName}@${version}`) describe('neo4j', () => { + let driver + let neo4j + let tracer + beforeEach(async () => { tracer = await require('../../dd-trace') - }) - describe('driver', () => { - let driver + await agent.load('neo4j') - before(() => { - return agent.load('neo4j') - }) + if (moduleName === 'neo4j-driver-core') { + neo4j = proxyquire(`../../../versions/neo4j-driver@${version}`, { + 'neo4j-driver-core': metaModule.get() + }).get() + } else { + neo4j = metaModule.get() + } - after(() => { - return agent.close({ ritmReset: false }) + driver = neo4j.driver('bolt://localhost:7687', neo4j.auth.basic('neo4j', 'test-password'), { + disableLosslessIntegers: true }) - beforeEach(async () => { - neo4j = metaModule.get() + await driver.verifyConnectivity() + }) - driver = neo4j.driver('bolt://localhost:11011', neo4j.auth.basic('neo4j', 'test'), { - disableLosslessIntegers: true - }) + afterEach(async () => { + await driver.close() + await agent.close({ ritmReset: false }) + }) + + describe('session', () => { + let session - await driver.verifyConnectivity() - await driver.session().run('MATCH (n) DETACH DELETE n') + beforeEach(() => { + session = driver.session() }) afterEach(async () => { - await driver.close() + await session.close() }) - describe('session', () => { - let session + it('should set the correct tags', async () => { + const statement = 'CREATE (n:Person { name: $name }) RETURN n.name' - beforeEach(() => { - session = driver.session() - }) - - afterEach(async () => { - await session.close() - }) + const expectedTagsPromise = agent + .use(traces => { + const span = traces[0][0] - it('should set the correct tags', async () => { - const statement = 'CREATE (n:Person { name: $name }) RETURN n.name' - - const expectedTagsPromise = agent - .use(traces => { - const span = traces[0][0] - - expect(span).to.have.property('name', 'neo4j.query') - expect(span).to.have.property('service', 'test-neo4j') - expect(span).to.have.property('resource', statement) - expect(span).to.have.property('type', 'cypher') - expect(span.meta).to.have.property('span.kind', 'client') - expect(span.meta).to.have.property('db.name', 'neo4j') - expect(span.meta).to.have.property('db.type', 'neo4j') - expect(span.meta).to.have.property('db.user', 'neo4j') - expect(span.meta).to.have.property('out.host', 'localhost') - expect(span.metrics).to.have.property('out.port', 11011) + expect(span).to.include({ + name: 'neo4j.query', + service: 'test-neo4j', + resource: statement, + type: 'cypher' }) + expect(span.meta).to.include({ + 'span.kind': 'client', + 'db.name': 'neo4j', + 'db.type': 'neo4j', + 'db.user': 'neo4j', + 'out.host': 'localhost' + }) + expect(span.metrics).to.have.property('out.port', 7687) + }) - await session.run(statement, { name: 'Alice' }) - - await expectedTagsPromise - }) + await session.run(statement, { name: 'Alice' }) - it('should propagate context', async () => { - const expectedSpanPromise = agent - .use(traces => { - const span = traces[0][0] + await expectedTagsPromise + }) - expect(span).to.include({ - name: 'test-context', - service: 'test' - }) + it('should propagate context', async () => { + const expectedSpanPromise = agent + .use(traces => { + const span = traces[0][0] - expect(span.parent_id).to.not.be.null + expect(span).to.include({ + name: 'test-context', + service: 'test' }) - const span = tracer.startSpan('test-context') - - await tracer.scope().activate(span, async () => { - await session.run('MATCH (n) return n LIMIT 1') - await span.finish() + expect(span.parent_id).to.not.be.null }) - await expectedSpanPromise + + const span = tracer.startSpan('test-context') + + await tracer.scope().activate(span, async () => { + await session.run('MATCH (n) return n LIMIT 1') + await span.finish() }) + await expectedSpanPromise + }) - it('should handle errors', async () => { - let error + it('should handle errors', async () => { + let error - const expectedSpanPromise = agent - .use(traces => { - const span = traces[0][0] + const expectedSpanPromise = agent + .use(traces => { + const span = traces[0][0] - expect(span.meta).to.have.property('error.type', error.name) - expect(span.meta).to.have.property('error.msg', error.message) - expect(span.meta).to.have.property('error.stack', error.stack) - }) + expect(span.meta).to.have.property(ERROR_TYPE, error.name) + expect(span.meta).to.have.property(ERROR_MESSAGE, error.message) + expect(span.meta).to.have.property(ERROR_STACK, error.stack) + }) - try { - await session.run('NOT_EXISTS_OPERATION') - } catch (err) { - error = err - } + try { + await session.run('NOT_EXISTS_OPERATION') + } catch (err) { + error = err + } - await expectedSpanPromise - }) + await expectedSpanPromise }) + }) - describe('transaction', () => { - let session + describe('transaction', () => { + let session - beforeEach(() => { - session = driver.session() - }) + beforeEach(() => { + session = driver.session() + }) - afterEach(async () => { - await session.close() - }) + afterEach(async () => { + await session.close() + }) - it('should set the correct tags', async () => { - const statement = 'MATCH (m:Movie { name: $name }) RETURN m.name' - - const expectedTagsPromise = agent - .use(traces => { - const span = traces[0][0] - - expect(span).to.have.property('name', 'neo4j.query') - expect(span).to.have.property('service', 'test-neo4j') - expect(span).to.have.property('resource', statement) - expect(span).to.have.property('type', 'cypher') - expect(span.meta).to.have.property('span.kind', 'client') - expect(span.meta).to.have.property('db.name', 'neo4j') - expect(span.meta).to.have.property('db.type', 'neo4j') - expect(span.meta).to.have.property('db.user', 'neo4j') - expect(span.meta).to.have.property('out.host', 'localhost') - expect(span.metrics).to.have.property('out.port', 11011) - }) + it('should set the correct tags', async () => { + const statement = 'MATCH (m:Movie { name: $name }) RETURN m.name' - const transaction = session.beginTransaction() - await transaction.run(statement, { name: 'Alice in Wonderland' }) - await transaction.commit() + const expectedTagsPromise = agent + .use(traces => { + const span = traces[0][0] - await expectedTagsPromise - }) + expect(span).to.include({ + name: 'neo4j.query', + service: 'test-neo4j', + resource: statement, + type: 'cypher' + }) + expect(span.meta).to.include({ + 'span.kind': 'client', + 'db.name': 'neo4j', + 'db.type': 'neo4j', + 'db.user': 'neo4j', + 'out.host': 'localhost' + }) + expect(span.metrics).to.have.property('out.port', 7687) + }) - it('should propagate context', async () => { - const expectedSpanPromise = agent - .use(traces => { - const span = traces[0][0] + const transaction = session.beginTransaction() + await transaction.run(statement, { name: 'Alice in Wonderland' }) + await transaction.commit() - expect(span).to.include({ - name: 'test-context', - service: 'test' - }) + await expectedTagsPromise + }) - expect(span.parent_id).to.not.be.null - }) + it('should propagate context', async () => { + const expectedSpanPromise = agent + .use(traces => { + const span = traces[0][0] - const span = tracer.startSpan('test-context') + expect(span).to.include({ + name: 'test-context', + service: 'test' + }) - await tracer.scope().activate(span, async () => { - const transaction = session.beginTransaction() - await transaction.run('MATCH (n) return n LIMIT 1') - await transaction.commit() - await span.finish() + expect(span.parent_id).to.not.be.null }) - await expectedSpanPromise + const span = tracer.startSpan('test-context') + + await tracer.scope().activate(span, async () => { + const transaction = session.beginTransaction() + await transaction.run('MATCH (n) return n LIMIT 1') + await transaction.commit() + await span.finish() }) - it('should handle errors', async () => { - let error + await expectedSpanPromise + }) + + it('should handle errors', async () => { + let error - const expectedSpanPromise = agent - .use(traces => { - const span = traces[0][0] + const expectedSpanPromise = agent + .use(traces => { + const span = traces[0][0] - expect(span.meta).to.have.property('error.type', error.name) - expect(span.meta).to.have.property('error.msg', error.message) - expect(span.meta).to.have.property('error.stack', error.stack) - }) + expect(span.meta).to.have.property(ERROR_TYPE, error.name) + expect(span.meta).to.have.property(ERROR_MESSAGE, error.message) + expect(span.meta).to.have.property(ERROR_STACK, error.stack) + }) - try { - const transaction = session.beginTransaction() - await transaction.run('NOT_EXISTS_OPERATION') - await transaction.commit() - } catch (err) { - error = err - } + try { + const transaction = session.beginTransaction() + await transaction.run('NOT_EXISTS_OPERATION') + await transaction.commit() + } catch (err) { + error = err + } - await expectedSpanPromise - }) + await expectedSpanPromise }) }) }) diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index cc2a971b5c3..aa837dbbf18 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -56,8 +56,9 @@ module.exports = { get 'mongodb-core' () { return require('../../../datadog-plugin-mongodb-core/src') }, get 'mysql' () { return require('../../../datadog-plugin-mysql/src') }, get 'mysql2' () { return require('../../../datadog-plugin-mysql2/src') }, + get 'neo4j-driver' () { return require('../../../datadog-plugin-neo4j/src') }, + get 'neo4j-driver-core' () { return require('../../../datadog-plugin-neo4j/src') }, get 'net' () { return require('../../../datadog-plugin-net/src') }, - get 'neo4j' () { return require('../../../datadog-plugin-neo4j/src') }, get 'next' () { return require('../../../datadog-plugin-next/src') }, get 'oracledb' () { return require('../../../datadog-plugin-oracledb/src') }, get 'openai' () { return require('../../../datadog-plugin-openai/src') },