Skip to content

Commit

Permalink
feat: add fastify support (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
therightstuff authored Nov 1, 2023
1 parent 1e3086d commit 86b6acc
Show file tree
Hide file tree
Showing 12 changed files with 345 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ trace.getActiveSpan()?.addEvent('<error-message>', {'lumigo.type': '<error-type>
| grpc-js | [@grpc](https://www.npmjs.com/package/@grpc) | 1.8.0~1.8.20 |
| amqplib | [amqplib](https://www.npmjs.com/package/amqplib) | 0.9.0~0.10.3 |
| express | [express](https://www.npmjs.com/package/express) | 4.9.0~4.18.2 |
| fastify | [fastify](https://www.npmjs.com/package/fastify) | 3.29.0~4.24.3 |
| ioredis | [ioredis](https://www.npmjs.com/package/ioredis) | 4.0.0~4.28.5 |
| | | 5.0.0~5.3.2 |
| kafkajs | [kafkajs](https://www.npmjs.com/package/kafkajs) | 2.0.0~2.2.4 |
Expand Down
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@opentelemetry/exporter-trace-otlp-http": "0.38.0",
"@opentelemetry/instrumentation": "0.38.0",
"@opentelemetry/instrumentation-amqplib": "0.33.2",
"@opentelemetry/instrumentation-fastify": "^0.32.3",
"@opentelemetry/instrumentation-grpc": "^0.41.0",
"@opentelemetry/instrumentation-http": "0.38.0",
"@opentelemetry/instrumentation-ioredis": "^0.35.2",
Expand Down
2 changes: 2 additions & 0 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FileSpanExporter } from './exporters';
import LumigoGrpcInstrumentation from './instrumentations/@grpc/grpc-js/GrpcInstrumentation';
import LumigoAmqplibInstrumentation from './instrumentations/amqplib/AmqplibInstrumentation';
import LumigoExpressInstrumentation from './instrumentations/express/ExpressInstrumentation';
import LumigoFastifyInstrumentation from './instrumentations/fastify/FastifyInstrumentation';
import LumigoHttpInstrumentation from './instrumentations/https/HttpInstrumentation';
import LumigoIORedisInstrumentation from './instrumentations/ioredis/IORedisInstrumentation';
import LumigoKafkaJsInstrumentation from './instrumentations/kafkajs/KafkaJsInstrumentation';
Expand Down Expand Up @@ -95,6 +96,7 @@ export const init = async (): Promise<LumigoSdkInitialization> => {
new LumigoAmqplibInstrumentation(),
new LumigoExpressInstrumentation(),
new LumigoGrpcInstrumentation(),
new LumigoFastifyInstrumentation(),
new LumigoHttpInstrumentation(...ignoredHostnames),
new LumigoIORedisInstrumentation(),
new LumigoKafkaJsInstrumentation(),
Expand Down
43 changes: 43 additions & 0 deletions src/instrumentations/fastify/FastifyInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { CommonUtils, ScrubContext } from '@lumigo/node-core';
import { Span } from '@opentelemetry/api';
import { FastifyInstrumentation, FastifyRequestInfo } from '@opentelemetry/instrumentation-fastify';
import { contentType, scrubHttpPayload } from '../../tools/payloads';
import { getSpanAttributeMaxLength } from '../../utils';
import { Instrumentor } from '../instrumentor';

export default class LumigoFastifyInstrumentation extends Instrumentor<FastifyInstrumentation> {
getInstrumentedModule(): string {
return 'fastify';
}

getInstrumentation(): FastifyInstrumentation {
return new FastifyInstrumentation({
requestHook: function (span: Span, info: FastifyRequestInfo) {
span.setAttribute(
'http.request.headers',
CommonUtils.payloadStringify(
info.request.headers,
ScrubContext.HTTP_REQUEST_HEADERS,
getSpanAttributeMaxLength()
)
);
span.setAttribute(
'http.request.query',
CommonUtils.payloadStringify(
info.request.query,
ScrubContext.HTTP_REQUEST_QUERY,
getSpanAttributeMaxLength()
)
);
span.setAttribute(
'http.request.body',
scrubHttpPayload(
info.request.body,
contentType(info.request.headers),
ScrubContext.HTTP_REQUEST_BODY
)
);
},
});
}
}
19 changes: 19 additions & 0 deletions src/instrumentations/fastify/fastifyInstrumentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import LumigoFastifyInstrumentation from './FastifyInstrumentation';

describe('LumigoFastifyInstrumentation', () => {
let lumigoFastifyInstrumentation = new LumigoFastifyInstrumentation();

test('getInstrumentedModule should return "fastify"', () => {
expect(lumigoFastifyInstrumentation.getInstrumentedModule()).toEqual('fastify');
});

// should not be skipped, see https://lumigo.atlassian.net/browse/RD-11195
test.skip('requireIfAvailable should return required name', () => {
const child_process = require('child_process');
child_process.execSync('npm install fastify', { stdio: 'inherit' });
const fastify = require('fastify');

expect(lumigoFastifyInstrumentation.requireIfAvailable()).toEqual(fastify);
child_process.execSync('npm uninstall fastify', { stdio: 'inherit' });
});
});
56 changes: 56 additions & 0 deletions src/instrumentations/fastify/tested_versions/fastify
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
3.29.0
4.0.0
4.0.1
4.0.2
4.0.3
4.1.0
4.2.0
3.29.1
4.2.1
4.3.0
4.4.0
4.5.0
4.5.1
4.5.2
4.5.3
3.29.2
4.6.0
4.7.0
4.8.0
4.8.1
3.29.3
4.9.0
4.9.1
4.9.2
4.10.0
4.10.1
4.10.2
3.29.4
4.11.0
3.29.5
4.12.0
4.13.0
4.14.0
4.14.1
4.15.0
4.16.0
4.16.1
4.16.2
4.16.3
4.17.0
4.18.0
4.19.0
4.19.1
4.19.2
4.20.0
4.21.0
4.22.0
4.22.1
4.22.2
4.23.0
4.23.1
4.23.2
4.24.0
4.24.1
4.24.2
4.24.3
1 change: 1 addition & 0 deletions test/instrumentations/fastify/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spans
1 change: 1 addition & 0 deletions test/instrumentations/fastify/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock.json
42 changes: 42 additions & 0 deletions test/instrumentations/fastify/app/fastify_app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const lumigo = require('@lumigo/opentelemetry');
const fastify = require('fastify')({
logger: true,
});

require('log-timestamp');

let tracerProvider;

fastify.get('/test-scrubbing', async (request, reply) => {
reply.send({
Authorization: 'SECRET',
});
});

fastify.get('/', async (request, reply) => {
reply.send('server is ready');
});

fastify.get('/basic', async (request, reply) => {
await tracerProvider.forceFlush();
reply.header('Content-Type', 'text/plain').send('Hello world');
});

fastify.get('/quit', async (request, reply) => {
console.error('Received quit command');
await tracerProvider.forceFlush();
reply.send({}).then(async () => {
// fastify.close() takes too long to do its thing
process.exit(0);
});
});

fastify.listen({ port: 0 }, async (err, address) => {
if (err) throw err;
tracerProvider = (await lumigo.init).tracerProvider;
const port = fastify.server.address().port;
console.error(`HTTP server listening on port ${port}`);
if (process.send) {
process.send(port);
}
});
14 changes: 14 additions & 0 deletions test/instrumentations/fastify/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "lumigo-fastify-test",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "node -r @lumigo/opentelemetry fastify_app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@lumigo/opentelemetry": "file:../../../../distro.tgz",
"fastify": "^3.29.0"
}
}
131 changes: 131 additions & 0 deletions test/instrumentations/fastify/fastify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as fs from 'fs';
import 'jest-expect-message';
import 'jest-json';
import { join } from 'path';

import { SpanKind, SpanStatusCode } from '@opentelemetry/api';

import { itTest } from '../../integration/setup';
import { getSpanByKind } from '../../utils/spans';
import { TestApp } from '../../utils/test-apps';
import { installPackage, reinstallPackages, uninstallPackage } from '../../utils/test-setup';
import { versionsToTest } from '../../utils/versions';

const INSTRUMENTATION_NAME = `fastify`;
const SPANS_DIR = join(__dirname, 'spans');
const TEST_APP_DIR = join(__dirname, 'app');
const TEST_TIMEOUT = 20_000;

const expectedResourceAttributes = {
attributes: {
framework: 'node',
'lumigo.distro.version': expect.stringMatching(/1\.\d+\.\d+/),
'process.environ': expect.any(String),
'process.executable.name': 'node',
'process.pid': expect.any(Number),
'process.runtime.name': 'nodejs',
'process.runtime.version': expect.stringMatching(/\d+\.\d+\.\d+/),
'service.name': 'fastify',
'telemetry.sdk.language': 'nodejs',
'telemetry.sdk.name': 'opentelemetry',
'telemetry.sdk.version': expect.any(String),
},
};

describe.each(versionsToTest(INSTRUMENTATION_NAME, INSTRUMENTATION_NAME))(
`Instrumentation tests for the ${INSTRUMENTATION_NAME} package`,
function (versionToTest) {
let testApp: TestApp;

beforeAll(function () {
reinstallPackages({ appDir: TEST_APP_DIR });
fs.mkdirSync(SPANS_DIR, { recursive: true });
installPackage({
appDir: TEST_APP_DIR,
packageName: INSTRUMENTATION_NAME,
packageVersion: versionToTest,
});
});

afterEach(async function () {
try {
await testApp.kill();
} catch (err) {
console.warn('Failed to kill test app', err);
}
});

afterAll(function () {
uninstallPackage({
appDir: TEST_APP_DIR,
packageName: INSTRUMENTATION_NAME,
packageVersion: versionToTest,
});
});

itTest(
{
testName: `basics: ${versionToTest}`,
packageName: INSTRUMENTATION_NAME,
version: versionToTest,
timeout: TEST_TIMEOUT,
},
async function () {
const exporterFile = `${SPANS_DIR}/basics.${INSTRUMENTATION_NAME}@${versionToTest}.json`;

testApp = new TestApp(TEST_APP_DIR, INSTRUMENTATION_NAME, exporterFile, {
OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: '4096',
});

await testApp.invokeGetPath('/basic');

const spans = await testApp.getFinalSpans(2);

expect(getSpanByKind(spans, SpanKind.SERVER)).toMatchObject({
traceId: expect.any(String),
name: 'GET /basic',
id: expect.any(String),
kind: SpanKind.SERVER,
timestamp: expect.any(Number),
duration: expect.any(Number),
resource: expectedResourceAttributes,
attributes: {
'http.method': 'GET',
'http.target': '/basic',
'http.host': expect.stringMatching(/localhost:\d+/),
'http.scheme': 'http',
'net.peer.ip': expect.any(String),
'http.route': '/basic',
'http.status_code': 200,
},
status: {
code: SpanStatusCode.UNSET,
},
events: [],
});

expect(getSpanByKind(spans, SpanKind.INTERNAL)).toMatchObject({
traceId: expect.any(String),
parentId: expect.any(String),
name: expect.stringMatching(/request handler - .+/),
id: expect.any(String),
kind: SpanKind.INTERNAL,
timestamp: expect.any(Number),
duration: expect.any(Number),
resource: expectedResourceAttributes,
attributes: {
'http.request.query': '{}',
'http.request.headers': expect.stringMatching(/\{.*\}/),
// 'http.response.headers': expect.stringMatching(/\{.*\}/),
// 'http.response.body': '"Hello world"',
'http.route': '/basic',
},
status: {
code: SpanStatusCode.UNSET,
},
events: [],
});
}
);
}
);

0 comments on commit 86b6acc

Please sign in to comment.