From 9913a000c9cd5b3c07a5a3d64b5ad88c3f8f77b2 Mon Sep 17 00:00:00 2001 From: Haddas Bronfman <85441461+haddasbronfman@users.noreply.github.com> Date: Sun, 18 Sep 2022 10:41:15 +0300 Subject: [PATCH 01/19] chore(mysql): migrate mysql examples (#1168) Co-authored-by: Osher Vaknin <81672378+osherv@users.noreply.github.com> --- examples/mysql/package.json | 46 --------- examples/mysql/tracer.js | 34 ------- .../.eslintignore | 1 + .../README.md | 2 +- .../examples}/README.md | 8 +- .../examples}/images/jaeger-ui.png | Bin .../examples}/images/zipkin-ui.png | Bin .../examples/package.json | 51 ++++++++++ .../examples/src/client.ts | 14 +-- .../examples/src/server.ts | 91 ++++++++++-------- .../examples/src/tracer.ts | 40 ++++++++ .../examples/tsconfig.json | 10 ++ .../package.json | 3 +- 13 files changed, 167 insertions(+), 133 deletions(-) delete mode 100644 examples/mysql/package.json delete mode 100644 examples/mysql/tracer.js rename {examples/mysql => plugins/node/opentelemetry-instrumentation-mysql/examples}/README.md (95%) rename {examples/mysql => plugins/node/opentelemetry-instrumentation-mysql/examples}/images/jaeger-ui.png (100%) rename {examples/mysql => plugins/node/opentelemetry-instrumentation-mysql/examples}/images/zipkin-ui.png (100%) create mode 100644 plugins/node/opentelemetry-instrumentation-mysql/examples/package.json rename examples/mysql/client.js => plugins/node/opentelemetry-instrumentation-mysql/examples/src/client.ts (88%) rename examples/mysql/server.js => plugins/node/opentelemetry-instrumentation-mysql/examples/src/server.ts (50%) create mode 100644 plugins/node/opentelemetry-instrumentation-mysql/examples/src/tracer.ts create mode 100644 plugins/node/opentelemetry-instrumentation-mysql/examples/tsconfig.json diff --git a/examples/mysql/package.json b/examples/mysql/package.json deleted file mode 100644 index 10c4f80c1f..0000000000 --- a/examples/mysql/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "mysql-example", - "private": true, - "version": "0.23.0", - "description": "Example of mysql integration with OpenTelemetry", - "main": "index.js", - "scripts": { - "zipkin:server": "cross-env EXPORTER=zipkin node ./server.js", - "zipkin:client": "cross-env EXPORTER=zipkin node ./client.js", - "jaeger:server": "cross-env EXPORTER=jaeger node ./server.js", - "jaeger:client": "cross-env EXPORTER=jaeger node ./client.js" - }, - "repository": { - "type": "git", - "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" - }, - "keywords": [ - "opentelemetry", - "mysql", - "tracing" - ], - "engines": { - "node": ">=8" - }, - "author": "OpenTelemetry Authors", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/open-telemetry/opentelemetry-js/issues" - }, - "dependencies": { - "@opentelemetry/api": "^1.0.2", - "@opentelemetry/exporter-jaeger": "^0.25.0", - "@opentelemetry/exporter-zipkin": "^0.25.0", - "@opentelemetry/instrumentation": "^0.25.0", - "@opentelemetry/instrumentation-http": "^0.25.0", - "@opentelemetry/instrumentation-mysql": "^0.23.0", - "@opentelemetry/sdk-trace-node": "^0.25.0", - "@opentelemetry/sdk-trace-base": "^0.25.0", - "mysql": "*" - }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", - "devDependencies": { - "@types/mysql": "*", - "cross-env": "^6.0.0" - } -} diff --git a/examples/mysql/tracer.js b/examples/mysql/tracer.js deleted file mode 100644 index 79147d0a5f..0000000000 --- a/examples/mysql/tracer.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const opentelemetry = require('@opentelemetry/api'); -const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); -const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); -const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); -const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); -const { registerInstrumentations } = require('@opentelemetry/instrumentation'); -const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); -const { MySQLInstrumentation } = require('@opentelemetry/instrumentation-mysql'); - -module.exports = (serviceName) => { - const provider = new NodeTracerProvider(); - - provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter({ - serviceName, - }))); - provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter({ - serviceName, - }))); - - // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings - provider.register(); - - registerInstrumentations({ - instrumentations: [ - new HttpInstrumentation(), - new MySQLInstrumentation(), - ], - tracerProvider: provider, - }); - - return opentelemetry.trace.getTracer('mysql-example'); -}; diff --git a/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore b/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore index 378eac25d3..514cc95d43 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore +++ b/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore @@ -1 +1,2 @@ build +examples diff --git a/plugins/node/opentelemetry-instrumentation-mysql/README.md b/plugins/node/opentelemetry-instrumentation-mysql/README.md index f7f338a1a7..94074d0448 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql/README.md +++ b/plugins/node/opentelemetry-instrumentation-mysql/README.md @@ -40,7 +40,7 @@ registerInstrumentations({ }) ``` -See [examples/mysql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/mysql) for a short example. +See [examples](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql/examples) for a short example. ## Useful links diff --git a/examples/mysql/README.md b/plugins/node/opentelemetry-instrumentation-mysql/examples/README.md similarity index 95% rename from examples/mysql/README.md rename to plugins/node/opentelemetry-instrumentation-mysql/examples/README.md index 6b8348d342..7b3180e372 100644 --- a/examples/mysql/README.md +++ b/plugins/node/opentelemetry-instrumentation-mysql/examples/README.md @@ -27,14 +27,14 @@ Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/ ```sh # from this directory - npm run server + npm run zipkin:server ``` - Run the client ```sh # from this directory - npm run client + npm run zipkin:client ``` #### Zipkin UI @@ -50,14 +50,14 @@ Go to Zipkin with your browser =8" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/exporter-jaeger": "^1.0.0", + "@opentelemetry/exporter-zipkin": "^1.0.0", + "@opentelemetry/instrumentation": "^0.32.0", + "@opentelemetry/instrumentation-http": "^0.32.0", + "@opentelemetry/instrumentation-mysql": "^0.31.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "@opentelemetry/sdk-trace-node": "^1.0.0", + "mysql": "^2.18.1" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": { + "@types/mysql": "^2.15.21", + "cross-env": "^6.0.0", + "ts-node": "^10.6.0", + "typescript": "4.3.5" + } +} diff --git a/examples/mysql/client.js b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/client.ts similarity index 88% rename from examples/mysql/client.js rename to plugins/node/opentelemetry-instrumentation-mysql/examples/src/client.ts index f67025c510..bdbb43649b 100644 --- a/examples/mysql/client.js +++ b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/client.ts @@ -1,9 +1,9 @@ 'use strict'; -const api = require('@opentelemetry/api'); -const tracer = require('./tracer')('example-mysql-http-client'); -// eslint-disable-next-line import/order -const http = require('http'); +import * as api from '@opentelemetry/api'; +import { setupTracing } from "./tracer"; +const tracer = setupTracing('example-mysql-client'); +import * as http from 'http'; /** A function which makes requests and handles response. */ function makeRequest() { @@ -22,7 +22,7 @@ function makeRequest() { port: 8080, path: '/connection/query', }, (response) => { - const body = []; + const body: any[] = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; @@ -38,7 +38,7 @@ function makeRequest() { port: 8080, path: '/pool/query', }, (response) => { - const body = []; + const body: any[] = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; @@ -54,7 +54,7 @@ function makeRequest() { port: 8080, path: '/cluster/query', }, (response) => { - const body = []; + const body: any[] = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; diff --git a/examples/mysql/server.js b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/server.ts similarity index 50% rename from examples/mysql/server.js rename to plugins/node/opentelemetry-instrumentation-mysql/examples/src/server.ts index 1599327a49..36d1083a8c 100644 --- a/examples/mysql/server.js +++ b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/server.ts @@ -1,23 +1,24 @@ 'use strict'; // eslint-disable-next-line import/order -const tracer = require('./tracer')('example-mysql-http-server'); -const api = require('@opentelemetry/api'); -const mysql = require('mysql'); -const http = require('http'); +import { setupTracing } from "./tracer"; +setupTracing('example-mysql-server'); +import * as api from '@opentelemetry/api'; +import * as mysql from 'mysql' +import * as http from 'http'; +import { MysqlError } from "mysql"; +import { PoolConnection } from "mysql"; const pool = mysql.createPool({ host: 'localhost', user: 'root', password: 'secret', - database: 'my_db', }); const connection = mysql.createConnection({ host: 'localhost', user: 'root', password: 'secret', - database: 'my_db', }); const cluster = mysql.createPoolCluster(); @@ -26,34 +27,34 @@ cluster.add({ host: 'localhost', user: 'root', password: 'secret', - database: 'my_db', }); /** Starts a HTTP server that receives requests on sample server port. */ -function startServer(port) { +function startServer(port: number | undefined) { // Creates a server const server = http.createServer(handleRequest); // Starts the server - server.listen(port, (err) => { - if (err) { - throw err; - } + server.listen(port, () => { console.log(`Node HTTP listening on ${port}`); }); } /** A function which handles requests and send response. */ -function handleRequest(request, response) { - const currentSpan = tracer.getCurrentSpan(); +function handleRequest(request: any, response: any) { + const currentSpan = api.trace.getSpan(api.context.active()) // display traceid in the terminal - const { traceId } = currentSpan.spanContext(); + const traceId = currentSpan?.spanContext(); console.log(`traceid: ${traceId}`); console.log(`Jaeger URL: http://localhost:16686/trace/${traceId}`); console.log(`Zipkin URL: http://localhost:9411/zipkin/traces/${traceId}`); try { const body = []; - request.on('error', (err) => console.log(err)); - request.on('data', (chunk) => body.push(chunk)); + request.on('error', + (err: any) => console.log(err) + ); + request.on('data', + (chunk: any) => body.push(chunk) + ); request.on('end', () => { if (request.url === '/connection/query') { handleConnectionQuery(response); @@ -72,22 +73,27 @@ function handleRequest(request, response) { startServer(8080); -function handlePoolQuery(response) { +function handlePoolQuery(response: any) { const query = 'SELECT 1 + 1 as pool_solution'; - pool.getConnection((connErr, conn, _fields) => { - conn.query(query, (err, results) => { - tracer.getCurrentSpan().addEvent('results'); - if (err) { - console.log('Error code:', err.code); - response.end(err.message); - } else { - response.end(`${query}: ${results[0].pool_solution}`); - } - }); + pool.getConnection((connErr: MysqlError, conn: PoolConnection) => { + if (connErr) { + console.log('Error connection: ', connErr.message); + response.end(connErr.message); + } else { + conn.query(query, (err, results) => { + api.trace.getSpan(api.context.active())?.addEvent('results'); + if (err) { + console.log('Error code:', err.code); + response.end(err.message); + } else { + response.end(`${query}: ${results[0].pool_solution}`); + } + }); + } }); } -function handleConnectionQuery(response) { +function handleConnectionQuery(response: any) { const query = 'SELECT 1 + 1 as solution'; connection.query(query, (err, results, _fields) => { if (err) { @@ -99,21 +105,26 @@ function handleConnectionQuery(response) { }); } -function handleClusterQuery(response) { +function handleClusterQuery(response: any) { const query = 'SELECT 1 + 1 as cluster_solution'; cluster.getConnection((connErr, conn) => { - conn.query(query, (err, results, _fields) => { - api.trace.getSpan(api.context.active()).addEvent('results'); - if (err) { - console.log('Error code:', err.code); - response.end(err.message); - } else { - response.end(`${query}: ${results[0].cluster_solution}`); - } - }); + if (connErr) { + console.log('Error connection: ', connErr.message); + response.end(connErr.message); + } else { + conn.query(query, (err, results, _fields) => { + api.trace.getSpan(api.context.active())?.addEvent('results'); + if (err) { + console.log('Error code:', err.code); + response.end(err.message); + } else { + response.end(`${query}: ${results[0].cluster_solution}`); + } + }); + } }); } -function handleNotFound(response) { +function handleNotFound(response: any) { response.end('not found'); } diff --git a/plugins/node/opentelemetry-instrumentation-mysql/examples/src/tracer.ts b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/tracer.ts new file mode 100644 index 0000000000..7f44c5b216 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/examples/src/tracer.ts @@ -0,0 +1,40 @@ +'use strict'; + +import opentelemetry from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +const EXPORTER = process.env.EXPORTER || ''; + +export const setupTracing = (serviceName: string) => { + const provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }),}); + + if (EXPORTER.toLowerCase().startsWith('z')) { + provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter())); + } else { + provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter())); + } + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register(); + + registerInstrumentations({ + instrumentations: [ + new HttpInstrumentation(), + new MySQLInstrumentation(), + ], + tracerProvider: provider, + }); + + return opentelemetry.trace.getTracer('mysql-example'); +}; diff --git a/plugins/node/opentelemetry-instrumentation-mysql/examples/tsconfig.json b/plugins/node/opentelemetry-instrumentation-mysql/examples/tsconfig.json new file mode 100644 index 0000000000..5a768b7e2c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/examples/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + }, + "include": [ + "src/**/*.ts", + ] +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/package.json b/plugins/node/opentelemetry-instrumentation-mysql/package.json index 80fb32b541..7f20c7c3ce 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql/package.json +++ b/plugins/node/opentelemetry-instrumentation-mysql/package.json @@ -15,7 +15,8 @@ "prepare": "npm run compile", "tdd": "npm run test -- --watch-extensions ts --watch", "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", - "version:update": "node ../../../scripts/version-update.js" + "version:update": "node ../../../scripts/version-update.js", + "compile:examples": "cd examples && npm run compile" }, "keywords": [ "instrumentation", From ea9eea33ec907de90a7eb18eb1c020282db1aa94 Mon Sep 17 00:00:00 2001 From: Haddas Bronfman <85441461+haddasbronfman@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:19:38 +0300 Subject: [PATCH 02/19] chore(redis): migrate redis examples (#1156) * chore(redis): migrate redis examples * Update package.json * chore(redis): lock typscript to version 4.3.5 * chore(redis): lint * chore(redis): change version of redis from 31 to 32 * chore(redis): add service name to Resource * chore(redis): remove original redis example * fix(redis): fixes Co-authored-by: Osher Vaknin <81672378+osherv@users.noreply.github.com> --- examples/redis/package.json | 49 ---------------- examples/redis/tracer.js | 42 -------------- .../.eslintignore | 1 + .../README.md | 2 +- .../examples}/README.md | 0 .../examples}/images/jaeger.jpg | Bin .../examples}/images/zipkin.jpg | Bin .../examples/package.json | 53 ++++++++++++++++++ .../examples/src/client.ts | 14 +++-- .../examples/src/express-tracer-handlers.ts | 20 +++---- .../examples/src/server.ts | 16 +++--- .../examples/src/setup-redis.ts | 4 +- .../examples/src/tracer.ts | 43 ++++++++++++++ .../examples/tsconfig.json | 10 ++++ .../package.json | 1 + 15 files changed, 136 insertions(+), 119 deletions(-) delete mode 100644 examples/redis/package.json delete mode 100644 examples/redis/tracer.js rename {examples/redis => plugins/node/opentelemetry-instrumentation-redis/examples}/README.md (100%) rename {examples/redis => plugins/node/opentelemetry-instrumentation-redis/examples}/images/jaeger.jpg (100%) rename {examples/redis => plugins/node/opentelemetry-instrumentation-redis/examples}/images/zipkin.jpg (100%) create mode 100644 plugins/node/opentelemetry-instrumentation-redis/examples/package.json rename examples/redis/client.js => plugins/node/opentelemetry-instrumentation-redis/examples/src/client.ts (60%) rename examples/redis/express-tracer-handlers.js => plugins/node/opentelemetry-instrumentation-redis/examples/src/express-tracer-handlers.ts (52%) rename examples/redis/server.js => plugins/node/opentelemetry-instrumentation-redis/examples/src/server.ts (77%) rename examples/redis/setup-redis.js => plugins/node/opentelemetry-instrumentation-redis/examples/src/setup-redis.ts (71%) create mode 100644 plugins/node/opentelemetry-instrumentation-redis/examples/src/tracer.ts create mode 100644 plugins/node/opentelemetry-instrumentation-redis/examples/tsconfig.json diff --git a/examples/redis/package.json b/examples/redis/package.json deleted file mode 100644 index 9dd6ab8bdf..0000000000 --- a/examples/redis/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "redis-example", - "private": true, - "version": "0.23.0", - "description": "Example of HTTP integration with OpenTelemetry", - "main": "index.js", - "scripts": { - "docker:start": "docker run -d -p 6379:6379 --name otjsredis redis:alpine", - "docker:stop": "docker stop otjsredis && docker rm otjsredis", - "zipkin:server": "cross-env EXPORTER=zipkin node ./server.js", - "zipkin:client": "cross-env EXPORTER=zipkin node ./client.js", - "jaeger:server": "cross-env EXPORTER=jaeger node ./server.js", - "jaeger:client": "cross-env EXPORTER=jaeger node ./client.js" - }, - "repository": { - "type": "git", - "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" - }, - "keywords": [ - "opentelemetry", - "redis", - "tracing" - ], - "engines": { - "node": ">=8" - }, - "author": "OpenTelemetry Authors", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/open-telemetry/opentelemetry-js/issues" - }, - "dependencies": { - "@opentelemetry/api": "^1.0.2", - "@opentelemetry/exporter-jaeger": "^0.25.0", - "@opentelemetry/exporter-zipkin": "^0.25.0", - "@opentelemetry/instrumentation": "^0.25.0", - "@opentelemetry/instrumentation-http": "^0.25.0", - "@opentelemetry/instrumentation-redis": "^0.23.0", - "@opentelemetry/sdk-trace-node": "^0.25.0", - "@opentelemetry/sdk-trace-base": "^0.25.0", - "axios": "^0.21.1", - "express": "^4.17.1", - "redis": "^2.8.0" - }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", - "devDependencies": { - "cross-env": "^6.0.0" - } -} diff --git a/examples/redis/tracer.js b/examples/redis/tracer.js deleted file mode 100644 index a41cde6545..0000000000 --- a/examples/redis/tracer.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const opentelemetry = require('@opentelemetry/api'); -const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); -const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); -const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); -const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); -const { registerInstrumentations } = require('@opentelemetry/instrumentation'); -const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); -const { RedisInstrumentation } = require('@opentelemetry/instrumentation-redis'); - -const EXPORTER = process.env.EXPORTER || ''; - -module.exports = (serviceName) => { - const provider = new NodeTracerProvider(); - - let exporter; - if (EXPORTER.toLowerCase().startsWith('z')) { - exporter = new ZipkinExporter({ - serviceName, - }); - } else { - exporter = new JaegerExporter({ - serviceName, - }); - } - - provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - - // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings - provider.register(); - - registerInstrumentations({ - instrumentations: [ - new HttpInstrumentation(), - new RedisInstrumentation(), - ], - tracerProvider: provider, - }); - - return opentelemetry.trace.getTracer('redis-example'); -}; diff --git a/plugins/node/opentelemetry-instrumentation-redis/.eslintignore b/plugins/node/opentelemetry-instrumentation-redis/.eslintignore index 378eac25d3..514cc95d43 100644 --- a/plugins/node/opentelemetry-instrumentation-redis/.eslintignore +++ b/plugins/node/opentelemetry-instrumentation-redis/.eslintignore @@ -1 +1,2 @@ build +examples diff --git a/plugins/node/opentelemetry-instrumentation-redis/README.md b/plugins/node/opentelemetry-instrumentation-redis/README.md index d730044a87..4386a95da7 100644 --- a/plugins/node/opentelemetry-instrumentation-redis/README.md +++ b/plugins/node/opentelemetry-instrumentation-redis/README.md @@ -41,7 +41,7 @@ registerInstrumentations({ }) ``` -See [examples/redis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/redis) for a short example. +See [examples](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-redis/examples) for a short example. ### Redis Instrumentation Options diff --git a/examples/redis/README.md b/plugins/node/opentelemetry-instrumentation-redis/examples/README.md similarity index 100% rename from examples/redis/README.md rename to plugins/node/opentelemetry-instrumentation-redis/examples/README.md diff --git a/examples/redis/images/jaeger.jpg b/plugins/node/opentelemetry-instrumentation-redis/examples/images/jaeger.jpg similarity index 100% rename from examples/redis/images/jaeger.jpg rename to plugins/node/opentelemetry-instrumentation-redis/examples/images/jaeger.jpg diff --git a/examples/redis/images/zipkin.jpg b/plugins/node/opentelemetry-instrumentation-redis/examples/images/zipkin.jpg similarity index 100% rename from examples/redis/images/zipkin.jpg rename to plugins/node/opentelemetry-instrumentation-redis/examples/images/zipkin.jpg diff --git a/plugins/node/opentelemetry-instrumentation-redis/examples/package.json b/plugins/node/opentelemetry-instrumentation-redis/examples/package.json new file mode 100644 index 0000000000..70a8d7441a --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/package.json @@ -0,0 +1,53 @@ +{ + "name": "redis-example", + "private": true, + "version": "0.23.0", + "description": "Example of HTTP integration with OpenTelemetry", + "main": "index.js", + "scripts": { + "docker:start": "docker run -d -p 6379:6379 --name otel-redis redis:alpine", + "docker:stop": "docker stop otel-redis && docker rm otel-redis", + "zipkin:server": "cross-env EXPORTER=zipkin ts-node src/server.ts", + "zipkin:client": "cross-env EXPORTER=zipkin ts-node src/client.ts", + "jaeger:server": "cross-env EXPORTER=jaeger ts-node src/server.ts", + "jaeger:client": "cross-env EXPORTER=jaeger ts-node src/client.ts", + "compile": "tsc -p ." + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git" + }, + "keywords": [ + "opentelemetry", + "redis", + "tracing" + ], + "engines": { + "node": ">=8" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "dependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/exporter-jaeger": "^1.0.0", + "@opentelemetry/exporter-zipkin": "^1.0.0", + "@opentelemetry/instrumentation": "^0.32.0", + "@opentelemetry/instrumentation-http": "^0.32.0", + "@opentelemetry/instrumentation-redis": "^0.32.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "@opentelemetry/sdk-trace-node": "^1.0.0", + "axios": "^0.21.1", + "express": "^4.17.1", + "redis": "^2.8.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js#readme", + "devDependencies": { + "@types/express": "^4.17.14", + "cross-env": "^6.0.0", + "ts-node": "^10.6.0", + "typescript": "4.3.5" + } +} diff --git a/examples/redis/client.js b/plugins/node/opentelemetry-instrumentation-redis/examples/src/client.ts similarity index 60% rename from examples/redis/client.js rename to plugins/node/opentelemetry-instrumentation-redis/examples/src/client.ts index 8e9f3f461a..4f83690393 100644 --- a/examples/redis/client.js +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/src/client.ts @@ -1,9 +1,9 @@ 'use strict'; -// eslint-disable-next-line import/order -const tracer = require('./tracer')('example-redis-client'); -const api = require('@opentelemetry/api'); -const axios = require('axios').default; +import { setupTracing } from "./tracer"; +const tracer = setupTracing('example-redis-client'); +import * as api from '@opentelemetry/api'; +import { default as axios } from 'axios'; function makeRequest() { const span = tracer.startSpan('client.makeRequest()', { @@ -13,10 +13,12 @@ function makeRequest() { api.context.with(api.trace.setSpan(api.ROOT_CONTEXT, span), async () => { try { const res = await axios.get('http://localhost:8080/run_test'); - span.setStatus({ code: api.StatusCode.OK }); + span.setStatus({ code: api.SpanStatusCode.OK }); console.log(res.statusText); } catch (e) { - span.setStatus({ code: api.StatusCode.ERROR, message: e.message }); + if(e instanceof Error) { + span.setStatus({ code: api.SpanStatusCode.ERROR, message: e.message }); + } } span.end(); console.log('Sleeping 5 seconds before shutdown to ensure all records are flushed.'); diff --git a/examples/redis/express-tracer-handlers.js b/plugins/node/opentelemetry-instrumentation-redis/examples/src/express-tracer-handlers.ts similarity index 52% rename from examples/redis/express-tracer-handlers.js rename to plugins/node/opentelemetry-instrumentation-redis/examples/src/express-tracer-handlers.ts index 7331a67754..8caed1357b 100644 --- a/examples/redis/express-tracer-handlers.js +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/src/express-tracer-handlers.ts @@ -1,16 +1,16 @@ 'use strict'; -const api = require('@opentelemetry/api'); +import * as api from '@opentelemetry/api'; -function getMiddlewareTracer(tracer) { - return (req, res, next) => { +export function getMiddlewareTracer(tracer: api.Tracer) { + return (req: any, res: any, next: any) => { const span = tracer.startSpan(`express.middleware.tracer(${req.method} ${req.path})`, { kind: api.SpanKind.SERVER, }); // End this span before sending out the response const originalSend = res.send; - res.send = function send(...args) { + res.send = function send(...args: any[]) { span.end(); originalSend.apply(res, args); }; @@ -19,17 +19,15 @@ function getMiddlewareTracer(tracer) { }; } -function getErrorTracer(tracer) { - return (err, _req, res, _next) => { +export function getErrorTracer(tracer: api.Tracer) { + return (err: any, _req: any, res: any, _next: any) => { console.error('Caught error', err.message); - const span = tracer.getCurrentSpan(); + const span = api.trace.getSpan(api.context.active()) + if (span) { - span.setStatus({ code: api.StatusCode.ERROR, message: err.message }); + span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message }); } res.status(500).send(err.message); }; } -module.exports = { - getMiddlewareTracer, getErrorTracer, -}; diff --git a/examples/redis/server.js b/plugins/node/opentelemetry-instrumentation-redis/examples/src/server.ts similarity index 77% rename from examples/redis/server.js rename to plugins/node/opentelemetry-instrumentation-redis/examples/src/server.ts index f293084e2d..3227fed8bf 100644 --- a/examples/redis/server.js +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/src/server.ts @@ -1,12 +1,12 @@ 'use strict'; -// eslint-disable-next-line import/order -const tracer = require('./tracer')('example-redis-server'); +import { setupTracing } from './tracer' +const tracer = setupTracing('example-redis-server'); // Require in rest of modules -const express = require('express'); -const axios = require('axios').default; -const tracerHandlers = require('./express-tracer-handlers'); +import * as express from 'express'; +import axios from 'axios'; +import * as tracerHandlers from './express-tracer-handlers'; const redisPromise = require('./setup-redis').redis; // Setup express @@ -19,7 +19,7 @@ const PORT = 8080; async function setupRoutes() { const redis = await redisPromise; - app.get('/run_test', async (req, res) => { + app.get('/run_test', async (req: express.Request, res: express.Response) => { const uuid = Math.random() .toString(36) .substring(2, 15) @@ -36,7 +36,7 @@ async function setupRoutes() { } }); - app.get('/:cmd', (req, res) => { + app.get('/:cmd', (req: any , res: any) => { if (!req.query.args) { res.status(400).send('No args provided'); return; @@ -44,7 +44,7 @@ async function setupRoutes() { const { cmd } = req.params; const args = req.query.args.split(','); - redis[cmd].call(redis, ...args, (err, result) => { + redis[cmd].call(redis, ...args, (err: any, result: any) => { if (err) { res.sendStatus(400); } else if (result) { diff --git a/examples/redis/setup-redis.js b/plugins/node/opentelemetry-instrumentation-redis/examples/src/setup-redis.ts similarity index 71% rename from examples/redis/setup-redis.js rename to plugins/node/opentelemetry-instrumentation-redis/examples/src/setup-redis.ts index 59b2e189b5..7c8984aaed 100644 --- a/examples/redis/setup-redis.js +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/src/setup-redis.ts @@ -1,8 +1,8 @@ 'use strict'; -const redis = require('redis'); +import {createClient} from 'redis'; -const client = redis.createClient('redis://localhost:6379'); +const client = createClient('redis://localhost:6379'); const redisPromise = new Promise(((resolve, reject) => { client.once('ready', () => { resolve(client); diff --git a/plugins/node/opentelemetry-instrumentation-redis/examples/src/tracer.ts b/plugins/node/opentelemetry-instrumentation-redis/examples/src/tracer.ts new file mode 100644 index 0000000000..e50633493a --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/src/tracer.ts @@ -0,0 +1,43 @@ +'use strict'; + +import * as api from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + +const EXPORTER = process.env.EXPORTER || ''; + +export const setupTracing = (serviceName: string) => { + const provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }),}); + + let exporter; + if (EXPORTER.toLowerCase().startsWith('z')) { + exporter = new ZipkinExporter(); + } else { + exporter = new JaegerExporter(); + } + + provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register(); + + registerInstrumentations({ + instrumentations: [ + new HttpInstrumentation(), + new RedisInstrumentation(), + ], + tracerProvider: provider, + }); + + return api.trace.getTracer(serviceName); +}; diff --git a/plugins/node/opentelemetry-instrumentation-redis/examples/tsconfig.json b/plugins/node/opentelemetry-instrumentation-redis/examples/tsconfig.json new file mode 100644 index 0000000000..5a768b7e2c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-redis/examples/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + }, + "include": [ + "src/**/*.ts", + ] +} diff --git a/plugins/node/opentelemetry-instrumentation-redis/package.json b/plugins/node/opentelemetry-instrumentation-redis/package.json index 115a29d379..995b88f5e0 100644 --- a/plugins/node/opentelemetry-instrumentation-redis/package.json +++ b/plugins/node/opentelemetry-instrumentation-redis/package.json @@ -21,6 +21,7 @@ "prewatch": "npm run precompile", "version:update": "node ../../../scripts/version-update.js", "compile": "tsc -p .", + "compile:examples": "cd examples && npm run compile", "prepare": "npm run compile" }, "keywords": [ From e0a2e8e4c48391415dd41668c580124f5421a790 Mon Sep 17 00:00:00 2001 From: Osher Vaknin <81672378+osherv@users.noreply.github.com> Date: Mon, 19 Sep 2022 12:30:32 +0300 Subject: [PATCH 03/19] chore(mongodb): migrate mongodb examples (#1160) --- examples/fastify/server.js | 1 - examples/graphql/tracer.js | 2 +- .../grpc-census-prop/capitalize_client.js | 6 +- .../grpc_dynamic_codegen/capitalize_client.js | 6 +- examples/mongodb/package.json | 45 -------- examples/mongodb/tracer.js | 36 ------ .../.eslintignore | 1 + .../examples}/README.md | 24 +++- .../examples/images/zipkin.png | Bin 0 -> 114237 bytes .../examples/package.json | 49 ++++++++ .../examples/src/client.ts | 16 +-- .../examples/src/server.ts | 105 ++++++++++-------- .../examples/src/tracer.ts | 38 +++++++ .../examples/src/utils.ts | 42 +++++++ .../examples/tsconfig.json | 10 ++ .../package.json | 1 + 16 files changed, 236 insertions(+), 146 deletions(-) delete mode 100644 examples/mongodb/package.json delete mode 100644 examples/mongodb/tracer.js rename {examples/mongodb => plugins/node/opentelemetry-instrumentation-mongodb/examples}/README.md (84%) create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/examples/images/zipkin.png create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json rename examples/mongodb/client.js => plugins/node/opentelemetry-instrumentation-mongodb/examples/src/client.ts (88%) rename examples/mongodb/server.js => plugins/node/opentelemetry-instrumentation-mongodb/examples/src/server.ts (52%) create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/examples/src/tracer.ts create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/examples/src/utils.ts create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/examples/tsconfig.json diff --git a/examples/fastify/server.js b/examples/fastify/server.js index deb99a4cd7..340ce6fa81 100644 --- a/examples/fastify/server.js +++ b/examples/fastify/server.js @@ -64,7 +64,6 @@ async function subsystem(fastify) { done(); }, 2000); }); - } app.post('/run_test/:id', async (req, res) => { diff --git a/examples/graphql/tracer.js b/examples/graphql/tracer.js index 8b804f4517..2545a20874 100644 --- a/examples/graphql/tracer.js +++ b/examples/graphql/tracer.js @@ -8,7 +8,7 @@ const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-http'); const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express'); const { Resource } = require('@opentelemetry/resources'); -const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); +const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); const provider = new NodeTracerProvider({ resource: new Resource({ diff --git a/examples/grpc-census-prop/capitalize_client.js b/examples/grpc-census-prop/capitalize_client.js index 1df13f1cf1..8a50846c72 100644 --- a/examples/grpc-census-prop/capitalize_client.js +++ b/examples/grpc-census-prop/capitalize_client.js @@ -25,8 +25,10 @@ const { Fetch } = grpc.load(PROTO_PATH).rpc; * Creates a gRPC client, makes a gRPC call and waits before shutting down */ function main() { - const client = new Fetch('localhost:50051', - grpc.credentials.createInsecure()); + const client = new Fetch( + 'localhost:50051', + grpc.credentials.createInsecure(), + ); const data = process.argv[2] || 'opentelemetry'; console.log('> ', data); diff --git a/examples/grpc_dynamic_codegen/capitalize_client.js b/examples/grpc_dynamic_codegen/capitalize_client.js index 8076c11754..6c0f29e9bc 100644 --- a/examples/grpc_dynamic_codegen/capitalize_client.js +++ b/examples/grpc_dynamic_codegen/capitalize_client.js @@ -15,8 +15,10 @@ const definition = protoLoader.loadSync(PROTO_PATH, PROTO_OPTIONS); const rpcProto = grpc.loadPackageDefinition(definition).rpc; function main() { - const client = new rpcProto.Fetch('localhost:50051', - grpc.credentials.createInsecure()); + const client = new rpcProto.Fetch( + 'localhost:50051', + grpc.credentials.createInsecure(), + ); const data = process.argv[2] || 'opentelemetry'; console.log('> ', data); diff --git a/examples/mongodb/package.json b/examples/mongodb/package.json deleted file mode 100644 index 903d10f84f..0000000000 --- a/examples/mongodb/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "mongodb-example", - "private": true, - "version": "0.23.0", - "description": "Example of mongodb integration with OpenTelemetry", - "main": "index.js", - "scripts": { - "zipkin:server": "cross-env EXPORTER=zipkin node ./server.js", - "zipkin:client": "cross-env EXPORTER=zipkin node ./client.js", - "jaeger:server": "cross-env EXPORTER=jaeger node ./server.js", - "jaeger:client": "cross-env EXPORTER=jaeger node ./client.js" - }, - "repository": { - "type": "git", - "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js-contrib.git" - }, - "keywords": [ - "opentelemetry", - "mongodb", - "tracing" - ], - "engines": { - "node": ">=8.5.0" - }, - "author": "OpenTelemetry Authors", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues" - }, - "dependencies": { - "@opentelemetry/api": "^1.0.2", - "@opentelemetry/exporter-jaeger": "^0.25.0", - "@opentelemetry/exporter-zipkin": "^0.25.0", - "@opentelemetry/instrumentation": "^0.25.0", - "@opentelemetry/instrumentation-http": "^0.25.0", - "@opentelemetry/instrumentation-mongodb": "^0.23.0", - "@opentelemetry/sdk-trace-node": "^0.25.0", - "@opentelemetry/sdk-trace-base": "^0.25.0", - "mongodb": "^3.5.7" - }, - "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib#readme", - "devDependencies": { - "cross-env": "^6.0.0" - } -} diff --git a/examples/mongodb/tracer.js b/examples/mongodb/tracer.js deleted file mode 100644 index 6222b0c02d..0000000000 --- a/examples/mongodb/tracer.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const opentelemetry = require('@opentelemetry/api'); -const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); -const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base'); -const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); -const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); -const { registerInstrumentations } = require('@opentelemetry/instrumentation'); -const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); -const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb'); - -module.exports = (serviceName) => { - const provider = new NodeTracerProvider(); - - provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter({ - serviceName, - }))); - provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter({ - serviceName, - }))); - - // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings - provider.register(); - - registerInstrumentations({ - instrumentations: [ - new HttpInstrumentation(), - new MongoDBInstrumentation({ - enhancedDatabaseReporting: true, - }), - ], - tracerProvider: provider, - }); - - return opentelemetry.trace.getTracer('mysql-example'); -}; diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/.eslintignore b/plugins/node/opentelemetry-instrumentation-mongodb/.eslintignore index 378eac25d3..514cc95d43 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/.eslintignore +++ b/plugins/node/opentelemetry-instrumentation-mongodb/.eslintignore @@ -1 +1,2 @@ build +examples diff --git a/examples/mongodb/README.md b/plugins/node/opentelemetry-instrumentation-mongodb/examples/README.md similarity index 84% rename from examples/mongodb/README.md rename to plugins/node/opentelemetry-instrumentation-mongodb/examples/README.md index ec34ff3a7e..d30fb9ed20 100644 --- a/examples/mongodb/README.md +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/README.md @@ -23,14 +23,21 @@ Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/ ### Zipkin +- Start MongoDB server via docker + + ```sh + # from this directory + npm run docker:start + ``` + - Run the server ```sh # from this directory - npm run server + npm run zipkin:server ``` -- Run the client +- Run the zipkin:client ```sh # from this directory @@ -42,22 +49,29 @@ Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/ `server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`). Go to Zipkin with your browser (e.g ) -

+

### Jaeger +- Start MongoDB server via docker + + ```sh + # from this directory + npm run docker:start + ``` + - Run the server ```sh # from this directory - npm run server + npm run jaeger:server ``` - Run the client ```sh # from this directory - npm run client + npm run jaeger:client ``` #### Jaeger UI diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/images/zipkin.png b/plugins/node/opentelemetry-instrumentation-mongodb/examples/images/zipkin.png new file mode 100644 index 0000000000000000000000000000000000000000..a4557e66c7e5c61ca1f72992483ec774fb8aa55a GIT binary patch literal 114237 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

&5!AJ2p;~l=+p?2S; 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 RKi_26=leb@-gfl6{{b*8Ay5DS literal 0 HcmV?d00001 diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json new file mode 100644 index 0000000000..f25bbe36ab --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/package.json @@ -0,0 +1,49 @@ +{ + "name": "mongodb-example", + "private": true, + "version": "0.28.0", + "description": "Example of mongodb integration with OpenTelemetry", + "main": "index.js", + "scripts": { + "docker:start": "docker run -e MONGODB_DB=opentelemetry-tests -e MONGODB_PORT=27017 -e MONGODB_HOST=127.0.0.1 -p 27017:27017 --rm mongo", + "zipkin:server": "cross-env EXPORTER=zipkin ts-node src/server.ts", + "zipkin:client": "cross-env EXPORTER=zipkin ts-node src/client.ts", + "jaeger:server": "cross-env EXPORTER=jaeger ts-node src/server.ts", + "jaeger:client": "cross-env EXPORTER=jaeger ts-node src/client.ts", + "compile": "tsc -p ." + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js-contrib.git" + }, + "keywords": [ + "opentelemetry", + "mongodb", + "tracing" + ], + "engines": { + "node": ">=8.12.0" + }, + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues" + }, + "dependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/exporter-jaeger": "^1.0.0", + "@opentelemetry/exporter-zipkin": "^1.0.0", + "@opentelemetry/instrumentation": "^0.32.0", + "@opentelemetry/instrumentation-http": "^0.32.0", + "@opentelemetry/instrumentation-mongodb": "^0.32.0", + "@opentelemetry/sdk-trace-node": "^1.0.0", + "@opentelemetry/sdk-trace-base": "^1.0.0", + "mongodb": "^3.5.7" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib#readme", + "devDependencies": { + "cross-env": "^7.0.3", + "ts-node": "^10.6.0", + "typescript": "4.3.5" + } +} diff --git a/examples/mongodb/client.js b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/client.ts similarity index 88% rename from examples/mongodb/client.js rename to plugins/node/opentelemetry-instrumentation-mongodb/examples/src/client.ts index 8ac0564df8..556aeb248f 100644 --- a/examples/mongodb/client.js +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/client.ts @@ -1,9 +1,11 @@ 'use strict'; -const api = require('@opentelemetry/api'); -const tracer = require('./tracer')('example-mongodb-http-client'); -// eslint-disable-next-line import/order -const http = require('http'); +import * as api from '@opentelemetry/api'; +import { setupTracing } from './tracer'; + +const tracer = setupTracing('example-mongodb-http-client') +import * as http from 'http'; + /** A function which makes requests and handles response. */ function makeRequest() { @@ -22,7 +24,7 @@ function makeRequest() { port: 8080, path: '/collection/', }, (response) => { - const body = []; + const body: any = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; @@ -38,7 +40,7 @@ function makeRequest() { port: 8080, path: '/insert/', }, (response) => { - const body = []; + const body: any = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; @@ -54,7 +56,7 @@ function makeRequest() { port: 8080, path: '/get/', }, (response) => { - const body = []; + const body: any = []; response.on('data', (chunk) => body.push(chunk)); response.on('end', () => { responses += 1; diff --git a/examples/mongodb/server.js b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/server.ts similarity index 52% rename from examples/mongodb/server.js rename to plugins/node/opentelemetry-instrumentation-mongodb/examples/src/server.ts index 082f599976..5953812d34 100644 --- a/examples/mongodb/server.js +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/server.ts @@ -1,41 +1,51 @@ -'use strict'; +import * as api from '@opentelemetry/api'; -const api = require('@opentelemetry/api'); -// eslint-disable-next-line import/order -require('./tracer')('example-mongodb-http-server'); +import { setupTracing } from './tracer'; -const { MongoClient } = require('mongodb'); -const http = require('http'); +import { accessDB } from './utils'; -const url = 'mongodb://localhost:27017/mydb'; -let db; +setupTracing('example-mongodb-server') + +import * as http from 'http'; +import { IncomingMessage, ServerResponse } from 'http'; +import * as mongodb from 'mongodb'; +import {Collection} from "mongodb"; + +const DB_NAME = 'mydb' +const COLLECTION_NAME = 'users' +const URL = `mongodb://localhost:27017/${DB_NAME}`; + +let db: mongodb.Db; /** Starts a HTTP server that receives requests on sample server port. */ -function startServer(port) { +function startServer(port: number) { // Connect to db - MongoClient.connect(url, (err, database) => { - if (err) throw err; - db = database.db('mydb'); - }); + accessDB(URL, DB_NAME) + .then(result => { + db = result; + }) + .catch((err: Error) => { + throw err; + }); + + // Creates a server const server = http.createServer(handleRequest); // Starts the server - server.listen(port, (err) => { - if (err) { - throw err; - } + server.listen(port, () => { console.log(`Node HTTP listening on ${port}`); }); } /** A function which handles requests and send response. */ -function handleRequest(request, response) { +function handleRequest(request: IncomingMessage, response: ServerResponse) { const currentSpan = api.trace.getSpan(api.context.active()); - // display traceid in the terminal - const { traceId } = currentSpan.spanContext(); + // display traceID in the terminal + const traceId = currentSpan?.spanContext(); console.log(`traceid: ${traceId}`); console.log(`Jaeger URL: http://localhost:16686/trace/${traceId}`); console.log(`Zipkin URL: http://localhost:9411/zipkin/traces/${traceId}`); + try { const body = []; request.on('error', (err) => console.log(err)); @@ -58,48 +68,49 @@ function handleRequest(request, response) { startServer(8080); -function handleInsertQuery(response) { +function handleInsertQuery(response: ServerResponse) { const obj = { name: 'John', age: '20' }; - const collection = db.collection('users'); - collection.insertOne(obj, (err) => { - if (err) { - console.log('Error code:', err.code); - response.end(err.message); - } else { + const usersCollection: Collection = db.collection(COLLECTION_NAME); + usersCollection.insertOne(obj) + .then(() => { console.log('1 document inserted'); // find document to test context propagation using callback - // eslint-disable-next-line prefer-arrow-callback - collection.findOne({}, function () { + usersCollection.findOne({}, function () { response.end(); }); - } - }); -} - -function handleGetQuery(response) { - db.collection('users').find({}, (err) => { - if (err) { + }) + .catch(err => { console.log('Error code:', err.code); response.end(err.message); - } else { + }); +} + +function handleGetQuery(response: ServerResponse) { + const usersCollection: Collection = db.collection(COLLECTION_NAME); + usersCollection. + find({}) + .toArray() + .then(() => { console.log('1 document served'); response.end(); - } - }); + }) + .catch(err => { + throw err; + }) } -function handleCreateCollection(response) { - db.createCollection('users', (err) => { - if (err) { - console.log('Error code:', err.code); - response.end(err.message); - } else { +function handleCreateCollection(response: ServerResponse) { + db.createCollection(COLLECTION_NAME) + .then(() => { console.log('1 collection created'); response.end(); - } + }) + .catch(err => { + console.log('Error code:', err.code); + response.end(err.message); }); } -function handleNotFound(response) { +function handleNotFound(response: ServerResponse) { response.end('not found'); } diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/tracer.ts b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/tracer.ts new file mode 100644 index 0000000000..e38075df25 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/tracer.ts @@ -0,0 +1,38 @@ +import * as api from "@opentelemetry/api"; + +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { Resource } from '@opentelemetry/resources'; +import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; + + +export const setupTracing = (serviceName: string): api.Tracer => { + const provider = new NodeTracerProvider({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: serviceName + }) + }); + + // Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings + provider.register(); + + registerInstrumentations({ + instrumentations: [ + new HttpInstrumentation(), + new MongoDBInstrumentation({ + enhancedDatabaseReporting: true, + }), + ], + tracerProvider: provider, + }); + + provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter())); + provider.addSpanProcessor(new SimpleSpanProcessor(new JaegerExporter())); + + return api.trace.getTracer('mongodb-example'); +}; diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/utils.ts b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/utils.ts new file mode 100644 index 0000000000..350149dcaf --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/src/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 * as mongodb from 'mongodb'; + +/** + * Access the mongodb Db. + * @param url The mongodb URL to access. + * @param dbName The mongodb database name. + * @param options The mongodb client config options. + */ +export function accessDB( + url: string, + dbName: string, + options: mongodb.MongoClientOptions = {} +): Promise { + return new Promise((resolve, reject) => { + mongodb.MongoClient.connect(url, { + serverSelectionTimeoutMS: 1000, + useUnifiedTopology: true + }) + .then(client => { + resolve(client.db(dbName)); + }) + .catch(reason => { + reject(reason); + }); + }); +} diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/examples/tsconfig.json b/plugins/node/opentelemetry-instrumentation-mongodb/examples/tsconfig.json new file mode 100644 index 0000000000..5a768b7e2c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/examples/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": ".", + }, + "include": [ + "src/**/*.ts", + ] +} diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/package.json index 90dd9f40ef..b551587d2d 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/package.json +++ b/plugins/node/opentelemetry-instrumentation-mongodb/package.json @@ -18,6 +18,7 @@ "prewatch": "npm run precompile", "version:update": "node ../../../scripts/version-update.js", "compile": "tsc -p .", + "compile:examples": "cd examples && npm run compile", "prepare": "npm run compile", "watch": "tsc -w" }, From 35d1e4579f7b160c501959f6fb45859b75cdde99 Mon Sep 17 00:00:00 2001 From: Manuel Spigolon Date: Tue, 20 Sep 2022 04:47:50 +0200 Subject: [PATCH 04/19] fix: readme snippet (#1182) --- packages/opentelemetry-host-metrics/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/opentelemetry-host-metrics/README.md b/packages/opentelemetry-host-metrics/README.md index 18f4ab84f3..d9635f1acf 100644 --- a/packages/opentelemetry-host-metrics/README.md +++ b/packages/opentelemetry-host-metrics/README.md @@ -20,24 +20,23 @@ npm install --save @opentelemetry/host-metrics ## Usage ```javascript -const { MeterProvider } = require('@opentelemetry/sdk-metrics-base'); +const { MeterProvider } = require('@opentelemetry/sdk-metrics'); const { HostMetrics } = require('@opentelemetry/host-metrics'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const exporter = new PrometheusExporter( - { startServer: true },() => { - console.log('prometheus scrape endpoint: http://localhost:9464/metrics'); + { + startServer: true + }, () => { + console.log('prometheus scrape endpoint: http://localhost:9464/metrics') } ); -const meterProvider = new MeterProvider({ - exporter, - interval: 2000, -}); +const meterProvider = new MeterProvider(); +meterProvider.addMetricReader(exporter); const hostMetrics = new HostMetrics({ meterProvider, name: 'example-host-metrics' }); hostMetrics.start(); - ``` ## Useful links From d463695f5258875f1da0c7b17c20f7df93494d4e Mon Sep 17 00:00:00 2001 From: Haddas Bronfman <85441461+haddasbronfman@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:17:14 +0300 Subject: [PATCH 05/19] fix(aws-sdk): set spanKind to CLIENT by default in v3 (#1177) * fix(aws-sdk): set soanKind to CLIENT by default * fix(aws-sdk): assert in v2 and v3 test the spanKind is CLIENT * fix(aws-sdk): lint --- .../node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts | 2 +- .../test/aws-sdk-v2.test.ts | 4 ++-- .../test/aws-sdk-v3.test.ts | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts index f94dffc35e..0342a87a70 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts @@ -213,7 +213,7 @@ export class AwsInstrumentation extends InstrumentationBase { metadata.spanName ?? `${normalizedRequest.serviceName}.${normalizedRequest.commandName}`; const newSpan = this.tracer.startSpan(name, { - kind: metadata.spanKind, + kind: metadata.spanKind ?? SpanKind.CLIENT, attributes: { ...extractAttributesFromNormalizedRequest(normalizedRequest), ...metadata.spanAttributes, diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v2.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v2.test.ts index d8c2fd09be..f40f80903b 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v2.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v2.test.ts @@ -28,7 +28,7 @@ const instrumentation = registerInstrumentationTesting( import * as AWS from 'aws-sdk'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import { SpanStatusCode, Span } from '@opentelemetry/api'; +import { SpanStatusCode, Span, SpanKind } from '@opentelemetry/api'; import { AttributeNames } from '../src/enums'; import { mockV2AwsSend } from './testing-utils'; import * as expect from 'expect'; @@ -123,7 +123,7 @@ describe('instrumentation-aws-sdk-v2', () => { ).toBe(200); expect(spanCreateBucket.name).toBe('S3.CreateBucket'); - + expect(spanCreateBucket.kind).toEqual(SpanKind.CLIENT); expect(spanPutObject.attributes[AttributeNames.AWS_OPERATION]).toBe( 'putObject' ); diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3.test.ts b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3.test.ts index f1539905b7..8623b6e873 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3.test.ts +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/test/aws-sdk-v3.test.ts @@ -32,6 +32,7 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { SQS } from '@aws-sdk/client-sqs'; +import { SpanKind } from '@opentelemetry/api'; // set aws environment variables, so tests in non aws environment are able to run process.env.AWS_ACCESS_KEY_ID = 'testing'; @@ -78,6 +79,7 @@ describe('instrumentation-aws-sdk-v3', () => { expect(span.attributes[SemanticAttributes.RPC_SERVICE]).toEqual('S3'); expect(span.attributes[AttributeNames.AWS_REGION]).toEqual(region); expect(span.name).toEqual('S3.PutObject'); + expect(span.kind).toEqual(SpanKind.CLIENT); expect(span.attributes[SemanticAttributes.HTTP_STATUS_CODE]).toEqual(200); }); From 01d6e9553690433ebf30d973e4e1157b39353ff6 Mon Sep 17 00:00:00 2001 From: Tom Wong Date: Wed, 21 Sep 2022 22:28:11 -0700 Subject: [PATCH 06/19] chore: add url to documentFetch span (#1129) * chore: add url and HTTP_USER_AGENT to documentFetch span * chore: add unit test * chore: remove user agent from documentFetch span Co-authored-by: Nev <54870357+MSNev@users.noreply.github.com> --- .../src/instrumentation.ts | 1 + .../test/documentLoad.test.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts index 9a537ad0a6..cd0fdccb84 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/src/instrumentation.ts @@ -110,6 +110,7 @@ export class DocumentLoadInstrumentation extends InstrumentationBase { entries ); if (fetchSpan) { + fetchSpan.setAttribute(SemanticAttributes.HTTP_URL, location.href); context.with(trace.setSpan(context.active(), fetchSpan), () => { addSpanNetworkEvents(fetchSpan, entries); this._endSpan(fetchSpan, PTN.RESPONSE_END, entries); diff --git a/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts b/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts index 54eef0045f..6b7422df98 100644 --- a/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts +++ b/plugins/web/opentelemetry-instrumentation-document-load/test/documentLoad.test.ts @@ -544,6 +544,11 @@ describe('DocumentLoad Instrumentation', () => { assert.strictEqual(fetchSpan.name, 'documentFetch'); assert.strictEqual(rootSpan.name, 'documentLoad'); + assert.strictEqual( + fetchSpan.attributes['http.url'], + 'http://localhost:9876/context.html' + ); + assert.strictEqual( rootSpan.attributes['http.url'], 'http://localhost:9876/context.html' From b35277bb5fc66910e8942bc0b64347b68ecffa26 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Thu, 22 Sep 2022 13:11:57 +0300 Subject: [PATCH 07/19] feat: add mongoose instrumentation (#1131) --- .github/component_owners.yml | 2 + .release-please-manifest.json | 2 +- .../auto-instrumentations-node/README.md | 1 + .../auto-instrumentations-node/package.json | 3 +- .../auto-instrumentations-node/src/utils.ts | 2 + .../test/utils.test.ts | 3 +- .../instrumentation-mongoose/.eslintignore | 1 + .../instrumentation-mongoose/.eslintrc.js | 7 + .../node/instrumentation-mongoose/.npmignore | 4 + .../node/instrumentation-mongoose/.tav.yml | 4 + plugins/node/instrumentation-mongoose/LICENSE | 201 +++++ .../node/instrumentation-mongoose/README.md | 69 ++ .../instrumentation-mongoose/package.json | 69 ++ .../instrumentation-mongoose/src/index.ts | 17 + .../instrumentation-mongoose/src/mongoose.ts | 369 +++++++++ .../instrumentation-mongoose/src/types.ts | 62 ++ .../instrumentation-mongoose/src/utils.ts | 107 +++ .../instrumentation-mongoose/test/asserts.ts | 35 + .../instrumentation-mongoose/test/config.ts | 20 + .../test/mongoose.test.ts | 726 ++++++++++++++++++ .../instrumentation-mongoose/test/user.ts | 59 ++ .../instrumentation-mongoose/tsconfig.json | 12 + release-please-config.json | 1 + 23 files changed, 1773 insertions(+), 3 deletions(-) create mode 100644 plugins/node/instrumentation-mongoose/.eslintignore create mode 100644 plugins/node/instrumentation-mongoose/.eslintrc.js create mode 100644 plugins/node/instrumentation-mongoose/.npmignore create mode 100644 plugins/node/instrumentation-mongoose/.tav.yml create mode 100644 plugins/node/instrumentation-mongoose/LICENSE create mode 100644 plugins/node/instrumentation-mongoose/README.md create mode 100644 plugins/node/instrumentation-mongoose/package.json create mode 100644 plugins/node/instrumentation-mongoose/src/index.ts create mode 100644 plugins/node/instrumentation-mongoose/src/mongoose.ts create mode 100644 plugins/node/instrumentation-mongoose/src/types.ts create mode 100644 plugins/node/instrumentation-mongoose/src/utils.ts create mode 100644 plugins/node/instrumentation-mongoose/test/asserts.ts create mode 100644 plugins/node/instrumentation-mongoose/test/config.ts create mode 100644 plugins/node/instrumentation-mongoose/test/mongoose.test.ts create mode 100644 plugins/node/instrumentation-mongoose/test/user.ts create mode 100644 plugins/node/instrumentation-mongoose/tsconfig.json diff --git a/.github/component_owners.yml b/.github/component_owners.yml index c73c5dc8ff..ecbe8c6ec8 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -44,6 +44,8 @@ components: - rauno56 plugins/node/opentelemetry-instrumentation-mongodb: - osherv + plugins/node/opentelemetry-instrumentation-mongoose: + - blumamir plugins/node/opentelemetry-instrumentation-nestjs-core: - rauno56 plugins/node/opentelemetry-instrumentation-redis: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4696cce64d..4f17e1bfed 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","metapackages/auto-instrumentations-node":"0.32.1","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.0","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.1","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.0","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1","plugins/node/opentelemetry-instrumentation-fastify":"0.29.0","packages/opentelemetry-propagation-utils":"0.28.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-instana":"0.3.0"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","metapackages/auto-instrumentations-node":"0.32.1","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.0","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.1","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.0","plugins/node/opentelemetry-instrumentation-mongoose":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1","plugins/node/opentelemetry-instrumentation-fastify":"0.29.0","packages/opentelemetry-propagation-utils":"0.28.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-instana":"0.3.0"} diff --git a/metapackages/auto-instrumentations-node/README.md b/metapackages/auto-instrumentations-node/README.md index a9825703fd..f9051c1748 100644 --- a/metapackages/auto-instrumentations-node/README.md +++ b/metapackages/auto-instrumentations-node/README.md @@ -78,6 +78,7 @@ registerInstrumentations({ - [@opentelemetry/instrumentation-koa](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-koa) - [@opentelemetry/instrumentation-memcached](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-memcached) - [@opentelemetry/instrumentation-mongodb](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mongodb) +- [@opentelemetry/instrumentation-mongoose](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-mongoose) - [@opentelemetry/instrumentation-mysql2](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql2) - [@opentelemetry/instrumentation-mysql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql) - [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index 5512ff6a54..b56184ed75 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -67,7 +67,8 @@ "@opentelemetry/instrumentation-lru-memoizer": "^0.31.0", "@opentelemetry/instrumentation-memcached": "^0.30.0", "@opentelemetry/instrumentation-mongodb": "^0.32.0", - "@opentelemetry/instrumentation-mysql": "^0.31.1", + "@opentelemetry/instrumentation-mongoose": "^0.30.0", + "@opentelemetry/instrumentation-mysql": "^0.31.0", "@opentelemetry/instrumentation-mysql2": "^0.32.0", "@opentelemetry/instrumentation-nestjs-core": "^0.31.0", "@opentelemetry/instrumentation-net": "^0.30.1", diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index 44163eaac8..f85f42eee8 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -36,6 +36,7 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer'; import { MemcachedInstrumentation } from '@opentelemetry/instrumentation-memcached'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; +import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; @@ -69,6 +70,7 @@ const InstrumentationMap = { '@opentelemetry/instrumentation-lru-memoizer': LruMemoizerInstrumentation, '@opentelemetry/instrumentation-memcached': MemcachedInstrumentation, '@opentelemetry/instrumentation-mongodb': MongoDBInstrumentation, + '@opentelemetry/instrumentation-mongoose': MongooseInstrumentation, '@opentelemetry/instrumentation-mysql2': MySQL2Instrumentation, '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, '@opentelemetry/instrumentation-nestjs-core': NestInstrumentation, diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index 20f15c1723..24d8d561ab 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -45,6 +45,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-lru-memoizer', '@opentelemetry/instrumentation-memcached', '@opentelemetry/instrumentation-mongodb', + '@opentelemetry/instrumentation-mongoose', '@opentelemetry/instrumentation-mysql2', '@opentelemetry/instrumentation-mysql', '@opentelemetry/instrumentation-nestjs-core', @@ -56,7 +57,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-restify', '@opentelemetry/instrumentation-winston', ]; - assert.strictEqual(instrumentations.length, 30); + assert.strictEqual(instrumentations.length, 31); for (let i = 0, j = instrumentations.length; i < j; i++) { assert.strictEqual( instrumentations[i].instrumentationName, diff --git a/plugins/node/instrumentation-mongoose/.eslintignore b/plugins/node/instrumentation-mongoose/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/instrumentation-mongoose/.eslintrc.js b/plugins/node/instrumentation-mongoose/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/instrumentation-mongoose/.npmignore b/plugins/node/instrumentation-mongoose/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/instrumentation-mongoose/.tav.yml b/plugins/node/instrumentation-mongoose/.tav.yml new file mode 100644 index 0000000000..8367cdde2e --- /dev/null +++ b/plugins/node/instrumentation-mongoose/.tav.yml @@ -0,0 +1,4 @@ +'mongoose': + versions: ">=5.9.7 <7" + commands: + - npm run test diff --git a/plugins/node/instrumentation-mongoose/LICENSE b/plugins/node/instrumentation-mongoose/LICENSE new file mode 100644 index 0000000000..e50e8c80f9 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2022] OpenTelemetry Authors + + Licensed 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. diff --git a/plugins/node/instrumentation-mongoose/README.md b/plugins/node/instrumentation-mongoose/README.md new file mode 100644 index 0000000000..22a600e07e --- /dev/null +++ b/plugins/node/instrumentation-mongoose/README.md @@ -0,0 +1,69 @@ +# OpenTelemetry mongoose Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for the [`mongoose`](https://github.com/Automattic/mongoose) module, which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package and is included in the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle. + +If total installation size is not constrained, it is recommended to use the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle with [@opentelemetry/sdk-node](`https://www.npmjs.com/package/@opentelemetry/sdk-node`) for the most seamless instrumentation experience. + +Compatible with OpenTelemetry JS API and SDK `1.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-mongoose +``` + +## Supported Versions + +- `>=5.9.7 <7` + +## Usage + +To load a specific plugin, specify it in the registerInstrumentations's configuration: + +```js +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { MongooseInstrumentation } = require('@opentelemetry/instrumentation-mongoose'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new MongooseInstrumentation(), + ], +}) +``` + +## Migration From opentelemetry-instrumentation-mongoose + +This instrumentation was originally published and maintained under the name `"opentelemetry-instrumentation-mongoose"` in [this repo](https://github.com/aspecto-io/opentelemetry-ext-js). + +Few breaking changes were made during porting to the contrib repo to align with conventions: + +### Hook Info + +The instrumentation's config `responseHook` functions signature changed, so the second function parameter is info object, containing the relevant hook data. + +### `moduleVersionAttributeName` config option + +The `moduleVersionAttributeName` config option is removed. To add the mongoose package version to spans, use the `moduleVersion` attribute in hook info for `responseHook` function. + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-mongoose +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-mongoose.svg diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json new file mode 100644 index 0000000000..db0b06f90d --- /dev/null +++ b/plugins/node/instrumentation-mongoose/package.json @@ -0,0 +1,69 @@ +{ + "name": "@opentelemetry/instrumentation-mongoose", + "version": "0.30.0", + "description": "OpenTelemetry automatic instrumentation package for mongoose", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "docker:start": "docker run -e MONGODB_DB=opentelemetry-tests -e MONGODB_PORT=27017 -e MONGODB_HOST=127.0.0.1 -p 27017:27017 --rm mongo", + "test": "ts-mocha -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.test.ts'", + "test-all-versions": "tav", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version && lerna run version --scope @opentelemetry/instrumentation-mongoose --include-dependencies", + "prewatch": "npm run precompile", + "prepare": "npm run compile", + "version:update": "node ../../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p ." + }, + "keywords": [ + "mongodb", + "mongoose", + "orm", + "instrumentation", + "nodejs", + "opentelemetry", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/contrib-test-utils": "^0.32.0", + "@opentelemetry/sdk-trace-base": "^1.3.1", + "@types/mocha": "8.2.3", + "@types/node": "16.11.21", + "expect": "27.4.2", + "gts": "3.1.0", + "mocha": "7.2.0", + "mongoose": "6.5.2", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "test-all-versions": "5.0.1", + "ts-mocha": "10.0.0", + "typescript": "4.3.5" + }, + "dependencies": { + "@opentelemetry/core": "^1.0.0", + "@opentelemetry/instrumentation": "^0.32.0", + "@opentelemetry/semantic-conventions": "^1.0.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-mongoose#readme" +} diff --git a/plugins/node/instrumentation-mongoose/src/index.ts b/plugins/node/instrumentation-mongoose/src/index.ts new file mode 100644 index 0000000000..5a9c213a2b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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. + */ +export * from './mongoose'; +export * from './types'; diff --git a/plugins/node/instrumentation-mongoose/src/mongoose.ts b/plugins/node/instrumentation-mongoose/src/mongoose.ts new file mode 100644 index 0000000000..67a170ba1b --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/mongoose.ts @@ -0,0 +1,369 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { + context, + Span, + trace, + SpanAttributes, + SpanKind, +} from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import type * as mongoose from 'mongoose'; +import { MongooseInstrumentationConfig, SerializerPayload } from './types'; +import { + handleCallbackResponse, + handlePromiseResponse, + getAttributesFromCollection, +} from './utils'; +import { + InstrumentationBase, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, +} from '@opentelemetry/instrumentation'; +import { VERSION } from './version'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +const contextCaptureFunctions = [ + 'remove', + 'deleteOne', + 'deleteMany', + 'find', + 'findOne', + 'estimatedDocumentCount', + 'countDocuments', + 'count', + 'distinct', + 'where', + '$where', + 'findOneAndUpdate', + 'findOneAndDelete', + 'findOneAndReplace', + 'findOneAndRemove', +]; + +// when mongoose functions are called, we store the original call context +// and then set it as the parent for the spans created by Query/Aggregate exec() +// calls. this bypass the unlinked spans issue on thenables await operations. +export const _STORED_PARENT_SPAN: unique symbol = Symbol('stored-parent-span'); + +export class MongooseInstrumentation extends InstrumentationBase { + protected override _config!: MongooseInstrumentationConfig; + + constructor(config: MongooseInstrumentationConfig = {}) { + super( + '@opentelemetry/instrumentation-mongoose', + VERSION, + Object.assign({}, config) + ); + } + + override setConfig(config: MongooseInstrumentationConfig = {}) { + this._config = Object.assign({}, config); + } + + protected init(): InstrumentationModuleDefinition { + const module = new InstrumentationNodeModuleDefinition( + 'mongoose', + ['>=5.9.7 <7'], + this.patch.bind(this), + this.unpatch.bind(this) + ); + return module; + } + + protected patch( + moduleExports: typeof mongoose, + moduleVersion: string | undefined + ) { + this._wrap( + moduleExports.Model.prototype, + 'save', + this.patchOnModelMethods('save', moduleVersion) + ); + // mongoose applies this code on moudle require: + // Model.prototype.$save = Model.prototype.save; + // which captures the save function before it is patched. + // so we need to apply the same logic after instrumenting the save function. + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + + this._wrap( + moduleExports.Model.prototype, + 'remove', + this.patchOnModelMethods('remove', moduleVersion) + ); + this._wrap( + moduleExports.Query.prototype, + 'exec', + this.patchQueryExec(moduleVersion) + ); + this._wrap( + moduleExports.Aggregate.prototype, + 'exec', + this.patchAggregateExec(moduleVersion) + ); + + contextCaptureFunctions.forEach((funcName: string) => { + this._wrap( + moduleExports.Query.prototype, + funcName as any, + this.patchAndCaptureSpanContext(funcName) + ); + }); + this._wrap(moduleExports.Model, 'aggregate', this.patchModelAggregate()); + + return moduleExports; + } + + protected unpatch(moduleExports: typeof mongoose): void { + this._diag.debug('mongoose instrumentation: unpatch mongoose'); + this._unwrap(moduleExports.Model.prototype, 'save'); + // revert the patch for $save which we applyed by aliasing it to patched `save` + moduleExports.Model.prototype.$save = moduleExports.Model.prototype.save; + this._unwrap(moduleExports.Model.prototype, 'remove'); + this._unwrap(moduleExports.Query.prototype, 'exec'); + this._unwrap(moduleExports.Aggregate.prototype, 'exec'); + + contextCaptureFunctions.forEach((funcName: string) => { + this._unwrap(moduleExports.Query.prototype, funcName as any); + }); + this._unwrap(moduleExports.Model, 'aggregate'); + } + + private patchAggregateExec(moduleVersion: string | undefined) { + const self = this; + this._diag.debug('patched mongoose Aggregate exec function'); + return (originalAggregate: Function) => { + return function exec(this: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalAggregate.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer('aggregate', { + options: this.options, + aggregatePipeline: this._pipeline, + }); + } + + const span = self._startSpan( + this._model.collection, + this._model?.modelName, + 'aggregate', + attributes, + parentSpan + ); + + return self._handleResponse( + span, + originalAggregate, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + private patchQueryExec(moduleVersion: string | undefined) { + const self = this; + this._diag.debug('patched mongoose Query exec function'); + return (originalExec: Function) => { + return function exec(this: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalExec.apply(this, arguments); + } + + const parentSpan = this[_STORED_PARENT_SPAN]; + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer(this.op, { + condition: this._conditions, + updates: this._update, + options: this.options, + fields: this._fields, + }); + } + const span = self._startSpan( + this.mongooseCollection, + this.model.modelName, + this.op, + attributes, + parentSpan + ); + + return self._handleResponse( + span, + originalExec, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + private patchOnModelMethods(op: string, moduleVersion: string | undefined) { + const self = this; + this._diag.debug(`patching mongoose Model '${op}' operation`); + return (originalOnModelFunction: Function) => { + return function method(this: any, options?: any, callback?: Function) { + if ( + self._config.requireParentSpan && + trace.getSpan(context.active()) === undefined + ) { + return originalOnModelFunction.apply(this, arguments); + } + + const serializePayload: SerializerPayload = { document: this }; + if (options && !(options instanceof Function)) { + serializePayload.options = options; + } + const attributes: SpanAttributes = {}; + if (self._config.dbStatementSerializer) { + attributes[SemanticAttributes.DB_STATEMENT] = + self._config.dbStatementSerializer(op, serializePayload); + } + const span = self._startSpan( + this.constructor.collection, + this.constructor.modelName, + op, + attributes + ); + + if (options instanceof Function) { + callback = options; + options = undefined; + } + + return self._handleResponse( + span, + originalOnModelFunction, + this, + arguments, + callback, + moduleVersion + ); + }; + }; + } + + // we want to capture the otel span on the object which is calling exec. + // in the special case of aggregate, we need have no function to path + // on the Aggregate object to capture the context on, so we patch + // the aggregate of Model, and set the context on the Aggregate object + private patchModelAggregate() { + const self = this; + this._diag.debug('patched mongoose model aggregate function'); + return (original: Function) => { + return function captureSpanContext(this: any) { + const currentSpan = trace.getSpan(context.active()); + const aggregate = self._callOriginalFunction(() => + original.apply(this, arguments) + ); + if (aggregate) aggregate[_STORED_PARENT_SPAN] = currentSpan; + return aggregate; + }; + }; + } + + private patchAndCaptureSpanContext(funcName: string) { + const self = this; + this._diag.debug(`patching mongoose query ${funcName} function`); + return (original: Function) => { + return function captureSpanContext(this: any) { + this[_STORED_PARENT_SPAN] = trace.getSpan(context.active()); + return self._callOriginalFunction(() => + original.apply(this, arguments) + ); + }; + }; + } + + private _startSpan( + collection: mongoose.Collection, + modelName: string, + operation: string, + attributes: SpanAttributes, + parentSpan?: Span + ): Span { + return this.tracer.startSpan( + `mongoose.${modelName}.${operation}`, + { + kind: SpanKind.CLIENT, + attributes: { + ...attributes, + ...getAttributesFromCollection(collection), + [SemanticAttributes.DB_OPERATION]: operation, + [SemanticAttributes.DB_SYSTEM]: 'mongoose', + }, + }, + parentSpan ? trace.setSpan(context.active(), parentSpan) : undefined + ); + } + + private _handleResponse( + span: Span, + exec: Function, + originalThis: any, + args: IArguments, + callback?: Function, + moduleVersion: string | undefined = undefined + ) { + const self = this; + if (callback instanceof Function) { + return self._callOriginalFunction(() => + handleCallbackResponse( + callback, + exec, + originalThis, + span, + self._config.responseHook, + moduleVersion + ) + ); + } else { + const response = self._callOriginalFunction(() => + exec.apply(originalThis, args) + ); + return handlePromiseResponse( + response, + span, + self._config.responseHook, + moduleVersion + ); + } + } + + private _callOriginalFunction(originalFunction: (...args: any[]) => T): T { + if (this._config?.suppressInternalInstrumentation) { + return context.with(suppressTracing(context.active()), originalFunction); + } else { + return originalFunction(); + } + } +} diff --git a/plugins/node/instrumentation-mongoose/src/types.ts b/plugins/node/instrumentation-mongoose/src/types.ts new file mode 100644 index 0000000000..8afce8e347 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/types.ts @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface SerializerPayload { + condition?: any; + options?: any; + updates?: any; + document?: any; + aggregatePipeline?: any; + fields?: any; +} + +export type DbStatementSerializer = ( + operation: string, + payload: SerializerPayload +) => string; + +export interface ResponseInfo { + moduleVersion: string | undefined; + response: any; +} + +export type MongooseResponseCustomAttributesFunction = ( + span: Span, + responseInfo: ResponseInfo +) => void; + +export interface MongooseInstrumentationConfig extends InstrumentationConfig { + /** + * Mongoose operation use mongodb under the hood. + * If mongodb instrumentation is enabled, a mongoose operation will also create + * a mongodb operation describing the communication with mongoDB servers. + * Setting the `suppressInternalInstrumentation` config value to `true` will + * cause the instrumentation to suppress instrumentation of underlying operations, + * effectively causing mongodb spans to be non-recordable. + */ + suppressInternalInstrumentation?: boolean; + + /** Custom serializer function for the db.statement tag */ + dbStatementSerializer?: DbStatementSerializer; + + /** hook for adding custom attributes using the response payload */ + responseHook?: MongooseResponseCustomAttributesFunction; + + /** Set to true if you do not want to collect traces that start with mongoose */ + requireParentSpan?: boolean; +} diff --git a/plugins/node/instrumentation-mongoose/src/utils.ts b/plugins/node/instrumentation-mongoose/src/utils.ts new file mode 100644 index 0000000000..2c44c87dae --- /dev/null +++ b/plugins/node/instrumentation-mongoose/src/utils.ts @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { SpanAttributes, SpanStatusCode, diag, Span } from '@opentelemetry/api'; +import type { Collection } from 'mongoose'; +import { MongooseResponseCustomAttributesFunction } from './types'; +import { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; + +export function getAttributesFromCollection( + collection: Collection +): SpanAttributes { + return { + [SemanticAttributes.DB_MONGODB_COLLECTION]: collection.name, + [SemanticAttributes.DB_NAME]: collection.conn.name, + [SemanticAttributes.DB_USER]: collection.conn.user, + [SemanticAttributes.NET_PEER_NAME]: collection.conn.host, + [SemanticAttributes.NET_PEER_PORT]: collection.conn.port, + }; +} + +function setErrorStatus(span: Span, error: any = {}) { + span.recordException(error); + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: `${error.message} ${ + error.code ? `\nMongoose Error Code: ${error.code}` : '' + }`, + }); +} + +function applyResponseHook( + span: Span, + response: any, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +) { + if (!responseHook) { + return; + } + + safeExecuteInTheMiddle( + () => responseHook(span, { moduleVersion, response }), + e => { + if (e) { + diag.error('mongoose instrumentation: responseHook error', e); + } + }, + true + ); +} + +export function handlePromiseResponse( + execResponse: any, + span: Span, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +): any { + if (!(execResponse instanceof Promise)) { + applyResponseHook(span, execResponse, responseHook, moduleVersion); + span.end(); + return execResponse; + } + + return execResponse + .then(response => { + applyResponseHook(span, response, responseHook, moduleVersion); + return response; + }) + .catch(err => { + setErrorStatus(span, err); + throw err; + }) + .finally(() => span.end()); +} + +export function handleCallbackResponse( + callback: Function, + exec: Function, + originalThis: any, + span: Span, + responseHook?: MongooseResponseCustomAttributesFunction, + moduleVersion: string | undefined = undefined +) { + return exec.apply(originalThis, [ + (err: Error, response: any) => { + err + ? setErrorStatus(span, err) + : applyResponseHook(span, response, responseHook, moduleVersion); + span.end(); + return callback!(err, response); + }, + ]); +} diff --git a/plugins/node/instrumentation-mongoose/test/asserts.ts b/plugins/node/instrumentation-mongoose/test/asserts.ts new file mode 100644 index 0000000000..f6c7e15ec3 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/asserts.ts @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 * as expect from 'expect'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { SpanStatusCode } from '@opentelemetry/api'; +import { SerializerPayload } from '../src'; +import { DB_NAME, MONGO_HOST, MONGO_PORT } from './config'; + +export const assertSpan = (span: ReadableSpan) => { + expect(span.status.code).toBe(SpanStatusCode.UNSET); + expect(span.attributes[SemanticAttributes.DB_SYSTEM]).toEqual('mongoose'); + expect(span.attributes[SemanticAttributes.DB_MONGODB_COLLECTION]).toEqual( + 'users' + ); + expect(span.attributes[SemanticAttributes.DB_NAME]).toEqual(DB_NAME); + expect(span.attributes[SemanticAttributes.NET_PEER_NAME]).toEqual(MONGO_HOST); + expect(span.attributes[SemanticAttributes.NET_PEER_PORT]).toEqual(MONGO_PORT); +}; + +export const getStatement = (span: ReadableSpan): SerializerPayload => + JSON.parse(span.attributes[SemanticAttributes.DB_STATEMENT] as string); diff --git a/plugins/node/instrumentation-mongoose/test/config.ts b/plugins/node/instrumentation-mongoose/test/config.ts new file mode 100644 index 0000000000..7e6426d34f --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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. + */ +export const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests'; +export const MONGO_HOST = process.env.MONGODB_HOST || 'localhost'; +export const MONGO_PORT = Number(process.env.MONGODB_PORT || 27017); + +export const MONGO_URI = `mongodb://${MONGO_HOST}/${MONGO_PORT}`; diff --git a/plugins/node/instrumentation-mongoose/test/mongoose.test.ts b/plugins/node/instrumentation-mongoose/test/mongoose.test.ts new file mode 100644 index 0000000000..6ba0297bd5 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/mongoose.test.ts @@ -0,0 +1,726 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 'mocha'; +import * as expect from 'expect'; +import { context, ROOT_CONTEXT } from '@opentelemetry/api'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { MongooseInstrumentation } from '../src'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; + +const instrumentation = registerInstrumentationTesting( + new MongooseInstrumentation() +); + +import * as mongoose from 'mongoose'; +import User, { IUser, loadUsers } from './user'; +import { assertSpan, getStatement } from './asserts'; +import { DB_NAME, MONGO_URI } from './config'; + +// Please run mongodb in the background: docker run -d -p 27017:27017 -v ~/data:/data/db mongo +describe('mongoose instrumentation', () => { + before(async () => { + try { + await mongoose.connect(MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + useFindAndModify: false, + useCreateIndex: true, + dbName: DB_NAME, + } as any); // TODO: amir - document older mongoose support + } catch (err) { + // connect signature changed from mongo v5 to v6. + // the following check tries both signatures, so test-all-versions + // can run against both versions. + if (err?.name === 'MongoParseError') { + await mongoose.connect(MONGO_URI, { + dbName: DB_NAME, + }); // TODO: amir - document older mongoose support + } + } + }); + + after(async () => { + await mongoose.connection.close(); + }); + + beforeEach(async () => { + instrumentation.disable(); + instrumentation.setConfig({ + dbStatementSerializer: (_operation: string, payload) => + JSON.stringify(payload), + }); + instrumentation.enable(); + await loadUsers(); + await User.createIndexes(); + }); + + afterEach(async () => { + instrumentation.disable(); + await User.collection.drop().catch(); + }); + + it('instrumenting save operation with promise', async () => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + + await user.save(); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + }); + + it('instrumenting save operation with callback', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + + user.save(() => { + const spans = getTestSpans(); + + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.document).toEqual(expect.objectContaining(document)); + done(); + }); + }); + + it('instrumenting find operation', async () => { + await User.find({ id: '_test' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + }); + + it('instrumenting multiple find operations', async () => { + await Promise.all([ + User.find({ id: '_test1' }), + User.find({ id: '_test2' }), + ]); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[0] as ReadableSpan); + assertSpan(spans[1] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + expect(spans[0].attributes[SemanticAttributes.DB_STATEMENT]).toMatch( + /.*{"id":"_test[1-2]"}.*/g + ); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + expect(spans[1].attributes[SemanticAttributes.DB_STATEMENT]).toMatch( + /.*{"id":"_test[1-2]"}.*/g + ); + }); + + it('instrumenting find operation with chaining structures', async () => { + await User.find({ id: '_test' }).skip(1).limit(2).sort({ email: 'asc' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('find'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + expect(statement.options).toEqual({ + skip: 1, + limit: 2, + sort: { email: 1 }, + }); + }); + + it('instrumenting remove operation [deprecated]', async () => { + const user = await User.findOne({ email: 'john.doe@example.com' }); + await user!.remove(); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe('remove'); + }); + + it('instrumenting remove operation with callbacks [deprecated]', done => { + User.findOne({ email: 'john.doe@example.com' }).then(user => + user!.remove({ overwrite: true }, () => { + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'remove' + ); + expect(getStatement(spans[1] as ReadableSpan).options).toEqual({ + overwrite: true, + }); + done(); + }) + ); + }); + + it('instrumenting deleteOne operation', async () => { + await User.deleteOne({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'deleteOne' + ); + }); + + it('instrumenting updateOne operation on models', async () => { + const user = await User.findOne({ email: 'john.doe@example.com' }); + await user!.updateOne({ $inc: { age: 1 } }, { skip: 0 }); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[1] as ReadableSpan); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + + const statement = getStatement(spans[1] as ReadableSpan); + expect(statement.options).toEqual({ skip: 0 }); + expect(statement.updates).toEqual({ $inc: { age: 1 } }); + expect(statement.condition._id).toBeDefined(); + }); + + it('instrumenting updateOne operation', async () => { + await User.updateOne( + { email: 'john.doe@example.com' }, + { $inc: { age: 1 } }, + { skip: 0 } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({ skip: 0 }); + expect(statement.updates).toEqual({ $inc: { age: 1 } }); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting count operation [deprecated]', async () => { + await User.count({}); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('count'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting countDocuments operation', async () => { + await User.countDocuments({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'countDocuments' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting estimatedDocumentCount operation', async () => { + await User.estimatedDocumentCount(); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'estimatedDocumentCount' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting deleteMany operation', async () => { + await User.deleteMany(); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'deleteMany' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({}); + }); + + it('instrumenting findOne operation', async () => { + await User.findOne({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOne' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting update operation [deprecated]', async () => { + await User.update( + { email: 'john.doe@example.com' }, + { email: 'john.doe2@example.com' } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('update'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ email: 'john.doe2@example.com' }); + }); + + it('instrumenting updateOne operation', async () => { + await User.updateOne({ email: 'john.doe@example.com' }, { age: 55 }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateOne' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ age: 55 }); + }); + + it('instrumenting updateMany operation', async () => { + await User.updateMany({ age: 18 }, { isDeleted: true }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'updateMany' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ age: 18 }); + expect(statement.updates).toEqual({ isDeleted: true }); + }); + + it('instrumenting findOneAndDelete operation', async () => { + await User.findOneAndDelete({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOneAndDelete' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting findOneAndUpdate operation', async () => { + await User.findOneAndUpdate( + { email: 'john.doe@example.com' }, + { isUpdated: true } + ); + + const spans = getTestSpans(); + expect(spans.length).toBe(2); + assertSpan(spans[0] as ReadableSpan); + assertSpan(spans[1] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOne' + ); + expect(spans[1].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOneAndUpdate' + ); + const statement = getStatement(spans[1] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + expect(statement.updates).toEqual({ isUpdated: true }); + }); + + it('instrumenting findOneAndRemove operation', async () => { + await User.findOneAndRemove({ email: 'john.doe@example.com' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'findOneAndRemove' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.condition).toEqual({ email: 'john.doe@example.com' }); + }); + + it('instrumenting create operation', async () => { + const document = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe+1@example.com', + }; + await User.create(document); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe('save'); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.options).toEqual({}); + expect(statement.document).toEqual(expect.objectContaining(document)); + }); + + it('instrumenting aggregate operation', async () => { + await User.aggregate([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'aggregate' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.aggregatePipeline).toEqual([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + }); + + it('instrumenting aggregate operation with callback', done => { + User.aggregate( + [ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ], + () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_OPERATION]).toBe( + 'aggregate' + ); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.aggregatePipeline).toEqual([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + done(); + } + ); + }); + + it('instrumenting combined operation with async/await', async () => { + await User.find({ id: '_test' }).skip(1).limit(2).sort({ email: 'asc' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + const statement = getStatement(spans[0] as ReadableSpan); + expect(statement.condition).toEqual({ id: '_test' }); + expect(statement.options).toEqual({ + skip: 1, + limit: 2, + sort: { email: 1 }, + }); + }); + + it('empty dbStatementSerializer does not create a statement attribute', async () => { + instrumentation.disable(); + instrumentation.setConfig({ dbStatementSerializer: undefined }); + instrumentation.enable(); + await User.find({ id: '_test' }); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[SemanticAttributes.DB_STATEMENT]).toBe( + undefined + ); + }); + + it('projection is sent to serializer', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + dbStatementSerializer: (_operation: string, payload) => + JSON.stringify(payload), + }); + instrumentation.enable(); + + const projection = { firstName: 1 }; + await User.find({ id: '_test1' }, projection); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + const reqPayload = JSON.parse( + spans[0].attributes[SemanticAttributes.DB_STATEMENT] as string + ); + expect(reqPayload.fields).toStrictEqual(projection); + }); + + describe('responseHook', () => { + const RESPONSE = 'db.response'; + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + responseHook: (span, responseInfo) => + span.setAttribute(RESPONSE, JSON.stringify(responseInfo.response)), + }); + instrumentation.enable(); + }); + + it('responseHook works with async/await in exec patch', async () => { + await User.deleteOne({ email: 'john.doe@example.com' }); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(JSON.parse(spans[0].attributes[RESPONSE] as string)).toMatchObject( + { + deletedCount: 1, + } + ); + }); + + it('responseHook works with callback in exec patch', done => { + User.deleteOne({ email: 'john.doe@example.com' }, { lean: 1 }, () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect( + JSON.parse(spans[0].attributes[RESPONSE] as string) + ).toMatchObject({ + deletedCount: 1, + }); + done(); + }); + }); + + it('responseHook works with async/await in model methods patch', async () => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + const createdUser = await user.save(); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toEqual( + JSON.stringify(createdUser) + ); + }); + + it('responseHook works with callback in model methods patch', done => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + user.save((_err, createdUser) => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toEqual( + JSON.stringify(createdUser) + ); + done(); + }); + }); + + it('responseHook works with async/await in aggregate patch', async () => { + await User.aggregate([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(JSON.parse(spans[0].attributes[RESPONSE] as string)).toEqual([ + { _id: 'John', total: 0 }, + ]); + }); + + it('responseHook works with callback in aggregate patch', done => { + User.aggregate( + [ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ], + () => { + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(JSON.parse(spans[0].attributes[RESPONSE] as string)).toEqual([ + { _id: 'John', total: 0 }, + ]); + done(); + } + ); + }); + + it('error in response hook does not fail anything', async () => { + instrumentation.disable(); + instrumentation.setConfig({ + responseHook: () => { + throw new Error('some error'); + }, + }); + instrumentation.enable(); + await User.deleteOne({ email: 'john.doe@example.com' }); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[RESPONSE]).toBe(undefined); + }); + }); + + describe('moduleVersion reporting on hook', () => { + const VERSION_ATTR = 'module.version'; + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + responseHook: (span, responseInfo) => + span.setAttribute(VERSION_ATTR, responseInfo.moduleVersion!), + }); + instrumentation.enable(); + }); + + it('exec patch', async () => { + await User.deleteOne({ email: 'john.doe@example.com' }); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + + it('model methods patch', async () => { + const document = { + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }; + const user: IUser = new User(document); + await user.save(); + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + + it('aggregate patch', async () => { + await User.aggregate([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + + const spans = getTestSpans(); + expect(spans.length).toBe(1); + assertSpan(spans[0] as ReadableSpan); + expect(spans[0].attributes[VERSION_ATTR]).toMatch( + /\d{1,4}\.\d{1,4}\.\d{1,5}.*/ + ); + }); + }); + + describe('requireParentSpan', () => { + beforeEach(() => { + instrumentation.disable(); + instrumentation.setConfig({ + requireParentSpan: true, + }); + instrumentation.enable(); + }); + + it('should not start span on mongoose method', async () => { + await context.with(ROOT_CONTEXT, async () => { + const user: IUser = new User({ + firstName: 'Test first name', + lastName: 'Test last name', + email: 'test@example.com', + }); + await user.save(); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + + it('should not start span on find', async () => { + await context.with(ROOT_CONTEXT, async () => { + await User.find({ id: '_test' }); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + + it('should not start span on aggregate', async () => { + await context.with(ROOT_CONTEXT, async () => { + await User.aggregate([ + { $match: { firstName: 'John' } }, + { $group: { _id: 'John', total: { $sum: '$amount' } } }, + ]); + }); + + const spans = getTestSpans(); + expect(spans.length).toBe(0); + }); + }); +}); diff --git a/plugins/node/instrumentation-mongoose/test/user.ts b/plugins/node/instrumentation-mongoose/test/user.ts new file mode 100644 index 0000000000..0e8eff02eb --- /dev/null +++ b/plugins/node/instrumentation-mongoose/test/user.ts @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { Schema, Document } from 'mongoose'; +import * as mongoose from 'mongoose'; + +export interface IUser extends Document { + email: string; + firstName: string; + lastName: string; + age: number; +} + +const UserSchema: Schema = new Schema({ + email: { type: String, required: true, unique: true }, + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + age: { type: Number, required: false }, +}); + +// Export the model and return your IUser interface +const User = mongoose.model('User', UserSchema); +export default User; + +export const loadUsers = async () => { + await User.insertMany([ + new User({ + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + age: 18, + }), + new User({ + firstName: 'Jane', + lastName: 'Doe', + email: 'jane.doe@example.com', + age: 19, + }), + new User({ + firstName: 'Michael', + lastName: 'Fox', + email: 'michael.fox@example.com', + age: 16, + }), + ]); + await User.createIndexes(); +}; diff --git a/plugins/node/instrumentation-mongoose/tsconfig.json b/plugins/node/instrumentation-mongoose/tsconfig.json new file mode 100644 index 0000000000..5c3680dd33 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index b3771cad45..9671aba388 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -19,6 +19,7 @@ "packages/opentelemetry-propagation-utils": {}, "packages/opentelemetry-test-utils": {}, "plugins/node/instrumentation-lru-memoizer": {}, + "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-amqplib": {}, "plugins/node/instrumentation-fs": {}, "plugins/node/instrumentation-tedious": {}, From 6920a554b46bf8af5e00b60073d479feacb18dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= Date: Thu, 22 Sep 2022 21:03:18 +0200 Subject: [PATCH 08/19] fix: remove unneeded type exports in mongodb instrumentation (#1194) --- .../src/instrumentation.ts | 5 +- .../src/internal-types.ts | 178 ++++++++++++++++++ .../src/types.ts | 110 ----------- 3 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts index cd0a751e0a..acf2e2f90f 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts @@ -33,16 +33,15 @@ import { DbSystemValues, SemanticAttributes, } from '@opentelemetry/semantic-conventions'; +import { MongoDBInstrumentationConfig, CommandResult } from './types'; import { CursorState, MongodbCommandType, - MongoDBInstrumentationConfig, MongoInternalCommand, MongoInternalTopology, WireProtocolInternal, - CommandResult, V4Connection, -} from './types'; +} from './internal-types'; import { VERSION } from './version'; /** mongodb instrumentation plugin for OpenTelemetry */ diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts new file mode 100644 index 0000000000..8e9ff0245d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { Span } from '@opentelemetry/api'; + +export interface MongoDBInstrumentationExecutionResponseHook { + (span: Span, responseInfo: MongoResponseHookInformation): void; +} + +/** + * Function that can be used to serialize db.statement tag + * @param cmd - MongoDB command object + * + * @returns serialized string that will be used as the db.statement attribute. + */ +export type DbStatementSerializer = (cmd: Record) => string; + +export interface MongoDBInstrumentationConfig extends InstrumentationConfig { + /** + * If true, additional information about query parameters and + * results will be attached (as `attributes`) to spans representing + * database operations. + */ + enhancedDatabaseReporting?: boolean; + + /** + * Hook that allows adding custom span attributes based on the data + * returned from MongoDB actions. + * + * @default undefined + */ + responseHook?: MongoDBInstrumentationExecutionResponseHook; + + /** + * Custom serializer function for the db.statement tag + */ + dbStatementSerializer?: DbStatementSerializer; +} + +export type Func = (...args: unknown[]) => T; +export type MongoInternalCommand = { + findandmodify: boolean; + createIndexes: boolean; + count: boolean; + ismaster: boolean; + indexes?: unknown[]; + query?: Record; + limit?: number; + q?: Record; + u?: Record; +}; + +export type CursorState = { cmd: MongoInternalCommand } & Record< + string, + unknown +>; + +export interface MongoResponseHookInformation { + data: CommandResult; +} + +// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/connection/command_result.js +export type CommandResult = { + result?: unknown; + connection?: unknown; + message?: unknown; +}; + +// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/wireprotocol/index.js +export type WireProtocolInternal = { + insert: ( + server: MongoInternalTopology, + ns: string, + ops: unknown[], + options: unknown | Function, + callback?: Function + ) => unknown; + update: ( + server: MongoInternalTopology, + ns: string, + ops: unknown[], + options: unknown | Function, + callback?: Function + ) => unknown; + remove: ( + server: MongoInternalTopology, + ns: string, + ops: unknown[], + options: unknown | Function, + callback?: Function + ) => unknown; + killCursors: ( + server: MongoInternalTopology, + ns: string, + cursorState: CursorState, + callback: Function + ) => unknown; + getMore: ( + server: MongoInternalTopology, + ns: string, + cursorState: CursorState, + batchSize: number, + options: unknown | Function, + callback?: Function + ) => unknown; + query: ( + server: MongoInternalTopology, + ns: string, + cmd: MongoInternalCommand, + cursorState: CursorState, + options: unknown | Function, + callback?: Function + ) => unknown; + command: ( + server: MongoInternalTopology, + ns: string, + cmd: MongoInternalCommand, + options: unknown | Function, + callback?: Function + ) => unknown; +}; + +// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/topologies/server.js#L172 +// https://github.com/mongodb/node-mongodb-native/blob/2.2/lib/server.js#L174 +export type MongoInternalTopology = { + s?: { + // those are for mongodb@3 + options?: { + host?: string; + port?: number; + servername?: string; + }; + // those are for mongodb@2 + host?: string; + port?: number; + }; + // mongodb@3 with useUnifiedTopology option + description?: { + address?: string; + }; +}; + +export enum MongodbCommandType { + CREATE_INDEXES = 'createIndexes', + FIND_AND_MODIFY = 'findAndModify', + IS_MASTER = 'isMaster', + COUNT = 'count', + UNKNOWN = 'unknown', +} + +// https://github.com/mongodb/js-bson/blob/main/src/bson.ts +export type Document = { + [key: string]: any; +}; + +// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connection.ts +export type V4Connection = { + command( + ns: any, + cmd: Document, + options: undefined | unknown, + callback: any + ): void; +}; diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts b/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts index d81c97b08d..9c75c45d14 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts @@ -51,24 +51,6 @@ export interface MongoDBInstrumentationConfig extends InstrumentationConfig { dbStatementSerializer?: DbStatementSerializer; } -export type Func = (...args: unknown[]) => T; -export type MongoInternalCommand = { - findandmodify: boolean; - createIndexes: boolean; - count: boolean; - ismaster: boolean; - indexes?: unknown[]; - query?: Record; - limit?: number; - q?: Record; - u?: Record; -}; - -export type CursorState = { cmd: MongoInternalCommand } & Record< - string, - unknown ->; - export interface MongoResponseHookInformation { data: CommandResult; } @@ -79,95 +61,3 @@ export type CommandResult = { connection?: unknown; message?: unknown; }; - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/wireprotocol/index.js -export type WireProtocolInternal = { - insert: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function - ) => unknown; - update: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function - ) => unknown; - remove: ( - server: MongoInternalTopology, - ns: string, - ops: unknown[], - options: unknown | Function, - callback?: Function - ) => unknown; - killCursors: ( - server: MongoInternalTopology, - ns: string, - cursorState: CursorState, - callback: Function - ) => unknown; - getMore: ( - server: MongoInternalTopology, - ns: string, - cursorState: CursorState, - batchSize: number, - options: unknown | Function, - callback?: Function - ) => unknown; - query: ( - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - cursorState: CursorState, - options: unknown | Function, - callback?: Function - ) => unknown; - command: ( - server: MongoInternalTopology, - ns: string, - cmd: MongoInternalCommand, - options: unknown | Function, - callback?: Function - ) => unknown; -}; - -// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/topologies/server.js#L172 -// https://github.com/mongodb/node-mongodb-native/blob/2.2/lib/server.js#L174 -export type MongoInternalTopology = { - s?: { - // those are for mongodb@3 - options?: { - host?: string; - port?: number; - servername?: string; - }; - // those are for mongodb@2 - host?: string; - port?: number; - }; - // mongodb@3 with useUnifiedTopology option - description?: { - address?: string; - }; -}; - -export enum MongodbCommandType { - CREATE_INDEXES = 'createIndexes', - FIND_AND_MODIFY = 'findAndModify', - IS_MASTER = 'isMaster', - COUNT = 'count', - UNKNOWN = 'unknown', -} - -// https://github.com/mongodb/node-mongodb-native/blob/v4.2.2/src/cmap/connection.ts -export type V4Connection = { - command( - ns: any, - cmd: Document, - options: undefined | unknown, - callback: any - ): void; -}; From 88730577d8d997896e5a430dbd3a8157c15eb68a Mon Sep 17 00:00:00 2001 From: Rauno Viskus Date: Fri, 23 Sep 2022 10:26:40 +0300 Subject: [PATCH 09/19] test: fix tests for restify >=5 (#1191) * fix(restify): upgrade package to fix type problem * fix(restify): updated so types are not leaking through #1132 * fix(restify): lint fix * fix(restify): final fix up for linter * style: avoid checking in manually edited package.json * test: require restify again for every test * test: differenciate between thorwing and failing gracefully * test: use custom error * feat: support restify@5 * feat: support restify@7 * feat: support restify@8 * test: cleanup * test: anyfy arguments on the anonymous handler * chore: add tav + update supported versions * fix: turn off tests on node@18 which is not supported for restify * refactor: remove the import to after setting up instrumentation Co-authored-by: jscherer92 --- .github/workflows/unit-test.yml | 2 +- .../.tav.yml | 3 + .../README.md | 2 +- .../package.json | 18 +++-- .../src/constants.ts | 2 +- .../src/instrumentation.ts | 20 +++--- .../src/types.ts | 2 +- .../test/restify.test.ts | 72 ++++++++++++++----- 8 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 plugins/node/opentelemetry-instrumentation-restify/.tav.yml diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a3f83977c0..75048b0277 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -11,11 +11,11 @@ jobs: matrix: node: ["14", "16", "18"] include: - # tests fail on node@18, incompatibility with nock? - node: "18" lerna-extra-args: >- --ignore @opentelemetry/resource-detector-alibaba-cloud --ignore @opentelemetry/instrumentation-fastify + --ignore @opentelemetry/instrumentation-restify runs-on: ubuntu-latest services: memcached: diff --git a/plugins/node/opentelemetry-instrumentation-restify/.tav.yml b/plugins/node/opentelemetry-instrumentation-restify/.tav.yml new file mode 100644 index 0000000000..62556552fb --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-restify/.tav.yml @@ -0,0 +1,3 @@ +restify: + versions: "4.3.4 || 5.2.0 || 6.4.0 || 7.7.0 || ^8.4.0" + commands: npm run test diff --git a/plugins/node/opentelemetry-instrumentation-restify/README.md b/plugins/node/opentelemetry-instrumentation-restify/README.md index 00ddb638e4..9ef35ea4da 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/README.md +++ b/plugins/node/opentelemetry-instrumentation-restify/README.md @@ -17,7 +17,7 @@ npm install --save @opentelemetry/instrumentation-restify ### Supported Versions -- `>=4.0.0` +- `>=4.0.0 <9` ## Usage diff --git a/plugins/node/opentelemetry-instrumentation-restify/package.json b/plugins/node/opentelemetry-instrumentation-restify/package.json index 6f064e8397..0a3c8b99bf 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/package.json +++ b/plugins/node/opentelemetry-instrumentation-restify/package.json @@ -6,16 +6,17 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js-contrib", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", - "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", + "compile": "tsc -p .", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/instrumentation-restify --include-dependencies", + "prepare": "npm run compile", "prewatch": "npm run precompile", + "tdd": "yarn test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", + "test-all-versions": "tav", "version:update": "node ../../../scripts/version-update.js", - "compile": "tsc -p .", - "prepare": "npm run compile", "watch": "tsc -w" }, "keywords": [ @@ -51,19 +52,22 @@ "@opentelemetry/sdk-trace-node": "^1.3.1", "@types/mocha": "7.0.2", "@types/node": "16.11.21", + "@types/restify": "4.3.8", + "@types/semver": "^7.3.12", "gts": "3.1.0", "mocha": "7.2.0", "nyc": "15.1.0", - "restify": "4.3.4", + "restify": "8.6.1", "rimraf": "3.0.2", + "semver": "^7.3.7", + "test-all-versions": "^5.0.1", "ts-mocha": "10.0.0", "typescript": "4.3.5" }, "dependencies": { "@opentelemetry/core": "^1.0.0", "@opentelemetry/instrumentation": "^0.32.0", - "@opentelemetry/semantic-conventions": "^1.0.0", - "@types/restify": "4.3.8" + "@opentelemetry/semantic-conventions": "^1.0.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-restify#readme" } diff --git a/plugins/node/opentelemetry-instrumentation-restify/src/constants.ts b/plugins/node/opentelemetry-instrumentation-restify/src/constants.ts index 24a21e6251..0ba23dccf5 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/src/constants.ts +++ b/plugins/node/opentelemetry-instrumentation-restify/src/constants.ts @@ -24,4 +24,4 @@ export const RESTIFY_METHODS = [ 'patch', ]; export const MODULE_NAME = 'restify'; -export const SUPPORTED_VERSIONS = ['>=4.0.0']; +export const SUPPORTED_VERSIONS = ['>=4.0.0 <9']; diff --git a/plugins/node/opentelemetry-instrumentation-restify/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-restify/src/instrumentation.ts index 89b39ad423..baa98469a1 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-restify/src/instrumentation.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import type * as types from './types'; +import type * as restify from 'restify'; + import * as api from '@opentelemetry/api'; -import * as restify from 'restify'; import { Server } from 'restify'; -import * as types from './types'; +import { LayerType } from './types'; import * as AttributeNames from './enums/AttributeNames'; import { VERSION } from './version'; import * as constants from './constants'; @@ -34,9 +36,7 @@ import { getRPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core'; const { diag } = api; -export class RestifyInstrumentation extends InstrumentationBase< - typeof restify -> { +export class RestifyInstrumentation extends InstrumentationBase { constructor(config: InstrumentationConfig = {}) { super(`@opentelemetry/instrumentation-${constants.MODULE_NAME}`, VERSION); } @@ -45,7 +45,7 @@ export class RestifyInstrumentation extends InstrumentationBase< private _isDisabled = false; init() { - const module = new InstrumentationNodeModuleDefinition( + const module = new InstrumentationNodeModuleDefinition( constants.MODULE_NAME, constants.SUPPORTED_VERSIONS, (moduleExports, moduleVersion) => { @@ -55,7 +55,7 @@ export class RestifyInstrumentation extends InstrumentationBase< ); module.files.push( - new InstrumentationNodeModuleFile( + new InstrumentationNodeModuleFile( 'restify/lib/server.js', constants.SUPPORTED_VERSIONS, (moduleExports, moduleVersion) => { @@ -113,7 +113,7 @@ export class RestifyInstrumentation extends InstrumentationBase< return original.call( this, instrumentation._handlerPatcher( - { type: types.LayerType.MIDDLEWARE, methodName }, + { type: LayerType.MIDDLEWARE, methodName }, handler ) ); @@ -131,7 +131,7 @@ export class RestifyInstrumentation extends InstrumentationBase< this, path, ...instrumentation._handlerPatcher( - { type: types.LayerType.REQUEST_HANDLER, path, methodName }, + { type: LayerType.REQUEST_HANDLER, path, methodName }, handler ) ); @@ -168,7 +168,7 @@ export class RestifyInstrumentation extends InstrumentationBase< const fnName = handler.name || undefined; const spanName = - metadata.type === types.LayerType.REQUEST_HANDLER + metadata.type === LayerType.REQUEST_HANDLER ? `request handler - ${route}` : `middleware - ${fnName || 'anonymous'}`; const attributes = { diff --git a/plugins/node/opentelemetry-instrumentation-restify/src/types.ts b/plugins/node/opentelemetry-instrumentation-restify/src/types.ts index 89da9fd49c..d6280a6bda 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-restify/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Span } from '@opentelemetry/api'; -import * as restify from 'restify'; +import type * as restify from 'restify'; export enum LayerType { MIDDLEWARE = 'middleware', diff --git a/plugins/node/opentelemetry-instrumentation-restify/test/restify.test.ts b/plugins/node/opentelemetry-instrumentation-restify/test/restify.test.ts index ba8845216c..27c35b3fb6 100644 --- a/plugins/node/opentelemetry-instrumentation-restify/test/restify.test.ts +++ b/plugins/node/opentelemetry-instrumentation-restify/test/restify.test.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import * as restify from 'restify'; import { context, trace } from '@opentelemetry/api'; import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -28,10 +27,19 @@ import RestifyInstrumentation from '../src'; import * as types from '../src/types'; const plugin = new RestifyInstrumentation(); +import * as semver from 'semver'; import * as assert from 'assert'; import * as http from 'http'; import { AddressInfo } from 'net'; +import * as restify from 'restify'; +const LIB_VERSION = require('restify/package.json').version; + +const assertIsVersion = (str: any) => { + assert.strictEqual(typeof str, 'string'); + assert(/^[0-9]+\.[0-9]+\.[0-9]+$/.test(str)); +}; + const httpRequest = { get: (options: http.ClientRequestArgs | string) => { return new Promise((resolve, reject) => { @@ -65,6 +73,12 @@ const defer = (): { return { promise, resolve, reject }; }; +class AppError extends Error { + toJSON() { + return { message: this.message }; + } +} + const useHandler: restify.RequestHandler = (req, res, next) => { // only run if route was found next(); @@ -73,7 +87,10 @@ const getHandler: restify.RequestHandler = (req, res, next) => { res.send({ route: req?.params?.param }); }; const throwError: restify.RequestHandler = (req, res, next) => { - throw new Error('NOK'); + throw new AppError('NOK'); +}; +const returnError: restify.RequestHandler = (req, res, next) => { + next(new AppError('NOK')); }; const createServer = async (setupRoutes?: Function) => { @@ -83,14 +100,15 @@ const createServer = async (setupRoutes?: Function) => { setupRoutes(server); } else { // to force an anonymous fn for testing - server.pre((req, res, next) => { + server.pre((res: any, req: any, next: any) => { // run before routing next(); }); server.use(useHandler); server.get('/route/:param', getHandler); - server.get('/failing', throwError); + server.get('/thowing', throwError); + server.get('/erroring', returnError); } await new Promise(resolve => server.listen(0, resolve)); @@ -150,7 +168,7 @@ describe('Restify Instrumentation', () => { assert.strictEqual(span.attributes['restify.method'], 'pre'); assert.strictEqual(span.attributes['restify.type'], 'middleware'); assert.strictEqual(span.attributes['restify.name'], undefined); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } { // span from use @@ -160,7 +178,7 @@ describe('Restify Instrumentation', () => { assert.strictEqual(span.attributes['restify.method'], 'use'); assert.strictEqual(span.attributes['restify.type'], 'middleware'); assert.strictEqual(span.attributes['restify.name'], 'useHandler'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } { // span from get @@ -173,7 +191,7 @@ describe('Restify Instrumentation', () => { 'request_handler' ); assert.strictEqual(span.attributes['restify.name'], 'getHandler'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } } ); @@ -199,7 +217,7 @@ describe('Restify Instrumentation', () => { assert.strictEqual(span.attributes['restify.method'], 'pre'); assert.strictEqual(span.attributes['restify.type'], 'middleware'); assert.strictEqual(span.attributes['restify.name'], undefined); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } assert.strictEqual( res, @@ -209,16 +227,32 @@ describe('Restify Instrumentation', () => { ); }); - it('should create a span for an endpoint that threw', async () => { + it('should create a span for an endpoint that called done(error)', async () => { const rootSpan = tracer.startSpan('clientSpan'); await context.with( trace.setSpan(context.active(), rootSpan), async () => { - await httpRequest.get(`http://localhost:${port}/failing`); + const result = await httpRequest.get( + `http://localhost:${port}/erroring` + ); rootSpan.end(); assert.strictEqual(memoryExporter.getFinishedSpans().length, 4); + if (semver.satisfies(LIB_VERSION, '>=8')) { + assert.deepEqual( + result, + '{"code":"Internal","message":"Error: NOK"}' + ); + } else if (semver.satisfies(LIB_VERSION, '>=7 <8')) { + assert.deepEqual( + result, + '{"code":"Internal","message":"caused by Error: NOK"}' + ); + } else { + assert.deepEqual(result, '{"message":"NOK"}'); + } + { // span from pre const span = memoryExporter.getFinishedSpans()[0]; @@ -227,30 +261,30 @@ describe('Restify Instrumentation', () => { assert.strictEqual(span.attributes['restify.method'], 'pre'); assert.strictEqual(span.attributes['restify.type'], 'middleware'); assert.strictEqual(span.attributes['restify.name'], undefined); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } { // span from use const span = memoryExporter.getFinishedSpans()[1]; assert.notStrictEqual(span, undefined); - assert.strictEqual(span.attributes['http.route'], '/failing'); + assert.strictEqual(span.attributes['http.route'], '/erroring'); assert.strictEqual(span.attributes['restify.method'], 'use'); assert.strictEqual(span.attributes['restify.type'], 'middleware'); assert.strictEqual(span.attributes['restify.name'], 'useHandler'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } { // span from get const span = memoryExporter.getFinishedSpans()[2]; assert.notStrictEqual(span, undefined); - assert.strictEqual(span.attributes['http.route'], '/failing'); + assert.strictEqual(span.attributes['http.route'], '/erroring'); assert.strictEqual(span.attributes['restify.method'], 'get'); assert.strictEqual( span.attributes['restify.type'], 'request_handler' ); - assert.strictEqual(span.attributes['restify.name'], 'throwError'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assert.strictEqual(span.attributes['restify.name'], 'returnError'); + assertIsVersion(span.attributes['restify.version']); } } ); @@ -318,7 +352,7 @@ describe('Restify Instrumentation', () => { 'request_handler' ); assert.strictEqual(span.attributes['restify.name'], 'getHandler'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } assert.strictEqual(res, '{"route":"hello"}'); } finally { @@ -376,7 +410,7 @@ describe('Restify Instrumentation', () => { 'request_handler' ); assert.strictEqual(span.attributes['restify.name'], 'asyncHandler'); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } } finally { testLocalServer.close(); @@ -441,7 +475,7 @@ describe('Restify Instrumentation', () => { span.attributes['restify.name'], 'promiseReturningHandler' ); - assert.strictEqual(span.attributes['restify.version'], 'n/a'); + assertIsVersion(span.attributes['restify.version']); } } finally { testLocalServer.close(); From 227ae20bbdec1e5da89b170c9d1c02d01c153c6b Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Sat, 24 Sep 2022 04:40:27 -0400 Subject: [PATCH 10/19] ci: introduce no extraneous dependencies lint check (#1086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marc Pichler Co-authored-by: Gerhard Stöbich --- eslint.config.js | 6 ++++-- .../node/opentelemetry-instrumentation-express/src/types.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6ac74884ca..1b57d26fd1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,8 @@ module.exports = { plugins: [ "@typescript-eslint", - "header" + "header", + "import" ], extends: [ "./node_modules/gts", @@ -32,7 +33,8 @@ module.exports = { pattern: / \* Copyright The OpenTelemetry Authors[\r\n]+ \*[\r\n]+ \* Licensed under the Apache License, Version 2\.0 \(the \"License\"\);[\r\n]+ \* you may not use this file except in compliance with the License\.[\r\n]+ \* You may obtain a copy of the License at[\r\n]+ \*[\r\n]+ \* https:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0[\r\n]+ \*[\r\n]+ \* Unless required by applicable law or agreed to in writing, software[\r\n]+ \* distributed under the License is distributed on an \"AS IS\" BASIS,[\r\n]+ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.[\r\n]+ \* See the License for the specific language governing permissions and[\r\n]+ \* limitations under the License\./gm, template: `\n * Copyright The OpenTelemetry Authors\n *\n * Licensed under the Apache License, Version 2.0 (the "License");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an "AS IS" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n ` - }]] + }]], + "import/no-extraneous-dependencies": ["error", { devDependencies: ["test/**/*.ts"] }], }, overrides: [ { diff --git a/plugins/node/opentelemetry-instrumentation-express/src/types.ts b/plugins/node/opentelemetry-instrumentation-express/src/types.ts index abf9817bbb..0c1f922f13 100644 --- a/plugins/node/opentelemetry-instrumentation-express/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-express/src/types.ts @@ -15,7 +15,7 @@ */ import { kLayerPatched } from './'; -import { Request } from 'express'; +import type { Request } from 'express'; import { Span, SpanAttributes } from '@opentelemetry/api'; import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { ExpressLayerType } from './enums/ExpressLayerType'; From cb83d300582b4d485be56563634cd3859069004c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Einar=20Nor=C3=B0fj=C3=B6r=C3=B0?= Date: Sat, 24 Sep 2022 07:07:13 -0400 Subject: [PATCH 11/19] feat(propagation-utils): end pub-sub process span on promise settled (#1055) Co-authored-by: Rauno Viskus Co-authored-by: Rauno Viskus Co-authored-by: Valentin Marchaud --- .../package.json | 10 ++ .../src/pubsub-propagation.ts | 17 ++- .../test/pubsub-propagation.test.ts | 131 ++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 packages/opentelemetry-propagation-utils/test/pubsub-propagation.test.ts diff --git a/packages/opentelemetry-propagation-utils/package.json b/packages/opentelemetry-propagation-utils/package.json index f3b26576b3..f5b0f4001c 100644 --- a/packages/opentelemetry-propagation-utils/package.json +++ b/packages/opentelemetry-propagation-utils/package.json @@ -15,6 +15,8 @@ "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/propagation-utils --include-dependencies", "prepare": "npm run compile", "prewatch": "npm run precompile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", "watch": "tsc --build --watch tsconfig.json tsconfig.esm.json" }, "repository": "open-telemetry/opentelemetry-js-contrib", @@ -43,8 +45,16 @@ }, "devDependencies": { "@opentelemetry/api": "^1.0.0", + "@opentelemetry/contrib-test-utils": "^0.31.0", + "@types/mocha": "^9.1.1", "@types/node": "16.11.21", + "@types/sinon": "^10.0.11", + "expect": "27.4.2", "gts": "3.1.0", + "mocha": "7.2.0", + "nyc": "15.1.0", + "sinon": "13.0.1", + "ts-mocha": "10.0.0", "typescript": "4.3.5" } } diff --git a/packages/opentelemetry-propagation-utils/src/pubsub-propagation.ts b/packages/opentelemetry-propagation-utils/src/pubsub-propagation.ts index 8302011ad8..1c7d2d5ad9 100644 --- a/packages/opentelemetry-propagation-utils/src/pubsub-propagation.ts +++ b/packages/opentelemetry-propagation-utils/src/pubsub-propagation.ts @@ -56,6 +56,11 @@ const patchArrayFilter = ( }); }; +function isPromise(value: unknown): value is Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return typeof (value as any)?.then === 'function'; +} + const patchArrayFunction = ( messages: OtelProcessedMessage[], functionName: 'forEach' | 'map', @@ -77,10 +82,18 @@ const patchArrayFunction = ( if (!messageSpan) return callback.apply(this, callbackArgs); const res = context.with(trace.setSpan(loopContext, messageSpan), () => { + let result: Promise | unknown; try { - return callback.apply(this, callbackArgs); + result = callback.apply(this, callbackArgs); + if (isPromise(result)) { + const endSpan = () => message[END_SPAN_FUNCTION]?.(); + result.then(endSpan, endSpan); + } + return result; } finally { - message[END_SPAN_FUNCTION]?.(); + if (!isPromise(result)) { + message[END_SPAN_FUNCTION]?.(); + } } }); diff --git a/packages/opentelemetry-propagation-utils/test/pubsub-propagation.test.ts b/packages/opentelemetry-propagation-utils/test/pubsub-propagation.test.ts new file mode 100644 index 0000000000..8c853b791f --- /dev/null +++ b/packages/opentelemetry-propagation-utils/test/pubsub-propagation.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 utils from '../src/pubsub-propagation'; +import { + getTestSpans, + registerInstrumentationTestingProvider, + resetMemoryExporter, +} from '@opentelemetry/contrib-test-utils'; +import { ROOT_CONTEXT, trace } from '@opentelemetry/api'; +import * as expect from 'expect'; + +registerInstrumentationTestingProvider(); + +const tracer = trace.getTracer('test'); + +afterEach(() => { + resetMemoryExporter(); +}); + +describe('Pubsub propagation', () => { + it('Span ends immediately when the function returns a non-promise', () => { + const messages = [{}]; + utils.patchMessagesArrayToStartProcessSpans({ + messages, + tracer, + parentContext: ROOT_CONTEXT, + messageToSpanDetails: () => ({ + name: 'test', + parentContext: ROOT_CONTEXT, + attributes: {}, + }), + }); + utils.patchArrayForProcessSpans(messages, tracer, ROOT_CONTEXT); + + expect(getTestSpans().length).toBe(0); + + messages.map(x => x); + + expect(getTestSpans().length).toBe(1); + expect(getTestSpans()[0]).toMatchObject({ name: 'test process' }); + }); + + it('Span ends on promise-resolution', () => { + const messages = [{}]; + utils.patchMessagesArrayToStartProcessSpans({ + messages, + tracer, + parentContext: ROOT_CONTEXT, + messageToSpanDetails: () => ({ + name: 'test', + parentContext: ROOT_CONTEXT, + attributes: {}, + }), + }); + utils.patchArrayForProcessSpans(messages, tracer, ROOT_CONTEXT); + + expect(getTestSpans().length).toBe(0); + + let resolve: (value: unknown) => void; + + messages.map( + () => + new Promise(res => { + resolve = res; + }) + ); + + expect(getTestSpans().length).toBe(0); + + // @ts-expect-error Typescript thinks this value is used before assignment + resolve(undefined); + + // We use setTimeout here to make sure our assertations run + // after the promise resolves + return new Promise(res => setTimeout(res, 0)).then(() => { + expect(getTestSpans().length).toBe(1); + expect(getTestSpans()[0]).toMatchObject({ name: 'test process' }); + }); + }); + + it('Span ends on promise-rejection', () => { + const messages = [{}]; + utils.patchMessagesArrayToStartProcessSpans({ + messages, + tracer, + parentContext: ROOT_CONTEXT, + messageToSpanDetails: () => ({ + name: 'test', + parentContext: ROOT_CONTEXT, + attributes: {}, + }), + }); + utils.patchArrayForProcessSpans(messages, tracer, ROOT_CONTEXT); + + expect(getTestSpans().length).toBe(0); + + let reject: (value: unknown) => void; + + messages.map( + () => + new Promise((_, rej) => { + reject = rej; + }) + ); + + expect(getTestSpans().length).toBe(0); + + // @ts-expect-error Typescript thinks this value is used before assignment + reject(new Error('Failed')); + + // We use setTimeout here to make sure our assertations run + // after the promise resolves + return new Promise(res => setTimeout(res, 0)).then(() => { + expect(getTestSpans().length).toBe(1); + expect(getTestSpans()[0]).toMatchObject({ name: 'test process' }); + }); + }); +}); From d932d3edcbf41685ca0af546347450fa81444b4e Mon Sep 17 00:00:00 2001 From: Mark Reynolds <53436180+markrzen@users.noreply.github.com> Date: Mon, 26 Sep 2022 14:55:47 -0400 Subject: [PATCH 12/19] feat(opentelemetry-instrumentation-fastify): Support Fastify V4 also (#1164) --- .../README.md | 2 +- .../package.json | 6 ++--- .../src/instrumentation.ts | 11 +++++++- .../test/instrumentation.test.ts | 26 ++++++++++++------- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/plugins/node/opentelemetry-instrumentation-fastify/README.md b/plugins/node/opentelemetry-instrumentation-fastify/README.md index 9835aa225b..89c4a6e1d0 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/README.md +++ b/plugins/node/opentelemetry-instrumentation-fastify/README.md @@ -17,7 +17,7 @@ npm install --save @opentelemetry/instrumentation-http @opentelemetry/instrument ### Supported Versions -- `^3.0.0` +- fastify: `^3.0.0 || ^4.0.0` ## Usage diff --git a/plugins/node/opentelemetry-instrumentation-fastify/package.json b/plugins/node/opentelemetry-instrumentation-fastify/package.json index 65d3a7f959..672c06fbcf 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/package.json +++ b/plugins/node/opentelemetry-instrumentation-fastify/package.json @@ -45,15 +45,15 @@ "@opentelemetry/api": "^1.0.0" }, "devDependencies": { + "@fastify/express": "^2.0.2", "@opentelemetry/api": "^1.0.0", "@opentelemetry/context-async-hooks": "^1.3.1", "@opentelemetry/instrumentation-http": "0.30.0", - "@opentelemetry/sdk-trace-node": "^1.3.1", "@opentelemetry/sdk-trace-base": "^1.3.1", + "@opentelemetry/sdk-trace-node": "^1.3.1", "@types/express": "4.17.13", "@types/mocha": "7.0.2", "@types/node": "16.11.21", - "fastify-express": "0.3.3", "gts": "3.1.0", "mocha": "7.2.0", "nyc": "15.1.0", @@ -65,7 +65,7 @@ "@opentelemetry/core": "^1.0.0", "@opentelemetry/instrumentation": "^0.32.0", "@opentelemetry/semantic-conventions": "^1.0.0", - "fastify": "^3.19.2" + "fastify": "^4.5.3" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-fastify#readme" } diff --git a/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts index cfe666c559..b267f03336 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-fastify/src/instrumentation.ts @@ -62,7 +62,7 @@ export class FastifyInstrumentation extends InstrumentationBase { return [ new InstrumentationNodeModuleDefinition( 'fastify', - ['^3.0.0'], + ['^3.0.0', '^4.0.0'], (moduleExports, moduleVersion) => { this._diag.debug(`Applying patch for fastify@${moduleVersion}`); return this._patchConstructor(moduleExports); @@ -100,6 +100,8 @@ export class FastifyInstrumentation extends InstrumentationBase { syncFunctionWithDone: boolean ): () => Promise { const instrumentation = this; + this._diag.debug('Patching fastify route.handler function'); + return function (this: any, ...args: unknown[]): Promise { if (!instrumentation.isEnabled()) { return original.apply(this, args); @@ -156,6 +158,8 @@ export class FastifyInstrumentation extends InstrumentationBase { original: FastifyInstance['addHook'] ) => () => FastifyInstance { const instrumentation = this; + this._diag.debug('Patching fastify server.addHook function'); + return function ( original: FastifyInstance['addHook'] ): () => FastifyInstance { @@ -188,6 +192,7 @@ export class FastifyInstrumentation extends InstrumentationBase { original: () => FastifyInstance ): () => FastifyInstance { const instrumentation = this; + this._diag.debug('Patching fastify constructor function'); function fastify(this: FastifyInstance, ...args: any) { const app: FastifyInstance = original.apply(this, args); @@ -206,6 +211,8 @@ export class FastifyInstrumentation extends InstrumentationBase { public _patchSend() { const instrumentation = this; + this._diag.debug('Patching fastify reply.send function'); + return function patchSend( original: () => FastifyReply ): () => FastifyReply { @@ -233,6 +240,8 @@ export class FastifyInstrumentation extends InstrumentationBase { public _hookPreHandler() { const instrumentation = this; + this._diag.debug('Patching fastify preHandler function'); + return function preHandler( this: any, request: FastifyRequest, diff --git a/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts index 8588c24c60..aa17f3b246 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts +++ b/plugins/node/opentelemetry-instrumentation-fastify/test/instrumentation.test.ts @@ -67,7 +67,7 @@ provider.addSpanProcessor(spanProcessor); instrumentation.enable(); httpInstrumentation.enable(); -import 'fastify-express'; +import '@fastify/express'; import { FastifyInstance } from 'fastify/types/instance'; const Fastify = require('fastify'); @@ -94,7 +94,7 @@ describe('fastify', () => { let app: FastifyInstance; async function startServer(): Promise { - const address = await app.listen(0); + const address = await app.listen({ port: 0 }); const url = new URL(address); PORT = parseInt(url.port, 10); } @@ -102,7 +102,7 @@ describe('fastify', () => { beforeEach(async () => { instrumentation.enable(); app = Fastify(); - app.register(require('fastify-express')); + app.register(require('@fastify/express')); }); afterEach(async () => { @@ -145,7 +145,7 @@ describe('fastify', () => { const span = spans[3]; assert.deepStrictEqual(span.attributes, { 'fastify.type': 'request_handler', - 'plugin.name': 'fastify-express', + 'plugin.name': 'fastify -> @fastify/express', [SemanticAttributes.HTTP_ROUTE]: '/test', }); assert.strictEqual(span.name, `request handler - ${ANONYMOUS_NAME}`); @@ -168,7 +168,7 @@ describe('fastify', () => { assert.deepStrictEqual(span.attributes, { 'fastify.type': 'request_handler', 'fastify.name': 'namedHandler', - 'plugin.name': 'fastify-express', + 'plugin.name': 'fastify -> @fastify/express', [SemanticAttributes.HTTP_ROUTE]: '/test', }); assert.strictEqual(span.name, 'request handler - namedHandler'); @@ -232,7 +232,7 @@ describe('fastify', () => { assert.strictEqual(span.name, 'middleware - runConnect'); assert.deepStrictEqual(span.attributes, { 'fastify.type': 'middleware', - 'plugin.name': 'fastify-express', + 'plugin.name': 'fastify -> @fastify/express', 'hook.name': 'onRequest', }); @@ -248,7 +248,7 @@ describe('fastify', () => { assert.strictEqual(span.name, 'middleware - enhanceRequest'); assert.deepStrictEqual(span.attributes, { 'fastify.type': 'middleware', - 'plugin.name': 'fastify-express', + 'plugin.name': 'fastify -> @fastify/express', 'hook.name': 'onRequest', }); @@ -338,12 +338,16 @@ describe('fastify', () => { // done was not yet called from the hook, so it should not end the span const preDoneSpans = getSpans().filter( - s => !s.attributes[AttributeNames.PLUGIN_NAME] + s => + !s.attributes[AttributeNames.PLUGIN_NAME] || + s.attributes[AttributeNames.PLUGIN_NAME] === 'fastify' ); assert.strictEqual(preDoneSpans.length, 0); hookDone!(); const postDoneSpans = getSpans().filter( - s => !s.attributes[AttributeNames.PLUGIN_NAME] + s => + !s.attributes[AttributeNames.PLUGIN_NAME] || + s.attributes[AttributeNames.PLUGIN_NAME] === 'fastify' ); assert.strictEqual(postDoneSpans.length, 1); }); @@ -367,7 +371,9 @@ describe('fastify', () => { await startServer(); await httpRequest.get(`http://localhost:${PORT}/test`); const spans = getSpans().filter( - s => !s.attributes[AttributeNames.PLUGIN_NAME] + s => + !s.attributes[AttributeNames.PLUGIN_NAME] || + s.attributes[AttributeNames.PLUGIN_NAME] === 'fastify' ); assert.strictEqual(spans.length, 1); }); From 3898b11800f857c75c286f22c4633b5baf4e1f84 Mon Sep 17 00:00:00 2001 From: Henri Normak Date: Tue, 27 Sep 2022 10:54:51 +0300 Subject: [PATCH 13/19] feat: add dataloader instrumentation (#1171) * feat: add dataloader instrumentation * fix: linter errors and a few mistypings * docs: add component_owners entry for dataloader instrumentation * fix: avoid double shimming of the batch load function * test: cover more lines in tests * refactor: remove prefix from directory name * feat: add configuration option for requiring parent span * chore: drop version number to 0.1.0 * chore: add dataloader instrumentation to release-please-config * feat: add dataloader instrumentation to auto-instrumentations-node * fix: use correct version of Node * fix: add missing export Also make instrumentation export explicit * test: adjust assertion to be correct * chore: change node types to be consistent with other plugins Co-authored-by: Rauno Viskus --- .github/component_owners.yml | 2 + .../auto-instrumentations-node/README.md | 1 + .../auto-instrumentations-node/package.json | 1 + .../auto-instrumentations-node/src/utils.ts | 2 + .../test/utils.test.ts | 3 +- .../instrumentation-dataloader/.eslintignore | 1 + .../instrumentation-dataloader/.eslintrc.js | 7 + .../instrumentation-dataloader/.npmignore | 4 + .../node/instrumentation-dataloader/.tav.yml | 4 + .../instrumentation-dataloader/CHANGELOG.md | 1 + .../node/instrumentation-dataloader/LICENSE | 201 ++++++++++++ .../node/instrumentation-dataloader/README.md | 70 +++++ .../instrumentation-dataloader/package.json | 68 +++++ .../instrumentation-dataloader/src/index.ts | 18 ++ .../src/instrumentation.ts | 248 +++++++++++++++ .../instrumentation-dataloader/src/types.ts | 26 ++ .../test/dataloader.test.ts | 287 ++++++++++++++++++ .../instrumentation-dataloader/tsconfig.json | 11 + release-please-config.json | 1 + 19 files changed, 955 insertions(+), 1 deletion(-) create mode 100644 plugins/node/instrumentation-dataloader/.eslintignore create mode 100644 plugins/node/instrumentation-dataloader/.eslintrc.js create mode 100644 plugins/node/instrumentation-dataloader/.npmignore create mode 100644 plugins/node/instrumentation-dataloader/.tav.yml create mode 100644 plugins/node/instrumentation-dataloader/CHANGELOG.md create mode 100644 plugins/node/instrumentation-dataloader/LICENSE create mode 100644 plugins/node/instrumentation-dataloader/README.md create mode 100644 plugins/node/instrumentation-dataloader/package.json create mode 100644 plugins/node/instrumentation-dataloader/src/index.ts create mode 100644 plugins/node/instrumentation-dataloader/src/instrumentation.ts create mode 100644 plugins/node/instrumentation-dataloader/src/types.ts create mode 100644 plugins/node/instrumentation-dataloader/test/dataloader.test.ts create mode 100644 plugins/node/instrumentation-dataloader/tsconfig.json diff --git a/.github/component_owners.yml b/.github/component_owners.yml index ecbe8c6ec8..3d091e32f4 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -18,6 +18,8 @@ components: - willarmiros plugins/node/instrumentation-amqplib: - blumamir + plugins/node/instrumentation-dataloader: + - henrinormak plugins/node/instrumentation-fs: - rauno56 plugins/node/instrumentation-tedious: diff --git a/metapackages/auto-instrumentations-node/README.md b/metapackages/auto-instrumentations-node/README.md index f9051c1748..21b7c9c8d3 100644 --- a/metapackages/auto-instrumentations-node/README.md +++ b/metapackages/auto-instrumentations-node/README.md @@ -60,6 +60,7 @@ registerInstrumentations({ - [@opentelemetry/instrumentation-bunyan](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-bunyan) - [@opentelemetry/instrumentation-cassandra-driver](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-cassandra) - [@opentelemetry/instrumentation-connect](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-connect) +- [@opentelemetry/instrumentation-dataloader](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-dataloader) - [@opentelemetry/instrumentation-dns](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-dns) - [@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http) - [@opentelemetry/instrumentation-grpc](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-grpc) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index b56184ed75..4720e59697 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -53,6 +53,7 @@ "@opentelemetry/instrumentation-bunyan": "^0.30.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.30.0", "@opentelemetry/instrumentation-connect": "^0.30.0", + "@opentelemetry/instrumentation-dataloader": "^0.1.0", "@opentelemetry/instrumentation-dns": "^0.30.0", "@opentelemetry/instrumentation-express": "^0.31.1", "@opentelemetry/instrumentation-fastify": "^0.29.0", diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index f85f42eee8..826dd30221 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -22,6 +22,7 @@ import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan'; import { CassandraDriverInstrumentation } from '@opentelemetry/instrumentation-cassandra-driver'; import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect'; +import { DataloaderInstrumentation } from '@opentelemetry/instrumentation-dataloader'; import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; @@ -56,6 +57,7 @@ const InstrumentationMap = { '@opentelemetry/instrumentation-cassandra-driver': CassandraDriverInstrumentation, '@opentelemetry/instrumentation-connect': ConnectInstrumentation, + '@opentelemetry/instrumentation-dataloader': DataloaderInstrumentation, '@opentelemetry/instrumentation-dns': DnsInstrumentation, '@opentelemetry/instrumentation-express': ExpressInstrumentation, '@opentelemetry/instrumentation-fastify': FastifyInstrumentation, diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index 24d8d561ab..aaef8784ee 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -31,6 +31,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-bunyan', '@opentelemetry/instrumentation-cassandra-driver', '@opentelemetry/instrumentation-connect', + '@opentelemetry/instrumentation-dataloader', '@opentelemetry/instrumentation-dns', '@opentelemetry/instrumentation-express', '@opentelemetry/instrumentation-fastify', @@ -57,7 +58,7 @@ describe('utils', () => { '@opentelemetry/instrumentation-restify', '@opentelemetry/instrumentation-winston', ]; - assert.strictEqual(instrumentations.length, 31); + assert.strictEqual(instrumentations.length, 32); for (let i = 0, j = instrumentations.length; i < j; i++) { assert.strictEqual( instrumentations[i].instrumentationName, diff --git a/plugins/node/instrumentation-dataloader/.eslintignore b/plugins/node/instrumentation-dataloader/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/instrumentation-dataloader/.eslintrc.js b/plugins/node/instrumentation-dataloader/.eslintrc.js new file mode 100644 index 0000000000..6aeb0710ef --- /dev/null +++ b/plugins/node/instrumentation-dataloader/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js'), +} diff --git a/plugins/node/instrumentation-dataloader/.npmignore b/plugins/node/instrumentation-dataloader/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/instrumentation-dataloader/.tav.yml b/plugins/node/instrumentation-dataloader/.tav.yml new file mode 100644 index 0000000000..f0baa3a47a --- /dev/null +++ b/plugins/node/instrumentation-dataloader/.tav.yml @@ -0,0 +1,4 @@ +dataloader: + # Testing ^2.0.0 covers about 90% of the downloaded versions + versions: "^2.0.0" + commands: npm run test diff --git a/plugins/node/instrumentation-dataloader/CHANGELOG.md b/plugins/node/instrumentation-dataloader/CHANGELOG.md new file mode 100644 index 0000000000..825c32f0d0 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/plugins/node/instrumentation-dataloader/LICENSE b/plugins/node/instrumentation-dataloader/LICENSE new file mode 100644 index 0000000000..e50e8c80f9 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2022] OpenTelemetry Authors + + Licensed 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. diff --git a/plugins/node/instrumentation-dataloader/README.md b/plugins/node/instrumentation-dataloader/README.md new file mode 100644 index 0000000000..9f3a1f0832 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/README.md @@ -0,0 +1,70 @@ +# OpenTelemetry instrumentation for dataloader + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for the injection of trace context to [`dataloader`](https://www.npmjs.com/package/dataloader), which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package and is included in the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle. + +If total installation size is not constrained, it is recommended to use the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle with [@opentelemetry/sdk-node](`https://www.npmjs.com/package/@opentelemetry/sdk-node`) for the most seamless instrumentation experience. + +Compatible with OpenTelemetry JS API and SDK `1.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-dataloader +``` + +### Supported Versions + +- `^2.0.0` + +## Usage + +```js +const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node"); +const { + DataloaderInstrumentation, +} = require("@opentelemetry/instrumentation-dataloader"); +const { registerInstrumentations } = require("@opentelemetry/instrumentation"); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new DataloaderInstrumentation(), + // other instrumentations + ], +}); +``` + +### Dataloader Instrumentation Options + +Dataloader instrumentation has some configuration options + +| Options | Type | Description | +| ------------------- | --------- | --------------------------------------------------------------------------------------- | +| `requireParentSpan` | `boolean` | Require a parent span in order to create dataloader spans, default when unset is false. | + +### Spans created + +Each call to `.load` or `.loadMany` will create a child span for the current active span. + +The batch load function of the dataloader also creates a span, which links to spans created as part of `.load` and `.loadMany`, it is a child span of whatever the active span is during which the dataloader is created. + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-dataloader +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-dataloader.svg diff --git a/plugins/node/instrumentation-dataloader/package.json b/plugins/node/instrumentation-dataloader/package.json new file mode 100644 index 0000000000..9767f60cf8 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/package.json @@ -0,0 +1,68 @@ +{ + "name": "@opentelemetry/instrumentation-dataloader", + "version": "0.1.0", + "description": "OpenTelemetry instrumentation for dataloader", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "clean": "rimraf build/*", + "compile": "tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/instrumentation-dataloader --include-dependencies", + "prewatch": "npm run precompile", + "prepare": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "test-all-versions": "tav", + "version:update": "node ../../../scripts/version-update.js" + }, + "keywords": [ + "dataloader", + "instrumentation", + "nodejs", + "opentelemetry", + "profiling", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/context-async-hooks": "^1.6.0", + "@opentelemetry/sdk-trace-base": "^1.6.0", + "@opentelemetry/sdk-trace-node": "^1.6.0", + "@types/mocha": "7.0.2", + "@types/node": "16.11.21", + "dataloader": "2.0.0", + "gts": "3.1.0", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "test-all-versions": "5.0.1", + "ts-mocha": "10.0.0", + "typescript": "4.3.5" + }, + "dependencies": { + "@opentelemetry/instrumentation": "^0.32.0" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-dataloader#readme" +} diff --git a/plugins/node/instrumentation-dataloader/src/index.ts b/plugins/node/instrumentation-dataloader/src/index.ts new file mode 100644 index 0000000000..6725a11566 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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. + */ + +export { DataloaderInstrumentationConfig } from './types'; +export { DataloaderInstrumentation } from './instrumentation'; diff --git a/plugins/node/instrumentation-dataloader/src/instrumentation.ts b/plugins/node/instrumentation-dataloader/src/instrumentation.ts new file mode 100644 index 0000000000..dd9d1e53df --- /dev/null +++ b/plugins/node/instrumentation-dataloader/src/instrumentation.ts @@ -0,0 +1,248 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + isWrapped, +} from '@opentelemetry/instrumentation'; +import { + diag, + trace, + context, + Link, + SpanStatusCode, + SpanKind, +} from '@opentelemetry/api'; +import { DataloaderInstrumentationConfig } from './types'; +import { VERSION } from './version'; +import type * as Dataloader from 'dataloader'; + +const MODULE_NAME = 'dataloader'; + +type DataloaderInternal = typeof Dataloader.prototype & { + _batchLoadFn: Dataloader.BatchLoadFn; + _batch: { spanLinks?: Link[] } | null; +}; + +type LoadFn = typeof Dataloader.prototype['load']; +type LoadManyFn = typeof Dataloader.prototype['loadMany']; + +export class DataloaderInstrumentation extends InstrumentationBase { + constructor(config: DataloaderInstrumentationConfig = {}) { + super('@opentelemetry/instrumentation-dataloader', VERSION, config); + } + + protected init() { + return [ + new InstrumentationNodeModuleDefinition( + MODULE_NAME, + ['^2.0.0'], + (dataloader, moduleVersion) => { + diag.debug(`Applying patch for ${MODULE_NAME}@${moduleVersion}`); + + this._patchLoad(dataloader.prototype); + this._patchLoadMany(dataloader.prototype); + + return this._getPatchedConstructor(dataloader); + }, + (dataloader, moduleVersion) => { + diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`); + + if (isWrapped(dataloader.prototype.load)) { + this._unwrap(dataloader.prototype, 'load'); + } + + if (isWrapped(dataloader.prototype.loadMany)) { + this._unwrap(dataloader.prototype, 'loadMany'); + } + } + ), + ]; + } + + override getConfig(): DataloaderInstrumentationConfig { + return this._config; + } + + override setConfig(config: DataloaderInstrumentationConfig) { + this._config = config; + } + + private shouldCreateSpans(): boolean { + const config = this.getConfig(); + const hasParentSpan = trace.getSpan(context.active()) !== undefined; + return hasParentSpan || !config.requireParentSpan; + } + + private _getPatchedConstructor( + constructor: typeof Dataloader + ): typeof Dataloader { + const prototype = constructor.prototype; + const self = this; + + function PatchedDataloader( + ...args: ConstructorParameters + ) { + const inst = new constructor(...args) as DataloaderInternal; + + if (!self.isEnabled()) { + return inst; + } + + if (isWrapped(inst._batchLoadFn)) { + self._unwrap(inst, '_batchLoadFn'); + } + + self._wrap(inst, '_batchLoadFn', original => { + return function patchedBatchLoadFn( + this: DataloaderInternal, + ...args: Parameters> + ) { + if (!self.isEnabled() || !self.shouldCreateSpans()) { + return original.call(this, ...args); + } + + const parent = context.active(); + const span = self.tracer.startSpan( + `${MODULE_NAME}.batch`, + { + links: this._batch?.spanLinks as Link[] | undefined, + }, + parent + ); + + return context.with(trace.setSpan(parent, span), () => { + return (original.apply(this, args) as Promise) + .then(value => { + span.end(); + return value; + }) + .catch(err => { + span.recordException(err); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.end(); + throw err; + }); + }); + }; + }); + + return inst; + } + + PatchedDataloader.prototype = prototype; + return PatchedDataloader as unknown as typeof Dataloader; + } + + private _patchLoad(proto: typeof Dataloader.prototype) { + if (isWrapped(proto.load)) { + this._unwrap(proto, 'load'); + } + + this._wrap(proto, 'load', this._getPatchedLoad.bind(this)); + } + + private _getPatchedLoad(original: LoadFn): LoadFn { + const instrumentation = this; + + return function patchedLoad( + this: typeof Dataloader.prototype, + ...args: Parameters + ) { + if (!instrumentation.shouldCreateSpans()) { + return original.call(this, ...args); + } + + const parent = context.active(); + const span = instrumentation.tracer.startSpan( + `${MODULE_NAME}.load`, + { kind: SpanKind.CLIENT }, + parent + ); + + return context.with(trace.setSpan(parent, span), () => { + const result = original + .call(this, ...args) + .then(value => { + span.end(); + return value; + }) + .catch(err => { + span.recordException(err); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + span.end(); + throw err; + }); + + const loader = this as DataloaderInternal; + + if (loader._batch) { + if (!loader._batch.spanLinks) { + loader._batch.spanLinks = []; + } + + loader._batch.spanLinks.push({ context: span.spanContext() } as Link); + } + + return result; + }); + }; + } + + private _patchLoadMany(proto: typeof Dataloader.prototype) { + if (isWrapped(proto.loadMany)) { + this._unwrap(proto, 'loadMany'); + } + + this._wrap(proto, 'loadMany', this._getPatchedLoadMany.bind(this)); + } + + private _getPatchedLoadMany(original: LoadManyFn): LoadManyFn { + const instrumentation = this; + + return function patchedLoadMany( + this: typeof Dataloader.prototype, + ...args: Parameters + ) { + if (!instrumentation.shouldCreateSpans()) { + return original.call(this, ...args); + } + + const parent = context.active(); + const span = instrumentation.tracer.startSpan( + `${MODULE_NAME}.loadMany`, + { kind: SpanKind.CLIENT }, + parent + ); + + return context.with(trace.setSpan(parent, span), () => { + // .loadMany never rejects, as errors from internal .load + // calls are caught by dataloader lib + return original.call(this, ...args).then(value => { + span.end(); + return value; + }); + }); + }; + } +} diff --git a/plugins/node/instrumentation-dataloader/src/types.ts b/plugins/node/instrumentation-dataloader/src/types.ts new file mode 100644 index 0000000000..4ff7a1ac29 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/src/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface DataloaderInstrumentationConfig extends InstrumentationConfig { + /** + * Whether the instrumentation requires a parent span, if set to true + * and there is no parent span, no additional spans are created + * (default: true) + */ + requireParentSpan?: boolean; +} diff --git a/plugins/node/instrumentation-dataloader/test/dataloader.test.ts b/plugins/node/instrumentation-dataloader/test/dataloader.test.ts new file mode 100644 index 0000000000..b7ca578a91 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/test/dataloader.test.ts @@ -0,0 +1,287 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed 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 + * + * https://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 { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; + +import { DataloaderInstrumentation } from '../src'; +const instrumentation = new DataloaderInstrumentation(); + +// For testing that double shimming/wrapping does not occur +const extraInstrumentation = new DataloaderInstrumentation(); +extraInstrumentation.disable(); + +import * as assert from 'assert'; +import * as Dataloader from 'dataloader'; + +describe('DataloaderInstrumentation', () => { + let dataloader: Dataloader; + let contextManager: AsyncHooksContextManager; + + const memoryExporter = new InMemorySpanExporter(); + const provider = new NodeTracerProvider(); + const tracer = provider.getTracer('default'); + + instrumentation.setTracerProvider(provider); + extraInstrumentation.setTracerProvider(provider); + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + + beforeEach(async () => { + instrumentation.enable(); + contextManager = new AsyncHooksContextManager(); + context.setGlobalContextManager(contextManager.enable()); + dataloader = new Dataloader(async keys => keys.map((_, idx) => idx), { + cache: false, + }); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + }); + + afterEach(() => { + memoryExporter.reset(); + context.disable(); + instrumentation.setConfig({}); + instrumentation.disable(); + extraInstrumentation.disable(); + }); + + describe('load', () => { + it('creates a span', async () => { + assert.strictEqual(await dataloader.load('test'), 0); + + // We should have exactly two spans (one for .load and one for the following batch) + assert.strictEqual(memoryExporter.getFinishedSpans().length, 2); + const [batchSpan, loadSpan] = memoryExporter.getFinishedSpans(); + + assert.strictEqual(loadSpan.name, 'dataloader.load'); + assert.strictEqual(loadSpan.kind, SpanKind.CLIENT); + + // Batch span should also be linked to load span + assert.strictEqual(batchSpan.name, 'dataloader.batch'); + assert.strictEqual(batchSpan.kind, SpanKind.INTERNAL); + assert.deepStrictEqual(batchSpan.links, [ + { context: loadSpan.spanContext(), attributes: {} }, + ]); + }); + + it('attaches span to parent', async () => { + const rootSpan: any = tracer.startSpan('root'); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + assert.strictEqual(await dataloader.load('test'), 0); + + const [_, loadSpan] = memoryExporter.getFinishedSpans(); + assert.strictEqual( + loadSpan.parentSpanId, + rootSpan.spanContext().spanId + ); + } + ); + }); + + it('attaches span to parent with required parent', async () => { + instrumentation.setConfig({ requireParentSpan: true }); + const rootSpan: any = tracer.startSpan('root'); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + assert.strictEqual(await dataloader.load('test'), 0); + + const [_, loadSpan] = memoryExporter.getFinishedSpans(); + assert.strictEqual( + loadSpan.parentSpanId, + rootSpan.spanContext().spanId + ); + } + ); + }); + + it('correctly catches exceptions', async () => { + const failingDataloader = new Dataloader(async keys => { + throw new Error('Error message'); + }); + + try { + await failingDataloader.load('test'); + assert.fail('.load should throw'); + } catch (e) {} + + // All spans should be finished, both load as well as the batch ones should have errored + assert.strictEqual(memoryExporter.getFinishedSpans().length, 2); + const [batchSpan, loadSpan] = memoryExporter.getFinishedSpans(); + + assert.deepStrictEqual(loadSpan.status, { + code: SpanStatusCode.ERROR, + message: 'Error message', + }); + + assert.deepStrictEqual(batchSpan.status, { + code: SpanStatusCode.ERROR, + message: 'Error message', + }); + }); + }); + + describe('loadMany', () => { + it('creates an additional span', async () => { + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + + // We should have exactly three spans (one for .loadMany, one for the underlying .load + // and one for the following batch) + assert.strictEqual(memoryExporter.getFinishedSpans().length, 3); + const [batchSpan, loadSpan, loadManySpan] = + memoryExporter.getFinishedSpans(); + + assert.strictEqual(batchSpan.name, 'dataloader.batch'); + assert.strictEqual(batchSpan.kind, SpanKind.INTERNAL); + assert.deepStrictEqual(batchSpan.links, [ + { context: loadSpan.spanContext(), attributes: {} }, + ]); + + assert.strictEqual(loadManySpan.name, 'dataloader.loadMany'); + assert.strictEqual(loadManySpan.kind, SpanKind.CLIENT); + + assert.strictEqual(loadSpan.name, 'dataloader.load'); + assert.strictEqual(loadSpan.kind, SpanKind.CLIENT); + assert.strictEqual( + loadSpan.parentSpanId, + loadManySpan.spanContext().spanId + ); + }); + + it('attaches span to parent', async () => { + const rootSpan: any = tracer.startSpan('root'); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + + const [, , loadManySpan] = memoryExporter.getFinishedSpans(); + assert.strictEqual( + loadManySpan.parentSpanId, + rootSpan.spanContext().spanId + ); + } + ); + }); + + it('attaches span to parent with required parent', async () => { + instrumentation.setConfig({ requireParentSpan: true }); + const rootSpan: any = tracer.startSpan('root'); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + + const [, , loadManySpan] = memoryExporter.getFinishedSpans(); + assert.strictEqual( + loadManySpan.parentSpanId, + rootSpan.spanContext().spanId + ); + } + ); + }); + + it('never errors, even if underlying load fails', async () => { + const failingDataloader = new Dataloader(async keys => { + throw new Error('Error message'); + }); + + try { + await failingDataloader.loadMany(['test']); + } catch (e) { + assert.fail('.loadMany should never throw'); + } + + // All spans should be finished, both load as well as the batch ones should have errored + // but loadMany one should not have errored + assert.strictEqual(memoryExporter.getFinishedSpans().length, 3); + const [batchSpan, loadSpan, loadManySpan] = + memoryExporter.getFinishedSpans(); + + assert.deepStrictEqual(loadSpan.status, { + code: SpanStatusCode.ERROR, + message: 'Error message', + }); + + assert.deepStrictEqual(batchSpan.status, { + code: SpanStatusCode.ERROR, + message: 'Error message', + }); + + assert.deepStrictEqual(loadManySpan.status, { + code: SpanStatusCode.UNSET, + }); + }); + }); + + it('should not create anything if disabled', async () => { + instrumentation.disable(); + + assert.strictEqual(await dataloader.load('test'), 0); + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + + // Same goes for any new dataloaders that are created while the instrumentation is disabled + const alternativeDataloader = new Dataloader( + async keys => keys.map(() => 1), + { cache: false } + ); + assert.strictEqual(await alternativeDataloader.load('test'), 1); + assert.deepStrictEqual(await alternativeDataloader.loadMany(['test']), [1]); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + }); + + it('should not create anything if parent span is required, but missing', async () => { + instrumentation.setConfig({ requireParentSpan: true }); + + assert.strictEqual(await dataloader.load('test'), 0); + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + }); + + it('should avoid double shimming of functions', async () => { + extraInstrumentation.enable(); + + // Dataloader created prior to the extra instrumentation + assert.strictEqual(await dataloader.load('test'), 0); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 2); + + assert.deepStrictEqual(await dataloader.loadMany(['test']), [0]); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 5); + memoryExporter.reset(); + + // Same goes for any new dataloaders that are created after the extra instrumentation is added + const alternativeDataloader = new Dataloader( + async keys => keys.map(() => 1), + { cache: false } + ); + assert.strictEqual(await alternativeDataloader.load('test'), 1); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 2); + + assert.deepStrictEqual(await alternativeDataloader.loadMany(['test']), [1]); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 5); + }); +}); diff --git a/plugins/node/instrumentation-dataloader/tsconfig.json b/plugins/node/instrumentation-dataloader/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/instrumentation-dataloader/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index 9671aba388..87271a6e39 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -21,6 +21,7 @@ "plugins/node/instrumentation-lru-memoizer": {}, "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-amqplib": {}, + "plugins/node/instrumentation-dataloader": {}, "plugins/node/instrumentation-fs": {}, "plugins/node/instrumentation-tedious": {}, "plugins/node/opentelemetry-instrumentation-aws-lambda": {}, From b49aed787cabe2783bcd17f4cd8312f5bc2ca0ee Mon Sep 17 00:00:00 2001 From: Rauno Viskus Date: Tue, 27 Sep 2022 12:47:58 +0300 Subject: [PATCH 14/19] chore: add dataloader to release-please manifest and fix mongoose (#1202) --- .release-please-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4f17e1bfed..abf273b609 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","metapackages/auto-instrumentations-node":"0.32.1","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.0","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.1","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.0","plugins/node/opentelemetry-instrumentation-mongoose":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1","plugins/node/opentelemetry-instrumentation-fastify":"0.29.0","packages/opentelemetry-propagation-utils":"0.28.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-instana":"0.3.0"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud": "0.27.2","detectors/node/opentelemetry-resource-detector-aws": "1.1.2","detectors/node/opentelemetry-resource-detector-docker": "0.1.2","detectors/node/opentelemetry-resource-detector-gcp": "0.27.2","detectors/node/opentelemetry-resource-detector-github": "0.27.0","detectors/node/opentelemetry-resource-detector-instana": "0.3.0","metapackages/auto-instrumentations-node": "0.32.1","metapackages/auto-instrumentations-web": "0.30.0","packages/opentelemetry-host-metrics": "0.30.0","packages/opentelemetry-id-generator-aws-xray": "1.1.0","packages/opentelemetry-propagation-utils": "0.28.0","packages/opentelemetry-test-utils": "0.32.0","plugins/node/instrumentation-amqplib": "0.31.0","plugins/node/instrumentation-dataloader": "0.1.0","plugins/node/instrumentation-fs": "0.5.0","plugins/node/instrumentation-lru-memoizer": "0.31.0","plugins/node/instrumentation-mongoose": "0.30.0","plugins/node/instrumentation-tedious": "0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda": "0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk": "0.9.1","plugins/node/opentelemetry-instrumentation-bunyan": "0.30.0","plugins/node/opentelemetry-instrumentation-cassandra": "0.30.0","plugins/node/opentelemetry-instrumentation-connect": "0.30.0","plugins/node/opentelemetry-instrumentation-dataloader": "0.30.0","plugins/node/opentelemetry-instrumentation-dns": "0.30.0","plugins/node/opentelemetry-instrumentation-express": "0.31.1","plugins/node/opentelemetry-instrumentation-fastify": "0.29.0","plugins/node/opentelemetry-instrumentation-generic-pool": "0.30.0","plugins/node/opentelemetry-instrumentation-graphql": "0.31.0","plugins/node/opentelemetry-instrumentation-hapi": "0.30.0","plugins/node/opentelemetry-instrumentation-ioredis": "0.32.1","plugins/node/opentelemetry-instrumentation-knex": "0.30.0","plugins/node/opentelemetry-instrumentation-koa": "0.32.0","plugins/node/opentelemetry-instrumentation-memcached": "0.30.0","plugins/node/opentelemetry-instrumentation-mongodb": "0.32.0","plugins/node/opentelemetry-instrumentation-mongoose": "0.30.0","plugins/node/opentelemetry-instrumentation-mysql": "0.31.1","plugins/node/opentelemetry-instrumentation-mysql2": "0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core": "0.31.0","plugins/node/opentelemetry-instrumentation-net": "0.30.1","plugins/node/opentelemetry-instrumentation-pg": "0.31.1","plugins/node/opentelemetry-instrumentation-pino": "0.32.0","plugins/node/opentelemetry-instrumentation-redis": "0.33.0","plugins/node/opentelemetry-instrumentation-redis-4": "0.33.0","plugins/node/opentelemetry-instrumentation-restify": "0.30.0","plugins/node/opentelemetry-instrumentation-router": "0.30.0","plugins/node/opentelemetry-instrumentation-winston": "0.30.0","plugins/web/opentelemetry-instrumentation-document-load": "0.30.0","plugins/web/opentelemetry-instrumentation-long-task": "0.31.0","plugins/web/opentelemetry-instrumentation-user-interaction": "0.31.0","plugins/web/opentelemetry-plugin-react-load": "0.28.0","propagators/opentelemetry-propagator-aws-xray": "1.1.0","propagators/opentelemetry-propagator-grpc-census-binary": "0.26.0","propagators/opentelemetry-propagator-instana": "0.2.0","propagators/opentelemetry-propagator-ot-trace": "0.26.1"} From d5b12906fe8c4c654224d76b205dec8945ac1c1c Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Tue, 27 Sep 2022 06:47:09 -0400 Subject: [PATCH 15/19] chore: release main (#1188) --- .release-please-manifest.json | 2 +- .../auto-instrumentations-node/CHANGELOG.md | 19 +++++++++++++++++++ .../auto-instrumentations-node/package.json | 12 ++++++------ .../opentelemetry-host-metrics/CHANGELOG.md | 7 +++++++ .../opentelemetry-host-metrics/package.json | 2 +- .../CHANGELOG.md | 7 +++++++ .../package.json | 2 +- .../instrumentation-dataloader/CHANGELOG.md | 10 ++++++++++ .../instrumentation-dataloader/package.json | 2 +- .../instrumentation-mongoose/CHANGELOG.md | 9 +++++++++ .../instrumentation-mongoose/package.json | 2 +- .../CHANGELOG.md | 14 ++++++++++++++ .../package.json | 4 ++-- .../CHANGELOG.md | 7 +++++++ .../package.json | 2 +- .../CHANGELOG.md | 7 +++++++ .../package.json | 2 +- 17 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 plugins/node/instrumentation-mongoose/CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index abf273b609..3f1c48cc38 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud": "0.27.2","detectors/node/opentelemetry-resource-detector-aws": "1.1.2","detectors/node/opentelemetry-resource-detector-docker": "0.1.2","detectors/node/opentelemetry-resource-detector-gcp": "0.27.2","detectors/node/opentelemetry-resource-detector-github": "0.27.0","detectors/node/opentelemetry-resource-detector-instana": "0.3.0","metapackages/auto-instrumentations-node": "0.32.1","metapackages/auto-instrumentations-web": "0.30.0","packages/opentelemetry-host-metrics": "0.30.0","packages/opentelemetry-id-generator-aws-xray": "1.1.0","packages/opentelemetry-propagation-utils": "0.28.0","packages/opentelemetry-test-utils": "0.32.0","plugins/node/instrumentation-amqplib": "0.31.0","plugins/node/instrumentation-dataloader": "0.1.0","plugins/node/instrumentation-fs": "0.5.0","plugins/node/instrumentation-lru-memoizer": "0.31.0","plugins/node/instrumentation-mongoose": "0.30.0","plugins/node/instrumentation-tedious": "0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda": "0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk": "0.9.1","plugins/node/opentelemetry-instrumentation-bunyan": "0.30.0","plugins/node/opentelemetry-instrumentation-cassandra": "0.30.0","plugins/node/opentelemetry-instrumentation-connect": "0.30.0","plugins/node/opentelemetry-instrumentation-dataloader": "0.30.0","plugins/node/opentelemetry-instrumentation-dns": "0.30.0","plugins/node/opentelemetry-instrumentation-express": "0.31.1","plugins/node/opentelemetry-instrumentation-fastify": "0.29.0","plugins/node/opentelemetry-instrumentation-generic-pool": "0.30.0","plugins/node/opentelemetry-instrumentation-graphql": "0.31.0","plugins/node/opentelemetry-instrumentation-hapi": "0.30.0","plugins/node/opentelemetry-instrumentation-ioredis": "0.32.1","plugins/node/opentelemetry-instrumentation-knex": "0.30.0","plugins/node/opentelemetry-instrumentation-koa": "0.32.0","plugins/node/opentelemetry-instrumentation-memcached": "0.30.0","plugins/node/opentelemetry-instrumentation-mongodb": "0.32.0","plugins/node/opentelemetry-instrumentation-mongoose": "0.30.0","plugins/node/opentelemetry-instrumentation-mysql": "0.31.1","plugins/node/opentelemetry-instrumentation-mysql2": "0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core": "0.31.0","plugins/node/opentelemetry-instrumentation-net": "0.30.1","plugins/node/opentelemetry-instrumentation-pg": "0.31.1","plugins/node/opentelemetry-instrumentation-pino": "0.32.0","plugins/node/opentelemetry-instrumentation-redis": "0.33.0","plugins/node/opentelemetry-instrumentation-redis-4": "0.33.0","plugins/node/opentelemetry-instrumentation-restify": "0.30.0","plugins/node/opentelemetry-instrumentation-router": "0.30.0","plugins/node/opentelemetry-instrumentation-winston": "0.30.0","plugins/web/opentelemetry-instrumentation-document-load": "0.30.0","plugins/web/opentelemetry-instrumentation-long-task": "0.31.0","plugins/web/opentelemetry-instrumentation-user-interaction": "0.31.0","plugins/web/opentelemetry-plugin-react-load": "0.28.0","propagators/opentelemetry-propagator-aws-xray": "1.1.0","propagators/opentelemetry-propagator-grpc-census-binary": "0.26.0","propagators/opentelemetry-propagator-instana": "0.2.0","propagators/opentelemetry-propagator-ot-trace": "0.26.1"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","detectors/node/opentelemetry-resource-detector-instana":"0.3.0","metapackages/auto-instrumentations-node":"0.33.0","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.1","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-propagation-utils":"0.29.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-dataloader":"0.2.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/instrumentation-mongoose":"0.31.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.2","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dataloader":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-fastify":"0.30.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.1","plugins/node/opentelemetry-instrumentation-mongoose":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1"} diff --git a/metapackages/auto-instrumentations-node/CHANGELOG.md b/metapackages/auto-instrumentations-node/CHANGELOG.md index 3278995710..2044115ef4 100644 --- a/metapackages/auto-instrumentations-node/CHANGELOG.md +++ b/metapackages/auto-instrumentations-node/CHANGELOG.md @@ -46,6 +46,25 @@ * @opentelemetry/instrumentation-pino bumped from ^0.31.0 to ^0.32.0 * @opentelemetry/instrumentation-redis-4 bumped from ^0.32.0 to ^0.33.0 +## [0.33.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/auto-instrumentations-node-v0.32.1...auto-instrumentations-node-v0.33.0) (2022-09-27) + + +### Features + +* add dataloader instrumentation ([#1171](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1171)) ([3898b11](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/3898b11800f857c75c286f22c4633b5baf4e1f84)) +* add mongoose instrumentation ([#1131](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1131)) ([b35277b](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/b35277bb5fc66910e8942bc0b64347b68ecffa26)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @opentelemetry/instrumentation-aws-sdk bumped from ^0.9.1 to ^0.9.2 + * @opentelemetry/instrumentation-dataloader bumped from ^0.1.0 to ^0.2.0 + * @opentelemetry/instrumentation-fastify bumped from ^0.29.0 to ^0.30.0 + * @opentelemetry/instrumentation-mongodb bumped from ^0.32.0 to ^0.32.1 + * @opentelemetry/instrumentation-mongoose bumped from ^0.30.0 to ^0.31.0 + ## [0.32.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/auto-instrumentations-node-v0.31.2...auto-instrumentations-node-v0.32.0) (2022-09-02) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index 4720e59697..c6866ba7ae 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/auto-instrumentations-node", - "version": "0.32.1", + "version": "0.33.0", "description": "Metapackage which bundles opentelemetry node core and contrib instrumentations", "author": "OpenTelemetry Authors", "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/metapackages/auto-instrumentations-node#readme", @@ -49,14 +49,14 @@ "@opentelemetry/instrumentation": "^0.32.0", "@opentelemetry/instrumentation-amqplib": "^0.31.0", "@opentelemetry/instrumentation-aws-lambda": "^0.33.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.9.1", + "@opentelemetry/instrumentation-aws-sdk": "^0.9.2", "@opentelemetry/instrumentation-bunyan": "^0.30.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.30.0", "@opentelemetry/instrumentation-connect": "^0.30.0", - "@opentelemetry/instrumentation-dataloader": "^0.1.0", + "@opentelemetry/instrumentation-dataloader": "^0.2.0", "@opentelemetry/instrumentation-dns": "^0.30.0", "@opentelemetry/instrumentation-express": "^0.31.1", - "@opentelemetry/instrumentation-fastify": "^0.29.0", + "@opentelemetry/instrumentation-fastify": "^0.30.0", "@opentelemetry/instrumentation-generic-pool": "^0.30.0", "@opentelemetry/instrumentation-graphql": "^0.31.0", "@opentelemetry/instrumentation-grpc": "^0.32.0", @@ -67,8 +67,8 @@ "@opentelemetry/instrumentation-koa": "^0.32.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.31.0", "@opentelemetry/instrumentation-memcached": "^0.30.0", - "@opentelemetry/instrumentation-mongodb": "^0.32.0", - "@opentelemetry/instrumentation-mongoose": "^0.30.0", + "@opentelemetry/instrumentation-mongodb": "^0.32.1", + "@opentelemetry/instrumentation-mongoose": "^0.31.0", "@opentelemetry/instrumentation-mysql": "^0.31.0", "@opentelemetry/instrumentation-mysql2": "^0.32.0", "@opentelemetry/instrumentation-nestjs-core": "^0.31.0", diff --git a/packages/opentelemetry-host-metrics/CHANGELOG.md b/packages/opentelemetry-host-metrics/CHANGELOG.md index 8ff11e096a..c3b0717860 100644 --- a/packages/opentelemetry-host-metrics/CHANGELOG.md +++ b/packages/opentelemetry-host-metrics/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.30.1](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/host-metrics-v0.30.0...host-metrics-v0.30.1) (2022-09-27) + + +### Bug Fixes + +* readme snippet ([#1182](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1182)) ([35d1e45](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/35d1e4579f7b160c501959f6fb45859b75cdde99)) + ## [0.30.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/host-metrics-v0.29.0...host-metrics-v0.30.0) (2022-09-02) diff --git a/packages/opentelemetry-host-metrics/package.json b/packages/opentelemetry-host-metrics/package.json index f89be2fd57..235b7c38c9 100644 --- a/packages/opentelemetry-host-metrics/package.json +++ b/packages/opentelemetry-host-metrics/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/host-metrics", - "version": "0.30.0", + "version": "0.30.1", "description": "OpenTelemetry Host Metrics for Node.js", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/packages/opentelemetry-propagation-utils/CHANGELOG.md b/packages/opentelemetry-propagation-utils/CHANGELOG.md index d045713442..52b4058a4f 100644 --- a/packages/opentelemetry-propagation-utils/CHANGELOG.md +++ b/packages/opentelemetry-propagation-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.29.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/propagation-utils-v0.28.0...propagation-utils-v0.29.0) (2022-09-27) + + +### Features + +* **propagation-utils:** end pub-sub process span on promise settled ([#1055](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1055)) ([cb83d30](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/cb83d300582b4d485be56563634cd3859069004c)) + ## [0.28.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/propagation-utils-v0.27.0...propagation-utils-v0.28.0) (2022-05-14) diff --git a/packages/opentelemetry-propagation-utils/package.json b/packages/opentelemetry-propagation-utils/package.json index f5b0f4001c..720d19b802 100644 --- a/packages/opentelemetry-propagation-utils/package.json +++ b/packages/opentelemetry-propagation-utils/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/propagation-utils", - "version": "0.28.0", + "version": "0.29.0", "description": "Propagation utilities for opentelemetry instrumentations", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/plugins/node/instrumentation-dataloader/CHANGELOG.md b/plugins/node/instrumentation-dataloader/CHANGELOG.md index 825c32f0d0..9f45b1a3c4 100644 --- a/plugins/node/instrumentation-dataloader/CHANGELOG.md +++ b/plugins/node/instrumentation-dataloader/CHANGELOG.md @@ -1 +1,11 @@ # Changelog + +## [0.2.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-dataloader-v0.1.0...instrumentation-dataloader-v0.2.0) (2022-09-27) + + +### Features + +* add dataloader instrumentation ([#1171](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1171)) ([3898b11](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/3898b11800f857c75c286f22c4633b5baf4e1f84)) +* upstream mocha instrumentation testing plugin from ext-js [#621](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/621) ([#669](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/669)) ([a5170c4](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/a5170c494706a2bec3ba51e59966d0ca8a41d00e)) + +## Changelog diff --git a/plugins/node/instrumentation-dataloader/package.json b/plugins/node/instrumentation-dataloader/package.json index 9767f60cf8..17a24ac1ac 100644 --- a/plugins/node/instrumentation-dataloader/package.json +++ b/plugins/node/instrumentation-dataloader/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-dataloader", - "version": "0.1.0", + "version": "0.2.0", "description": "OpenTelemetry instrumentation for dataloader", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/plugins/node/instrumentation-mongoose/CHANGELOG.md b/plugins/node/instrumentation-mongoose/CHANGELOG.md new file mode 100644 index 0000000000..e1d906a4a4 --- /dev/null +++ b/plugins/node/instrumentation-mongoose/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## [0.31.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-mongoose-v0.30.0...instrumentation-mongoose-v0.31.0) (2022-09-27) + + +### Features + +* add mongoose instrumentation ([#1131](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1131)) ([b35277b](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/b35277bb5fc66910e8942bc0b64347b68ecffa26)) +* upstream mocha instrumentation testing plugin from ext-js [#621](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/621) ([#669](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/669)) ([a5170c4](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/a5170c494706a2bec3ba51e59966d0ca8a41d00e)) diff --git a/plugins/node/instrumentation-mongoose/package.json b/plugins/node/instrumentation-mongoose/package.json index db0b06f90d..2968026639 100644 --- a/plugins/node/instrumentation-mongoose/package.json +++ b/plugins/node/instrumentation-mongoose/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-mongoose", - "version": "0.30.0", + "version": "0.31.0", "description": "OpenTelemetry automatic instrumentation package for mongoose", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/CHANGELOG.md b/plugins/node/opentelemetry-instrumentation-aws-sdk/CHANGELOG.md index fea6252585..a4f3348fab 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/CHANGELOG.md +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [0.9.2](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-aws-sdk-v0.9.1...instrumentation-aws-sdk-v0.9.2) (2022-09-27) + + +### Bug Fixes + +* **aws-sdk:** set spanKind to CLIENT by default in v3 ([#1177](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1177)) ([d463695](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/d463695f5258875f1da0c7b17c20f7df93494d4e)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @opentelemetry/propagation-utils bumped from ^0.28.0 to ^0.29.0 + ## [0.9.1](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-aws-sdk-v0.9.0...instrumentation-aws-sdk-v0.9.1) (2022-09-15) diff --git a/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json b/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json index 7b9ea8beb9..6b1684a313 100644 --- a/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json +++ b/plugins/node/opentelemetry-instrumentation-aws-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-aws-sdk", - "version": "0.9.1", + "version": "0.9.2", "description": "OpenTelemetry automatic instrumentation for the `aws-sdk` package", "keywords": [ "aws", @@ -49,7 +49,7 @@ "@opentelemetry/core": "^1.0.0", "@opentelemetry/instrumentation": "^0.32.0", "@opentelemetry/semantic-conventions": "^1.0.0", - "@opentelemetry/propagation-utils": "^0.28.0" + "@opentelemetry/propagation-utils": "^0.29.0" }, "devDependencies": { "@aws-sdk/client-dynamodb": "3.85.0", diff --git a/plugins/node/opentelemetry-instrumentation-fastify/CHANGELOG.md b/plugins/node/opentelemetry-instrumentation-fastify/CHANGELOG.md index 0f092c44f6..2f94ee1d3c 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/CHANGELOG.md +++ b/plugins/node/opentelemetry-instrumentation-fastify/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.30.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-fastify-v0.29.0...instrumentation-fastify-v0.30.0) (2022-09-27) + + +### Features + +* **opentelemetry-instrumentation-fastify:** Support Fastify V4 also ([#1164](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1164)) ([d932d3e](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/d932d3edcbf41685ca0af546347450fa81444b4e)) + ## [0.29.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-fastify-v0.28.0...instrumentation-fastify-v0.29.0) (2022-09-02) diff --git a/plugins/node/opentelemetry-instrumentation-fastify/package.json b/plugins/node/opentelemetry-instrumentation-fastify/package.json index 672c06fbcf..1682bc0ba3 100644 --- a/plugins/node/opentelemetry-instrumentation-fastify/package.json +++ b/plugins/node/opentelemetry-instrumentation-fastify/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-fastify", - "version": "0.29.0", + "version": "0.30.0", "description": "OpenTelemetry fastify automatic instrumentation package.", "main": "build/src/index.js", "types": "build/src/index.d.ts", diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/CHANGELOG.md b/plugins/node/opentelemetry-instrumentation-mongodb/CHANGELOG.md index 0919dd944a..30eee5598f 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/CHANGELOG.md +++ b/plugins/node/opentelemetry-instrumentation-mongodb/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.32.1](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-mongodb-v0.32.0...instrumentation-mongodb-v0.32.1) (2022-09-27) + + +### Bug Fixes + +* remove unneeded type exports in mongodb instrumentation ([#1194](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1194)) ([6920a55](https://github.com/open-telemetry/opentelemetry-js-contrib/commit/6920a554b46bf8af5e00b60073d479feacb18dcd)) + ## [0.32.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-mongodb-v0.31.1...instrumentation-mongodb-v0.32.0) (2022-09-02) diff --git a/plugins/node/opentelemetry-instrumentation-mongodb/package.json b/plugins/node/opentelemetry-instrumentation-mongodb/package.json index b551587d2d..f3cc1f3f47 100644 --- a/plugins/node/opentelemetry-instrumentation-mongodb/package.json +++ b/plugins/node/opentelemetry-instrumentation-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/instrumentation-mongodb", - "version": "0.32.0", + "version": "0.32.1", "description": "OpenTelemetry mongodb automatic instrumentation package.", "main": "build/src/index.js", "types": "build/src/index.d.ts", From 32de1b960e1dd7977e58a7bb684d143981993f9d Mon Sep 17 00:00:00 2001 From: Rauno Viskus Date: Tue, 27 Sep 2022 14:59:20 +0300 Subject: [PATCH 16/19] chore: update lerna (#1200) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3a0b1df174..ea02aba526 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "eslint-plugin-import": "2.25.4", "gts": "3.1.0", "husky": "7.0.4", - "lerna": "3.22.1", - "lerna-changelog": "1.0.1", + "lerna": "5.5.2", + "lerna-changelog": "2.2.0", "typescript": "4.3.5" }, "changelog": { From e94c81afcf8841eb9abbad797063967f5c9e55cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= Date: Tue, 27 Sep 2022 14:00:28 +0200 Subject: [PATCH 17/19] chore: remove outdated entries from RP manifest (#1206) removed wrong dataloader and mongoose entries from release-please manifest. sort entries in release-please config. Co-authored-by: Rauno Viskus --- .release-please-manifest.json | 2 +- release-please-config.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3f1c48cc38..c82f77e77a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","detectors/node/opentelemetry-resource-detector-instana":"0.3.0","metapackages/auto-instrumentations-node":"0.33.0","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.1","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-propagation-utils":"0.29.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-dataloader":"0.2.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/instrumentation-mongoose":"0.31.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.2","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dataloader":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-fastify":"0.30.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.1","plugins/node/opentelemetry-instrumentation-mongoose":"0.30.0","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.2","detectors/node/opentelemetry-resource-detector-aws":"1.1.2","detectors/node/opentelemetry-resource-detector-docker":"0.1.2","detectors/node/opentelemetry-resource-detector-gcp":"0.27.2","detectors/node/opentelemetry-resource-detector-github":"0.27.0","detectors/node/opentelemetry-resource-detector-instana":"0.3.0","metapackages/auto-instrumentations-node":"0.33.0","metapackages/auto-instrumentations-web":"0.30.0","packages/opentelemetry-host-metrics":"0.30.1","packages/opentelemetry-id-generator-aws-xray":"1.1.0","packages/opentelemetry-propagation-utils":"0.29.0","packages/opentelemetry-test-utils":"0.32.0","plugins/node/instrumentation-amqplib":"0.31.0","plugins/node/instrumentation-dataloader":"0.2.0","plugins/node/instrumentation-fs":"0.5.0","plugins/node/instrumentation-lru-memoizer":"0.31.0","plugins/node/instrumentation-mongoose":"0.31.0","plugins/node/instrumentation-tedious":"0.4.0","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.33.0","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.9.2","plugins/node/opentelemetry-instrumentation-bunyan":"0.30.0","plugins/node/opentelemetry-instrumentation-cassandra":"0.30.0","plugins/node/opentelemetry-instrumentation-connect":"0.30.0","plugins/node/opentelemetry-instrumentation-dns":"0.30.0","plugins/node/opentelemetry-instrumentation-express":"0.31.1","plugins/node/opentelemetry-instrumentation-fastify":"0.30.0","plugins/node/opentelemetry-instrumentation-generic-pool":"0.30.0","plugins/node/opentelemetry-instrumentation-graphql":"0.31.0","plugins/node/opentelemetry-instrumentation-hapi":"0.30.0","plugins/node/opentelemetry-instrumentation-ioredis":"0.32.1","plugins/node/opentelemetry-instrumentation-knex":"0.30.0","plugins/node/opentelemetry-instrumentation-koa":"0.32.0","plugins/node/opentelemetry-instrumentation-memcached":"0.30.0","plugins/node/opentelemetry-instrumentation-mongodb":"0.32.1","plugins/node/opentelemetry-instrumentation-mysql":"0.31.1","plugins/node/opentelemetry-instrumentation-mysql2":"0.32.0","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.31.0","plugins/node/opentelemetry-instrumentation-net":"0.30.1","plugins/node/opentelemetry-instrumentation-pg":"0.31.1","plugins/node/opentelemetry-instrumentation-pino":"0.32.0","plugins/node/opentelemetry-instrumentation-redis":"0.33.0","plugins/node/opentelemetry-instrumentation-redis-4":"0.33.0","plugins/node/opentelemetry-instrumentation-restify":"0.30.0","plugins/node/opentelemetry-instrumentation-router":"0.30.0","plugins/node/opentelemetry-instrumentation-winston":"0.30.0","plugins/web/opentelemetry-instrumentation-document-load":"0.30.0","plugins/web/opentelemetry-instrumentation-long-task":"0.31.0","plugins/web/opentelemetry-instrumentation-user-interaction":"0.31.0","plugins/web/opentelemetry-plugin-react-load":"0.28.0","propagators/opentelemetry-propagator-aws-xray":"1.1.0","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.0","propagators/opentelemetry-propagator-instana":"0.2.0","propagators/opentelemetry-propagator-ot-trace":"0.26.1"} diff --git a/release-please-config.json b/release-please-config.json index 87271a6e39..00070fc0c4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -18,11 +18,11 @@ "packages/opentelemetry-id-generator-aws-xray": {}, "packages/opentelemetry-propagation-utils": {}, "packages/opentelemetry-test-utils": {}, - "plugins/node/instrumentation-lru-memoizer": {}, - "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-amqplib": {}, "plugins/node/instrumentation-dataloader": {}, "plugins/node/instrumentation-fs": {}, + "plugins/node/instrumentation-lru-memoizer": {}, + "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-tedious": {}, "plugins/node/opentelemetry-instrumentation-aws-lambda": {}, "plugins/node/opentelemetry-instrumentation-aws-sdk": {}, @@ -46,8 +46,8 @@ "plugins/node/opentelemetry-instrumentation-net": {}, "plugins/node/opentelemetry-instrumentation-pg": {}, "plugins/node/opentelemetry-instrumentation-pino": {}, - "plugins/node/opentelemetry-instrumentation-redis-4": {}, "plugins/node/opentelemetry-instrumentation-redis": {}, + "plugins/node/opentelemetry-instrumentation-redis-4": {}, "plugins/node/opentelemetry-instrumentation-restify": {}, "plugins/node/opentelemetry-instrumentation-router": {}, "plugins/node/opentelemetry-instrumentation-winston": {}, From e20e6f09e078b0e01c1a5a79a9ac464b5b55581d Mon Sep 17 00:00:00 2001 From: Rauno Viskus Date: Tue, 27 Sep 2022 15:39:02 +0300 Subject: [PATCH 18/19] chore: enable running TAV on PRs (#1198) * chore: ignore packages that don't work on node@18 * chore: enable running TAV on PRs * chore: filter out scopes that don't have test-all-versions script * chore: remove checklist with the reminder to run TAV * chore: remove unneeded containers from tav jobs * chore: sort keys * chore: skip more tests --- .github/PULL_REQUEST_TEMPLATE.md | 4 -- .github/workflows/test-all-versions.pr.yml | 37 ++++++++++++ .github/workflows/test-all-versions.push.yml | 13 +++++ .github/workflows/test-all-versions.yml | 58 +++++++++---------- .../test/ioredis.test.ts | 4 +- scripts/parse-lerna-scopes.mjs | 56 ++++++++++++++++++ 6 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/test-all-versions.pr.yml create mode 100644 .github/workflows/test-all-versions.push.yml create mode 100644 scripts/parse-lerna-scopes.mjs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f4fa70647d..8a17e1d13d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,3 @@ Before creating a pull request, please make sure: ## Short description of the changes - - -## Checklist - -- [ ] Ran `npm run test-all-versions` for the edited package(s) on the latest commit if applicable. diff --git a/.github/workflows/test-all-versions.pr.yml b/.github/workflows/test-all-versions.pr.yml new file mode 100644 index 0000000000..d0f9528d31 --- /dev/null +++ b/.github/workflows/test-all-versions.pr.yml @@ -0,0 +1,37 @@ +name: TAV for PR +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + +jobs: + parse-labels: + runs-on: ubuntu-latest + container: + image: node:16 + env: + PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + outputs: + args: ${{ steps.lerna-args.outputs.args }} + steps: + - name: Checkout + uses: actions/checkout@v3 + # Need lerna to list all packages + - name: Install lerna + run: npm install -g lerna + - name: Parse labels into lerna scope arguments + id: lerna-args + run: | + OUTPUT=`node scripts/parse-lerna-scopes.mjs "$PR_LABELS"` + echo "::set-output name=args::$OUTPUT" + + tav: + uses: ./.github/workflows/test-all-versions.yml + needs: parse-labels + with: + lerna-args: ${{ needs.parse-labels.outputs.args }} + if: ${{ needs.parse-labels.outputs.args != '' }} diff --git a/.github/workflows/test-all-versions.push.yml b/.github/workflows/test-all-versions.push.yml new file mode 100644 index 0000000000..29702bfd4b --- /dev/null +++ b/.github/workflows/test-all-versions.push.yml @@ -0,0 +1,13 @@ +name: TAV for Push +on: + push: + branches: + - "main" + - "release/**" + - "release-please/**" + +jobs: + tav: + uses: ./.github/workflows/test-all-versions.yml + with: + lerna-args: "" diff --git a/.github/workflows/test-all-versions.yml b/.github/workflows/test-all-versions.yml index 6052643038..3ef425993e 100644 --- a/.github/workflows/test-all-versions.yml +++ b/.github/workflows/test-all-versions.yml @@ -1,13 +1,16 @@ name: Test All Versions on: - push: - branches: - - "main" - - "release/**" - - "release-please/**" schedule: - cron: "30 4 * * *" workflow_dispatch: + inputs: + lerna-args: + type: string + workflow_call: + inputs: + lerna-args: + required: true + type: string jobs: tav: @@ -16,12 +19,14 @@ jobs: fail-fast: false matrix: node: ["14", "16", "18"] + include: + - node: "18" + lerna-extra-args: >- + --ignore @opentelemetry/instrumentation-fastify + --ignore @opentelemetry/instrumentation-restify + --ignore @opentelemetry/resource-detector-alibaba-cloud runs-on: ubuntu-latest services: - memcached: - image: memcached:1.6.9-alpine - ports: - - 11211:11211 mongo: image: mongo ports: @@ -65,6 +70,13 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + rabbitmq: + image: rabbitmq:3 + ports: + - 22221:5672 + env: + RABBITMQ_DEFAULT_USER: username + RABBITMQ_DEFAULT_PASS: password redis: image: redis ports: @@ -74,26 +86,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - cassandra: - image: bitnami/cassandra:3 - ports: - - 9042:9042 - rabbitmq: - image: rabbitmq:3 - ports: - - 22221:5672 - env: - RABBITMQ_DEFAULT_USER: username - RABBITMQ_DEFAULT_PASS: password env: - RUN_CASSANDRA_TESTS: 1 - RUN_MEMCACHED_TESTS: 1 - RUN_MONGODB_TESTS: 1 - RUN_MYSQL_TESTS: 1 - RUN_MSSQL_TESTS: 1 - RUN_POSTGRES_TESTS: 1 - RUN_REDIS_TESTS: 1 - CASSANDRA_HOST: localhost MONGODB_DB: opentelemetry-tests MONGODB_HOST: 127.0.0.1 MONGODB_PORT: 27017 @@ -103,14 +96,17 @@ jobs: MYSQL_PASSWORD: secret MYSQL_PORT: 3306 MYSQL_USER: otel - OPENTELEMETRY_MEMCACHED_HOST: localhost - OPENTELEMETRY_MEMCACHED_PORT: 11211 OPENTELEMETRY_REDIS_HOST: localhost OPENTELEMETRY_REDIS_PORT: 6379 POSTGRES_DB: circle_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 POSTGRES_USER: postgres + RUN_MONGODB_TESTS: 1 + RUN_MSSQL_TESTS: 1 + RUN_MYSQL_TESTS: 1 + RUN_POSTGRES_TESTS: 1 + RUN_REDIS_TESTS: 1 NPM_CONFIG_UNSAFE_PERM: true steps: - name: Checkout @@ -147,4 +143,4 @@ jobs: - name: Bootstrap Dependencies run: lerna bootstrap --no-ci --hoist --nohoist='zone.js' --nohoist='mocha' --nohoist='ts-mocha' - name: Run test-all-versions - run: lerna run test-all-versions ${{ matrix.lerna-extra-args }} --stream --concurrency 1 + run: lerna run test-all-versions ${{ inputs.lerna-args }} ${{ matrix.lerna-extra-args }} --stream --concurrency 1 diff --git a/plugins/node/opentelemetry-instrumentation-ioredis/test/ioredis.test.ts b/plugins/node/opentelemetry-instrumentation-ioredis/test/ioredis.test.ts index b2724f4bf1..dae05f48ba 100644 --- a/plugins/node/opentelemetry-instrumentation-ioredis/test/ioredis.test.ts +++ b/plugins/node/opentelemetry-instrumentation-ioredis/test/ioredis.test.ts @@ -373,7 +373,7 @@ describe('ioredis', () => { }); }); - it('should create a child span for pubsub', async () => { + it.skip('should create a child span for pubsub', async () => { const span = provider.getTracer('ioredis-test').startSpan('test span'); await context.with(trace.setSpan(context.active(), span), async () => { try { @@ -681,7 +681,7 @@ describe('ioredis', () => { ); }); - it('should instrument connect with requireParentSpan equal false', async () => { + it.skip('should instrument connect with requireParentSpan equal false', async () => { const config: IORedisInstrumentationConfig = { requireParentSpan: false, }; diff --git a/scripts/parse-lerna-scopes.mjs b/scripts/parse-lerna-scopes.mjs new file mode 100644 index 0000000000..af66a5af1d --- /dev/null +++ b/scripts/parse-lerna-scopes.mjs @@ -0,0 +1,56 @@ +import * as childProcess from 'child_process'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +/* + Formats `--scope` arguments for lerna from "pkg:"-prefixed labels. + Takes a JSON string as an argument and returns the formatted args in stdout. + Filters out packages that do not have test-all-versions script because it's the only + location we are using this script. + + arg: '["pkg:404", "pkg:", "pkg:instrumentation-pino", "pkg:instrumentation-dns", "pkg:instrumentation-express", "urgent", "pkg:instrumentation-fs"]' + stdout: '--scope @opentelemetry/instrumentation-pino --scope @opentelemetry/instrumentation-express' +*/ + +const labels = JSON.parse(process.argv[2]); +const lernaList = JSON.parse( + childProcess.spawnSync('lerna', ['list', '--json']).stdout + .toString('utf8') +); +const packageList = new Map( + lernaList.map((pkg) => { + return [pkg.name, pkg]; + }) +); +// Checking this is not strictly required, but saves the whole setup for TAV workflows +const hasTavScript = (pkgLocation) => { + const { scripts } = JSON.parse(readFileSync(join(pkgLocation, 'package.json'))); + return !!scripts['test-all-versions']; +}; + +console.error('Labels:', labels); +console.error('Packages:', [...packageList.keys()]); + +const scopes = labels + .filter((l) => { + return l.startsWith('pkg:'); + }) + .map((l) => { + return l.replace(/^pkg:/, '@opentelemetry/'); + }) + .filter((pkgName) => { + const info = packageList.get(pkgName); + if (!info) { + return false + } + return hasTavScript(info.location); + }) + +console.error('Scopes:', scopes); + +console.log( + scopes.map((scope) => { + return `--scope ${scope}`; + }) + .join(' ') +); From 1c450f5fe31437aa7157ff6bb8602d330dc3b429 Mon Sep 17 00:00:00 2001 From: WhiteSource Renovate Date: Tue, 27 Sep 2022 16:44:45 +0200 Subject: [PATCH 19/19] chore(deps): update actions/stale action to v6 (#1195) --- .github/workflows/close-stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-stale.yml b/.github/workflows/close-stale.yml index 442db8b66d..01b92f2366 100644 --- a/.github/workflows/close-stale.yml +++ b/.github/workflows/close-stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 with: days-before-stale: 60 days-before-close: 14