diff --git a/.github/workflows/test-edot.yml b/.github/workflows/test-edot.yml index 38ee72d6..d48f65f9 100644 --- a/.github/workflows/test-edot.yml +++ b/.github/workflows/test-edot.yml @@ -83,6 +83,14 @@ jobs: MSSQL_SA_PASSWORD: 'Very(!)Secure' ports: - "1433:1433" + + cassandra: + image: cassandra:3 + env: + MAX_HEAP_SIZE: "1G" + HEAP_NEWSIZE: 400m + ports: + - "9042:9042" steps: - uses: actions/checkout@v4 diff --git a/packages/opentelemetry-node/CHANGELOG.md b/packages/opentelemetry-node/CHANGELOG.md index f17a9742..9ec1c93a 100644 --- a/packages/opentelemetry-node/CHANGELOG.md +++ b/packages/opentelemetry-node/CHANGELOG.md @@ -6,6 +6,9 @@ of instrumentations. See - feat: Add `@opentelemetry/instrumentation-mysql2` to the default set of instrumentations. See +- feat: Add `@opentelemetry/instrumentation-cassandra-driver` to the default set + of instrumentations. See + ## v0.6.0 diff --git a/packages/opentelemetry-node/docs/supported-technologies.md b/packages/opentelemetry-node/docs/supported-technologies.md index 5c1f0ae2..214c610c 100644 --- a/packages/opentelemetry-node/docs/supported-technologies.md +++ b/packages/opentelemetry-node/docs/supported-technologies.md @@ -38,6 +38,7 @@ requires: | `@elastic/opentelemetry-instrumentation-openai` | `openai` version range `>=4.19.0 <5` | [README](https://github.com/elastic/elastic-otel-node/tree/main/packages/instrumentation-openai#readme) | | `@opentelemetry/instrumentation-aws-sdk` | `aws-sdk` v2 and `@aws-sdk/client-*` v3 | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-aws-sdk#readme) | | `@opentelemetry/instrumentation-bunyan` | `bunyan` version range `^1.0.0` | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-bunyan#readme) | +| `@opentelemetry/instrumentation-cassandra-driver` | `cassandra-driver` version range `>=4.4.0 <5` | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-cassandra#readme) | | `@opentelemetry/instrumentation-express` | `express` version range `^4.0.0` | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-express#readme) | | `@opentelemetry/instrumentation-fastify` | `fastify` version range `>=3 <5` | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-fastify#readme) | | `@opentelemetry/instrumentation-generic-pool` | `generic-pool` version range `2 - 2.3, ^2.4, >=3` | [README](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-generic-pool#readme) | diff --git a/packages/opentelemetry-node/lib/instrumentations.js b/packages/opentelemetry-node/lib/instrumentations.js index 793f40b7..0f650d37 100644 --- a/packages/opentelemetry-node/lib/instrumentations.js +++ b/packages/opentelemetry-node/lib/instrumentations.js @@ -28,6 +28,7 @@ * "@opentelemetry/instrumentation-aws-sdk": import('@opentelemetry/instrumentation-aws-sdk').AwsSdkInstrumentationConfig | InstrumentationFactory, * "@opentelemetry/instrumentation-bunyan": import('@opentelemetry/instrumentation-bunyan').BunyanInstrumentationConfig | InstrumentationFactory, * "@opentelemetry/instrumentation-connect": import('@opentelemetry/instrumentation').InstrumentationConfig | InstrumentationFactory, + * "@opentelemetry/instrumentation-cassandra-driver": import('@opentelemetry/instrumentation-cassandra-driver').CassandraDriverInstrumentation | InstrumentationFactory, * "@opentelemetry/instrumentation-cucumber": import('@opentelemetry/instrumentation-cucumber').CucumberInstrumentationConfig | InstrumentationFactory, * "@opentelemetry/instrumentation-dataloader": import('@opentelemetry/instrumentation-dataloader').DataloaderInstrumentationConfig | InstrumentationFactory, * "@opentelemetry/instrumentation-dns": import('@opentelemetry/instrumentation-dns').DnsInstrumentationConfig | InstrumentationFactory, @@ -68,6 +69,7 @@ const {OpenAIInstrumentation} = require('@elastic/opentelemetry-instrumentation- const {AwsInstrumentation} = require('@opentelemetry/instrumentation-aws-sdk'); const {BunyanInstrumentation} = require('@opentelemetry/instrumentation-bunyan'); const {ConnectInstrumentation} = require('@opentelemetry/instrumentation-connect'); +const {CassandraDriverInstrumentation} = require('@opentelemetry/instrumentation-cassandra-driver'); const {CucumberInstrumentation} = require('@opentelemetry/instrumentation-cucumber'); const {DataloaderInstrumentation} = require('@opentelemetry/instrumentation-dataloader'); const {DnsInstrumentation} = require('@opentelemetry/instrumentation-dns'); @@ -117,6 +119,7 @@ const INSTRUMENTATIONS = { '@opentelemetry/instrumentation-aws-sdk': (cfg) => new AwsInstrumentation(cfg), '@opentelemetry/instrumentation-bunyan': (cfg) => new BunyanInstrumentation(cfg), '@opentelemetry/instrumentation-connect': (cfg) => new ConnectInstrumentation(cfg), + '@opentelemetry/instrumentation-cassandra-driver': (cfg) => new CassandraDriverInstrumentation(cfg), '@opentelemetry/instrumentation-cucumber': (cfg) => new CucumberInstrumentation(cfg), '@opentelemetry/instrumentation-dataloader': (cfg) => new DataloaderInstrumentation(cfg), '@opentelemetry/instrumentation-dns': (cfg) => new DnsInstrumentation(cfg), diff --git a/packages/opentelemetry-node/package-lock.json b/packages/opentelemetry-node/package-lock.json index 193abe7b..b779de53 100644 --- a/packages/opentelemetry-node/package-lock.json +++ b/packages/opentelemetry-node/package-lock.json @@ -22,6 +22,7 @@ "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-aws-sdk": "^0.49.0", "@opentelemetry/instrumentation-bunyan": "^0.45.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.45.0", "@opentelemetry/instrumentation-connect": "^0.43.0", "@opentelemetry/instrumentation-cucumber": "^0.13.0", "@opentelemetry/instrumentation-dataloader": "^0.16.0", @@ -77,6 +78,7 @@ "@hapi/hapi": "^21.3.10", "@types/tape": "^5.6.4", "bunyan": "^1.8.15", + "cassandra-driver": "^4.7.2", "dotenv": "^16.4.5", "express": "^4.21.0", "fastify": "^5.1.0", @@ -2432,6 +2434,21 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.45.0.tgz", + "integrity": "sha512-IKoA0lLfF7EyIL85MfqzvfAa/Oz9zHNFXwzSiQ6Iqej89BMyOm3eYaAsyUDAvgiLG12M189temMMyRuR07YsZg==", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-connect": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", @@ -4319,6 +4336,16 @@ "@types/node": "*" } }, + "node_modules/@types/long": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", + "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", + "deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "long": "*" + } + }, "node_modules/@types/memcached": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", @@ -4489,6 +4516,15 @@ "acorn": "^8" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4904,6 +4940,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cassandra-driver": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz", + "integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==", + "dev": true, + "dependencies": { + "@types/long": "~5.0.0", + "@types/node": ">=8", + "adm-zip": "~0.5.10", + "long": "~5.2.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", @@ -10965,6 +11016,15 @@ } } }, + "@opentelemetry/instrumentation-cassandra-driver": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.45.0.tgz", + "integrity": "sha512-IKoA0lLfF7EyIL85MfqzvfAa/Oz9zHNFXwzSiQ6Iqej89BMyOm3eYaAsyUDAvgiLG12M189temMMyRuR07YsZg==", + "requires": { + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + } + }, "@opentelemetry/instrumentation-connect": { "version": "0.43.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", @@ -12325,6 +12385,15 @@ "@types/node": "*" } }, + "@types/long": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", + "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", + "dev": true, + "requires": { + "long": "*" + } + }, "@types/memcached": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", @@ -12481,6 +12550,12 @@ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "requires": {} }, + "adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -12777,6 +12852,18 @@ "set-function-length": "^1.2.1" } }, + "cassandra-driver": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz", + "integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==", + "dev": true, + "requires": { + "@types/long": "~5.0.0", + "@types/node": ">=8", + "adm-zip": "~0.5.10", + "long": "~5.2.3" + } + }, "cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index fc2cf16a..7248646e 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -80,6 +80,7 @@ "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-aws-sdk": "^0.49.0", "@opentelemetry/instrumentation-bunyan": "^0.45.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.45.0", "@opentelemetry/instrumentation-connect": "^0.43.0", "@opentelemetry/instrumentation-cucumber": "^0.13.0", "@opentelemetry/instrumentation-dataloader": "^0.16.0", @@ -135,6 +136,7 @@ "@hapi/hapi": "^21.3.10", "@types/tape": "^5.6.4", "bunyan": "^1.8.15", + "cassandra-driver": "^4.7.2", "dotenv": "^16.4.5", "express": "^4.21.0", "fastify": "^5.1.0", diff --git a/packages/opentelemetry-node/test/docker-compose.yaml b/packages/opentelemetry-node/test/docker-compose.yaml index a1d8d0d8..9436e437 100644 --- a/packages/opentelemetry-node/test/docker-compose.yaml +++ b/packages/opentelemetry-node/test/docker-compose.yaml @@ -86,3 +86,16 @@ services: interval: 1s timeout: 10s retries: 30 + + cassandra: + image: cassandra:3 + environment: + MAX_HEAP_SIZE: "1G" + HEAP_NEWSIZE: 400m + ports: + - "9042:9042" + healthcheck: + test: ["CMD-SHELL", "[ $$(nodetool statusgossip) = running ]"] + interval: 1s + timeout: 10s + retries: 30 diff --git a/packages/opentelemetry-node/test/fixtures/use-cassandra-driver.js b/packages/opentelemetry-node/test/fixtures/use-cassandra-driver.js new file mode 100644 index 00000000..d4440c5d --- /dev/null +++ b/packages/opentelemetry-node/test/fixtures/use-cassandra-driver.js @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ + +// Usage: node -r @elastic/opentelemetry-node use-cassandra-driver.js + +const cassandra = require('cassandra-driver'); +const otel = require('@opentelemetry/api'); + +const host = process.env.CASSANDRA_HOST; +const port = process.env.CASSANDRA_PORT || '9042'; +const keyspace = 'keyspace1'; +const table = 'table1'; + +async function main() { + const client = new cassandra.Client({ + contactPoints: [`${host}:${port}`], + localDataCenter: 'datacenter1', + }); + + await client.connect(); + await client.execute( + `CREATE KEYSPACE IF NOT EXISTS ${keyspace} WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 1 };` + ); + await client.execute(`USE ${keyspace}`); + await client.execute( + `CREATE TABLE IF NOT EXISTS ${keyspace}.${table}(id uuid,text varchar,PRIMARY KEY(id));` + ); + await client.batch([ + { + query: `INSERT INTO ${keyspace}.${table} (id, text) VALUES (uuid(), ?)`, + params: ['value1'], + }, + { + query: `INSERT INTO ${keyspace}.${table} (id, text) VALUES (uuid(), ?)`, + params: ['value2'], + }, + ]); + await client.execute(`DROP TABLE IF EXISTS ${keyspace}.${table}`); + await client.shutdown(); +} +const tracer = otel.trace.getTracer('test'); +tracer.startActiveSpan('manual-span', async (span) => { + await main(); + span.end(); +}); diff --git a/packages/opentelemetry-node/test/instr-cassandra-driver.test.js b/packages/opentelemetry-node/test/instr-cassandra-driver.test.js new file mode 100644 index 00000000..42228901 --- /dev/null +++ b/packages/opentelemetry-node/test/instr-cassandra-driver.test.js @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ + +const test = require('tape'); +const {filterOutDnsNetSpans, runTestFixtures} = require('./testutils'); + +// TODO: check https://github.com/elastic/apm-agent-nodejs/blob/main/test/_is_cassandra_incompat.js +const skip = process.env.CASSANDRA_HOST === undefined; +if (skip) { + console.log( + '# SKIP cassandra-driver tests: CASSANDRA_HOST is not set (try with `CASSANDRA_HOST=localhost`)' + ); +} + +/** @type {import('./testutils').TestFixture[]} */ +const testFixtures = [ + { + name: 'use-cassandra-driver.js', + args: ['./fixtures/use-cassandra-driver.js'], + cwd: __dirname, + env: { + NODE_OPTIONS: '--require=@elastic/opentelemetry-node', + }, + // verbose: true, + checkTelemetry: (t, col) => { + // Expected a trace like this: + // ------ trace 15ca3e (8 spans) ------ + // span 3d3aa5 "manual-span" (402.5ms, SPAN_KIND_INTERNAL) + // +27ms `- span b8ab8f "tcp.connect" (0.5ms, SPAN_KIND_INTERNAL) + // +17ms `- span 6c9179 "tcp.connect" (0.4ms, SPAN_KIND_INTERNAL) + // +25ms `- span 97c330 "cassandra-driver.execute" (2.7ms, SPAN_KIND_CLIENT) + // +2ms `- span 91e594 "cassandra-driver.execute" (1.6ms, SPAN_KIND_CLIENT) + // +2ms `- span 9fbb84 "cassandra-driver.execute" (88.3ms, SPAN_KIND_CLIENT) + // +88ms `- span ef48b9 "cassandra-driver.batch" (5.7ms, SPAN_KIND_CLIENT) + // +6ms `- span fa5471 "cassandra-driver.execute" (232.5ms, SPAN_KIND_CLIENT) + const spans = filterOutDnsNetSpans(col.sortedSpans); + t.equal(spans.length, 6); + spans.slice(1).forEach((s) => { + t.equal(s.traceId, spans[0].traceId, 'traceId'); + t.equal(s.parentSpanId, spans[0].spanId, 'parentSpanId'); + t.equal(s.kind, 'SPAN_KIND_CLIENT', 'kind'); + t.equal( + s.scope.name, + '@opentelemetry/instrumentation-cassandra-driver' + ); + t.equal(s.attributes['db.system'], 'cassandra'); + }); + t.equal(spans[1].name, 'cassandra-driver.execute'); + t.equal(spans[2].name, 'cassandra-driver.execute'); + t.equal(spans[3].name, 'cassandra-driver.execute'); + t.equal(spans[4].name, 'cassandra-driver.batch'); + t.equal(spans[5].name, 'cassandra-driver.execute'); + }, + }, +]; + +test('cassandra-driver instrumentation', {skip}, (suite) => { + runTestFixtures(suite, testFixtures); + suite.end(); +}); diff --git a/packages/opentelemetry-node/test/test-services.env b/packages/opentelemetry-node/test/test-services.env index 973c96f4..0664e6c4 100644 --- a/packages/opentelemetry-node/test/test-services.env +++ b/packages/opentelemetry-node/test/test-services.env @@ -3,6 +3,7 @@ PGHOST=localhost MONGODB_HOST=localhost MSSQL_HOST=localhost MYSQL_HOST=localhost +CASSANDRA_HOST=localhost OPENAI_BASE_URL=http://127.0.0.1:11434/v1 OPENAI_API_KEY=notused diff --git a/packages/opentelemetry-node/types/instrumentations.d.ts b/packages/opentelemetry-node/types/instrumentations.d.ts index d7d7b31c..6aba262e 100644 --- a/packages/opentelemetry-node/types/instrumentations.d.ts +++ b/packages/opentelemetry-node/types/instrumentations.d.ts @@ -5,6 +5,7 @@ export type InstrumentaionsMap = { "@opentelemetry/instrumentation-aws-sdk": import('@opentelemetry/instrumentation-aws-sdk').AwsSdkInstrumentationConfig | InstrumentationFactory; "@opentelemetry/instrumentation-bunyan": import('@opentelemetry/instrumentation-bunyan').BunyanInstrumentationConfig | InstrumentationFactory; "@opentelemetry/instrumentation-connect": import('@opentelemetry/instrumentation').InstrumentationConfig | InstrumentationFactory; + "@opentelemetry/instrumentation-cassandra-driver": import('@opentelemetry/instrumentation-cassandra-driver').CassandraDriverInstrumentation | InstrumentationFactory; "@opentelemetry/instrumentation-cucumber": import('@opentelemetry/instrumentation-cucumber').CucumberInstrumentationConfig | InstrumentationFactory; "@opentelemetry/instrumentation-dataloader": import('@opentelemetry/instrumentation-dataloader').DataloaderInstrumentationConfig | InstrumentationFactory; "@opentelemetry/instrumentation-dns": import('@opentelemetry/instrumentation-dns').DnsInstrumentationConfig | InstrumentationFactory;