opentelemetry-instrumentation-neo4j
Version:
open telemetry instrumentation for the `neo4j` javascript driver
425 lines • 19.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("mocha");
const expect_1 = __importDefault(require("expect"));
const api_1 = require("@opentelemetry/api");
const src_1 = require("../src");
const assert_1 = require("./assert");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const test_utils_1 = require("./test-utils");
const operators_1 = require("rxjs/operators");
const rxjs_1 = require("rxjs");
const contrib_test_utils_1 = require("@opentelemetry/contrib-test-utils");
const instrumentation = (0, contrib_test_utils_1.registerInstrumentationTesting)(new src_1.Neo4jInstrumentation());
instrumentation.enable();
instrumentation.disable();
const neo4j_driver_1 = __importDefault(require("neo4j-driver"));
/**
* Tests require neo4j to run, and expose bolt port of 11011
*
* Use this command to run the required neo4j using docker:
* docker run --name testneo4j -p7474:7474 -p11011:7687 -d --env NEO4J_AUTH=neo4j/test neo4j:4.4.34
* */
describe('neo4j instrumentation', function () {
this.timeout(10000);
let driver;
const getSingleSpan = () => {
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(1);
return spans[0];
};
before(async () => {
driver = neo4j_driver_1.default.driver('bolt://localhost:11011', neo4j_driver_1.default.auth.basic('neo4j', 'your_password'), {
disableLosslessIntegers: true,
});
let keepChecking = true;
const timeoutId = setTimeout(() => {
keepChecking = false;
}, 8000);
while (keepChecking) {
try {
await driver.verifyConnectivity();
clearTimeout(timeoutId);
return;
}
catch (err) {
await new Promise((res) => setTimeout(res, 1000));
}
}
throw new Error('Could not connect to neo4j in allowed time frame');
});
after(async () => {
await driver.close();
});
beforeEach(async () => {
await driver.session().run('MATCH (n) DETACH DELETE n');
instrumentation.enable();
});
afterEach(async () => {
instrumentation.disable();
instrumentation.setConfig({});
});
describe('session', () => {
it('instruments "run" with promise', async () => {
const res = await driver.session().run('CREATE (n:MyLabel) RETURN n');
(0, expect_1.default)(res.records.length).toBe(1);
(0, expect_1.default)(res.records[0].toObject().n.labels).toEqual(['MyLabel']);
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.name).toBe('CREATE neo4j');
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('CREATE');
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('CREATE (n:MyLabel) RETURN n');
});
it('instruments "run" with subscribe', (done) => {
driver
.session()
.run('CREATE (n:MyLabel) RETURN n')
.subscribe({
onCompleted: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('CREATE');
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('CREATE (n:MyLabel) RETURN n');
done();
},
});
});
it('handles "run" exceptions with promise', async () => {
try {
await driver.session().run('NOT_EXISTS_OPERATION');
}
catch (err) {
const span = getSingleSpan();
(0, expect_1.default)(span.status.code).toBe(api_1.SpanStatusCode.ERROR);
(0, expect_1.default)(span.status.message).toBe(err.message);
return;
}
throw Error('should not be here');
});
it('handles "run" exceptions with subscribe', (done) => {
driver
.session()
.run('NOT_EXISTS_OPERATION')
.subscribe({
onError: (err) => {
const span = getSingleSpan();
(0, expect_1.default)(span.status.code).toBe(api_1.SpanStatusCode.ERROR);
(0, expect_1.default)(span.status.message).toBe(err.message);
done();
},
});
});
it('closes span when on "onKeys" event', (done) => {
driver
.session()
.run('MATCH (n) RETURN n')
.subscribe({
onKeys: (keys) => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(keys).toEqual(['n']);
done();
},
});
});
it('when passing "onKeys" and onCompleted, span is closed in onCompleted, and response hook is called', (done) => {
instrumentation.disable();
instrumentation.setConfig({ responseHook: (span) => span.setAttribute('test', 'cool') });
instrumentation.enable();
driver
.session()
.run('MATCH (n) RETURN n')
.subscribe({
onKeys: () => {
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(0);
},
onCompleted: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes['test']).toBe('cool');
done();
},
});
});
it('handles multiple promises', async () => {
await Promise.all([
driver.session().run('MATCH (n) RETURN n'),
driver.session().run('MATCH (k) RETURN k'),
driver.session().run('MATCH (d) RETURN d'),
]);
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(3);
for (let span of spans) {
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('MATCH');
}
});
it('captures operation with trailing white spaces', async () => {
await driver.session().run(' MATCH (k) RETURN k ');
const span = getSingleSpan();
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('MATCH');
});
it('set module versions when config is set', async () => {
instrumentation.disable();
instrumentation.setConfig({ moduleVersionAttributeName: 'module.version' });
instrumentation.enable();
await driver.session().run('CREATE (n:MyLabel) RETURN n');
const span = getSingleSpan();
(0, expect_1.default)(span.attributes['module.version']).toMatch(/\d{1,4}\.\d{1,4}\.\d{1,5}.*/);
});
it('does not capture any span when ignoreOrphanedSpans is set to true', async () => {
instrumentation.disable();
instrumentation.setConfig({ ignoreOrphanedSpans: true });
instrumentation.enable();
await api_1.context.with(api_1.ROOT_CONTEXT, async () => {
await driver.session().run('CREATE (n:MyLabel) RETURN n');
});
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(0);
});
it('does capture span when ignoreOrphanedSpans is set to true and has parent span', async () => {
instrumentation.disable();
instrumentation.setConfig({ ignoreOrphanedSpans: true });
instrumentation.enable();
const parent = api_1.trace.getTracerProvider().getTracer('test-tracer').startSpan('main');
await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), parent), () => driver.session().run('CREATE (n:MyLabel) RETURN n'));
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(1);
});
it('responseHook works with promise', async () => {
instrumentation.disable();
instrumentation.setConfig({
responseHook: (span, response) => {
span.setAttribute('db.response', (0, test_utils_1.normalizeResponse)(response));
},
});
instrumentation.enable();
const res = await driver
.session()
.run('CREATE (n:Rick), (b:Meeseeks { purpose: "help"}), (c:Morty) RETURN *');
(0, expect_1.default)(res.records.length).toBe(1);
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(JSON.parse(span.attributes['db.response'])).toEqual([
{
b: { labels: ['Meeseeks'], properties: { purpose: 'help' } },
c: { labels: ['Morty'], properties: {} },
n: { labels: ['Rick'], properties: {} },
},
]);
});
it('responseHook works with subscribe', (done) => {
instrumentation.disable();
instrumentation.setConfig({
responseHook: (span, response) => {
span.setAttribute('db.response', (0, test_utils_1.normalizeResponse)(response));
},
});
instrumentation.enable();
driver
.session()
.run('CREATE (n:Rick), (b:Meeseeks { purpose: "help"}), (c:Morty) RETURN *')
.subscribe({
onCompleted: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(JSON.parse(span.attributes['db.response'])).toEqual([
{
b: { labels: ['Meeseeks'], properties: { purpose: 'help' } },
c: { labels: ['Morty'], properties: {} },
n: { labels: ['Rick'], properties: {} },
},
]);
done();
},
});
});
it('does not fail when responseHook throws', async () => {
instrumentation.disable();
instrumentation.setConfig({
responseHook: () => {
throw new Error('I throw..');
},
});
instrumentation.enable();
await driver.session().run('CREATE (n:MyLabel) RETURN n');
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
});
});
describe('transaction', async () => {
it('instruments session readTransaction', async () => {
await driver.session().readTransaction((txc) => {
return txc.run('MATCH (person:Person) RETURN person.name AS name');
});
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('MATCH');
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('MATCH (person:Person) RETURN person.name AS name');
});
it('instruments session writeTransaction', async () => {
await driver.session().writeTransaction((txc) => {
return txc.run('MATCH (person:Person) RETURN person.name AS name');
});
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_OPERATION]).toBe('MATCH');
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('MATCH (person:Person) RETURN person.name AS name');
});
it('instruments explicit transactions', async () => {
const txc = driver.session().beginTransaction();
await txc.run('MERGE (bob:Person {name: "Bob"}) RETURN bob.name AS name');
await txc.run('MERGE (adam:Person {name: "Adam"}) RETURN adam.name AS name');
await txc.commit();
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(2);
});
});
describe('rxSession', () => {
it('instruments "run"', (done) => {
driver
.rxSession()
.run('MERGE (n:MyLabel) RETURN n')
.records()
.subscribe({
complete: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
done();
},
});
});
it('works when piping response', (done) => {
const rxSession = driver.rxSession();
rxSession
.run('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name', {
nameParam: 'Bob',
})
.records()
.pipe((0, operators_1.map)((record) => record.get('name')))
.subscribe({
next: () => { },
complete: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('MERGE (james:Person {name: $nameParam}) RETURN james.name AS name');
done();
},
error: () => { },
});
});
it('works with response hook', (done) => {
instrumentation.disable();
instrumentation.setConfig({
responseHook: (span, response) => {
span.setAttribute('db.response', (0, test_utils_1.normalizeResponse)(response));
},
});
instrumentation.enable();
driver
.rxSession()
.run('MERGE (n:MyLabel) RETURN n')
.records()
.subscribe({
complete: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes['db.response']).toBe(`[{"n":{"labels":["MyLabel"],"properties":{}}}]`);
done();
},
});
});
});
describe('reactive transaction', () => {
it('instruments rx session readTransaction', (done) => {
driver
.rxSession()
.readTransaction((txc) => txc
.run('MATCH (person:Person) RETURN person.name AS name')
.records()
.pipe((0, operators_1.map)((record) => record.get('name'))))
.subscribe({
next: () => { },
complete: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('MATCH (person:Person) RETURN person.name AS name');
done();
},
error: () => { },
});
});
it('instruments rx session writeTransaction', (done) => {
driver
.rxSession()
.writeTransaction((txc) => txc
.run('MATCH (person:Person) RETURN person.name AS name')
.records()
.pipe((0, operators_1.map)((record) => record.get('name'))))
.subscribe({
next: () => { },
complete: () => {
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
(0, expect_1.default)(span.attributes[semantic_conventions_1.SEMATTRS_DB_STATEMENT]).toBe('MATCH (person:Person) RETURN person.name AS name');
done();
},
error: () => { },
});
});
it('instruments rx explicit transactions', (done) => {
driver
.rxSession()
.beginTransaction()
.pipe((0, operators_1.mergeMap)((txc) => (0, rxjs_1.concat)(txc
.run('MERGE (bob:Person {name: $nameParam}) RETURN bob.name AS name', {
nameParam: 'Bob',
})
.records()
.pipe((0, operators_1.map)((r) => r.get('name'))), txc
.run('MERGE (adam:Person {name: $nameParam}) RETURN adam.name AS name', {
nameParam: 'Adam',
})
.records()
.pipe((0, operators_1.map)((r) => r.get('name'))), txc.commit())))
.subscribe({
next: () => { },
complete: () => {
const spans = (0, contrib_test_utils_1.getTestSpans)();
(0, expect_1.default)(spans.length).toBe(2);
done();
},
error: () => { },
});
});
});
describe('routing mode', () => {
// When the connection string starts with "neo4j" routing mode is used
let routingDriver;
const version = require('neo4j-driver/package.json').version;
const shouldCheck = !['4.0.0', '4.0.1', '4.0.2'].includes(version);
before(() => {
if (shouldCheck) {
routingDriver = neo4j_driver_1.default.driver('neo4j://localhost:11011', neo4j_driver_1.default.auth.basic('neo4j', 'your_password'));
}
});
after(async () => {
shouldCheck && (await routingDriver.close());
});
it('instruments as expected in routing mode', async () => {
if (!shouldCheck) {
// Versions 4.0.0, 4.0.1 and 4.0.2 of neo4j-driver don't allow connection to local neo4j in routing mode.
console.log(`Skipping unsupported test for version ${version}`);
return;
}
await routingDriver.session().run('CREATE (n:MyLabel) RETURN n');
const span = getSingleSpan();
(0, assert_1.assertSpan)(span);
});
});
});
//# sourceMappingURL=neo4j.spec.js.map