Skip to content

Commit

Permalink
feat(collector): tracing all spans when client app is using ES modules (
Browse files Browse the repository at this point in the history
#672)

Added support for client app being an ESM app.

Unsupported: ESM instrumentations. We will extend the esm-loader.mjs to add this support asap. But this missing feature should not affect a customer right now, because there are no known bigger NPM modules, which have dropped the support for CJS (except e.g. got). Furthermore we do not offer an API to add custom instrumentations.
  • Loading branch information
kirrg001 authored Dec 29, 2022
1 parent a46b81f commit 7d471ff
Show file tree
Hide file tree
Showing 47 changed files with 5,135 additions and 12 deletions.
61 changes: 60 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,11 @@ shared: &shared
. ./bin/ci_glob_setup.sh
date
if [[ -z "$NODE_COVERAGE" ]]; then
npm run test:ci
if [[ -z "$COLLECTOR_ONLY" ]]; then
npm run test:ci
else
npm run test:ci:collector
fi
else
npm run coverage-all
fi
Expand Down Expand Up @@ -370,6 +374,50 @@ jobs:
<<: *shared
parallelism: 20

"node-18-esm":
environment:
RUN_ESM: true
COLLECTOR_ONLY: true
docker:
- image: cimg/node:18.0
- <<: *zookeeper
- <<: *elasticsearch
- <<: *mongo
- <<: *redis
- <<: *kafka
- <<: *schema-registry
- <<: *mysql
- <<: *postgres
- <<: *mssql
- <<: *rabbitmq
- <<: *nats
- <<: *nats-streaming
- <<: *memcached
<<: *shared
parallelism: 20

"node-16-esm":
environment:
RUN_ESM: true
COLLECTOR_ONLY: true
docker:
- image: circleci/node:16
- <<: *zookeeper
- <<: *elasticsearch
- <<: *mongo
- <<: *redis
- <<: *kafka
- <<: *schema-registry
- <<: *mysql
- <<: *postgres
- <<: *mssql
- <<: *rabbitmq
- <<: *nats
- <<: *nats-streaming
- <<: *memcached
<<: *shared
parallelism: 20

"node-14":
environment:
NPM_VERSION: 8.6.0
Expand Down Expand Up @@ -458,6 +506,17 @@ workflows:
jobs:
- "node-18"
- "node-16"
esm-build:
triggers:
- schedule:
cron: "7 4 * * *"
filters:
branches:
only:
- main
jobs:
- "node-18-esm"
- "node-16-esm"
legacy-nodejs-versions:
triggers:
- schedule:
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ Some of the tests require infrastructure components (databases etc.) to run loca

If you want to see the Node.js collector's debug output while running the tests, make sure the environment variable `WITH_STDOUT` is set to a non-empty string. You can also use `npm run test:debug` instead of `npm test` to achieve this.

## Adding Tests

### ES6

We have added a CI build to test our instrumentations against ES module apps.
See https://github.com/instana/nodejs/pull/672

Not all of the current test apps have a `app.mjs`, because the effort is high. If you are adding a new test, it is wishful to also generate a `app.mjs`. You can use the npm module [cjs-to-es6](https://www.npmjs.com/package/cjs-to-es6) to generate the `app.mjs` from the `app.js`. Setting `RUN_ESM=true` locally will run use the ESM app instead of the CJS app.

## Executing code coverage tool

If you are actively developing a feature and you would like to know which lines and files you have alreasy covered in your tests, it's recommended to use `.only` for the target test file and then run:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"reinstall-deps": "lerna clean -y && rm -rf node_modules && npm install",
"test": "lerna run test --stream",
"test:ci": "lerna run test:ci --stream",
"test:ci:collector": "lerna run test:ci --scope=@instana/collector",
"test:debug": "lerna run test:debug --stream",
"typecheck": "tsc",
"update-deps": "lerna exec '../../node_modules/.bin/ncu --upgrade' && lerna exec 'npm i --package-lock-only' && npm run reinstall-deps",
Expand Down
31 changes: 31 additions & 0 deletions packages/collector/esm-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* (c) Copyright IBM Corp. 2022
*/

'use strict';

/**
* We currently only instrument CJS modules. As soon as we want
* to instrument ES modules (such as `got` v12), the requireHook will
* no longer work. Therefor we would need to wrap the target ES module
* with our instrumentations using the resolve & load hook.
*
* Usage:
* node --experimental-loader=@instana/collector/esm-loader.mjs server.js
*
* NOTE: When using ESM the customer can only configure the collector with
* ENV variables.
*/

import instana from './src/index.js';
instana();

/*
export async function resolve(specifier, context, nextResolve) {
return nextResolve(specifier, context, nextResolve);
}
export async function load(url, context, nextLoad) {
return nextLoad(url, context, nextLoad);
}
*/
3 changes: 2 additions & 1 deletion packages/collector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"files": [
"src",
"CHANGELOG.md"
"CHANGELOG.md",
"esm-loader.mjs"
],
"publishConfig": {
"access": "public"
Expand Down
10 changes: 9 additions & 1 deletion packages/collector/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ function init(_config) {
// initialized, potentially from a different installation of @instana/collector somewhere else in the file system.
// Find that module in the require cache and return its exports (this is necessary to make sure calls to our API
// work as expected).
const collectorIndexCacheKey = Object.keys(require.cache).find(
let collectorIndexCacheKey = Object.keys(require.cache).find(
cacheKey => cacheKey.indexOf('/@instana/collector/src/index.js') >= 0
);

// Requiring the collector package twice in the test package using a relative path such as `../../..`
if (process.env.NODE_ENV === 'test') {
collectorIndexCacheKey = Object.keys(require.cache).find(
cacheKey => cacheKey.indexOf('collector/src/index.js') >= 0
);
}

if (collectorIndexCacheKey) {
return require.cache[collectorIndexCacheKey].exports;
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/collector/test/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const mochaOptions = {
file: ['test/initEnv.js']
};

process.env.NODE_ENV = 'test';

if (process.env.CI) {
// Retry failed tests once on CI.
mochaOptions.retries = 1;
Expand Down
14 changes: 13 additions & 1 deletion packages/collector/test/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
// The globalAgent module manages an agent stub instance that can be used globally for all tests.

const { startGlobalAgent, stopGlobalAgent } = require('./globalAgent');

const isCI = require('@instana/core/test/test_util/is_ci');
const fs = require('fs');

exports.mochaHooks = {
async beforeAll() {
Expand All @@ -23,6 +23,18 @@ exports.mochaHooks = {
},

beforeEach() {
const testFile = this.currentTest.file;

if (process.env.RUN_ESM) {
const files = fs.readdirSync(testFile.split('/').slice(0, -1).join('/'));
const esmApp = files.find(f => f.indexOf('.mjs') !== -1);

if (!esmApp) {
this.skip();
return;
}
}

if (isCI()) {
// Troubleshooting issues on CI often involves timing-based questions like:
// * Why was this test application not able to connect to the database container? Did the DB container take too
Expand Down
19 changes: 18 additions & 1 deletion packages/collector/test/test_util/ProcessControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,24 @@ class ProcessControls {
throw new Error('Invalid config, appPath and dirname are mutually exclusive.');
}
if (!opts.appPath && opts.dirname) {
opts.appPath = path.join(opts.dirname, 'app');
opts.appPath = path.join(opts.dirname, 'app.js');
}

if (process.env.RUN_ESM && !opts.execArgv) {
if (opts.dirname) {
try {
const files = fs.readdirSync(opts.dirname);
const esmApp = files.find(f => f.indexOf('.mjs') !== -1);

if (esmApp) {
opts.execArgv = [`--experimental-loader=${path.join(__dirname, '..', '..', 'esm-loader.mjs')}`];
opts.appPath = path.join(opts.dirname, 'app.mjs');
}
} catch (err) {
// eslint-disable-next-line no-console
console.log('Unable to load the target app.mjs', err);
}
}
}

// absolute path to .js file that should be executed
Expand Down
92 changes: 92 additions & 0 deletions packages/collector/test/tracing/api/app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2019
*/

/* eslint-disable no-console */

'use strict';

import instanaFactory from '../../../src/index.js';
const instana = instanaFactory();

import express from 'express';
import morgan from 'morgan';
import bodyParser from 'body-parser';

const app = express();
const logPrefix = `API: Server (${process.pid}):\t`;

if (process.env.WITH_STDOUT) {
app.use(morgan(`${logPrefix}:method :url :status`));
}

app.use(bodyParser.json());

app.get('/', (req, res) => {
res.sendStatus(200);
});

app.get('/span/active', (req, res) => {
const span = instana.currentSpan();
res.json({
span: serialize(span)
});
});

app.get('/span/annotate-path-flat-string', (req, res) => {
const span = instana.currentSpan();
span.annotate('sdk.custom.tags.key', 'custom nested tag value');
span.annotate('http.path_tpl', '/custom/{template}');
span.annotate('..redundant....dots..', 'will be silently dropped');
res.json({
span: serialize(span)
});
});

app.get('/span/annotate-path-array', (req, res) => {
const span = instana.currentSpan();
span.annotate(['sdk', 'custom', 'tags', 'key'], 'custom nested tag value');
span.annotate(['http', 'path_tpl'], '/custom/{template}');
res.json({
span: serialize(span)
});
});

app.get('/span/manuallyended', (req, res) => {
const span = instana.currentSpan();
span.disableAutoEnd();
setTimeout(() => {
span.end(42);
res.json({
span: serialize(span)
});
}, 50);
});

app.listen(process.env.APP_PORT, () => {
log(`Listening on port: ${process.env.APP_PORT}`);
});

function serialize(span) {
return {
traceId: span.getTraceId(),
spanId: span.getSpanId(),
parentSpanId: span.getParentSpanId(),
name: span.getName(),
isEntry: span.isEntrySpan(),
isExit: span.isExitSpan(),
isIntermediate: span.isIntermediateSpan(),
timestamp: span.getTimestamp(),
duration: span.getDuration(),
errorCount: span.getErrorCount(),
data: span.span ? span.span.data : null,
handleConstructorName: span.constructor.name
};
}

function log() {
const args = Array.prototype.slice.call(arguments);
args[0] = logPrefix + args[0];
console.log.apply(console, args);
}
Loading

0 comments on commit 7d471ff

Please sign in to comment.