From 9bad7569a20d6c7afcc806f032f3c2096e3629f0 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 1 Dec 2024 19:55:31 +0100 Subject: [PATCH 01/20] feat: initial commit --- .gitattributes | 2 + .github/dependabot.yml | 13 ++++ .github/stale.yml | 21 ++++++ .github/workflows/ci.yml | 23 ++++++ .gitignore | 152 +++++++++++++++++++++++++++++++++++++++ .npmrc | 1 + LICENSE | 21 ++++++ package.json | 40 +++++++++++ 8 files changed, 273 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/stale.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 LICENSE create mode 100644 package.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a0e7df9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Set default behavior to automatically convert line endings +* text=auto eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dfa7fa6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..d51ce63 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,21 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 15 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - "discussion" + - "feature request" + - "bug" + - "help wanted" + - "plugin suggestion" + - "good first issue" +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8ef7a89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: + - main + - master + - next + - 'v*' + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + test: + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 + with: + lint: true + license-check: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b6aed4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vim swap files +*.swp + +# macOS files +.DS_Store + +# Clinic +.clinic + +# lock files +bun.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock + +# editor files +.vscode +.idea + +#tap files +.tap/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..226c5a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Fastify + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..89a7644 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "@fastify/otel", + "version": "0.0.0", + "description": "Official Fastifyt OpenTelemetry Instrumentation", + "main": "index.js", + "type": "commonjs", + "types": "types/index.d.ts", + "scripts": { + "lint": "standard", + "test": "npm run test:unit && npm run test:typescript", + "test:unit": "c8 --100 node --test", + "test:coverage": "c8 node --test && c8 report --reporter=html", + "test:typescript": "tsd" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fastify/fastify-plugin.git" + }, + "keywords": [ + "plugin", + "helper", + "fastify" + ], + "author": "Carlos Fuentes - @metcoder95 (https://metcoder.dev)", + "license": "MIT", + "bugs": { + "url": "https://github.com/fastify/otel/issues" + }, + "homepage": "https://github.com/fastify/otel#readme", + "devDependencies": { + "@fastify/pre-commit": "^2.1.0", + "@fastify/type-provider-typebox": "^5.0.0-pre.fv5.1", + "@types/node": "^22.0.0", + "c8": "^10.1.2", + "fastify": "^5.0.0", + "proxyquire": "^2.1.3", + "standard": "^17.1.0", + "tsd": "^0.31.0" + } +} From 1f0230bd5f1a87e4b4d6d71737427700e9260379 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 6 Dec 2024 10:19:03 +0100 Subject: [PATCH 02/20] chore: standardise settings --- eslint.config.js | 6 ++++++ package.json | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..89fd678 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,6 @@ +'use strict' + +module.exports = require('neostandard')({ + ignores: require('neostandard').resolveIgnoresFromGitignore(), + ts: true +}) diff --git a/package.json b/package.json index 89a7644..628e4ac 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "commonjs", "types": "types/index.d.ts", "scripts": { - "lint": "standard", + "lint": "eslint", + "lint:fix": "eslint --fix", "test": "npm run test:unit && npm run test:typescript", "test:unit": "c8 --100 node --test", "test:coverage": "c8 node --test && c8 report --reporter=html", @@ -14,12 +15,15 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/fastify/fastify-plugin.git" + "url": "git+https://github.com/fastify/otel.git" }, "keywords": [ "plugin", "helper", - "fastify" + "fastify", + "instrumentation", + "otel", + "opentelemetry" ], "author": "Carlos Fuentes - @metcoder95 (https://metcoder.dev)", "license": "MIT", @@ -32,9 +36,10 @@ "@fastify/type-provider-typebox": "^5.0.0-pre.fv5.1", "@types/node": "^22.0.0", "c8": "^10.1.2", + "eslint": "^9.16.0", "fastify": "^5.0.0", + "neostandard": "^0.11.9", "proxyquire": "^2.1.3", - "standard": "^17.1.0", "tsd": "^0.31.0" } } From a47791176611b6d8be98b32c251b02acb217b923 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 11 Dec 2024 12:46:42 +0100 Subject: [PATCH 03/20] feat: initial implementation --- index.js | 252 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 18 +++- test/index.test.js | 155 ++++++++++++++++++++++++++++ 3 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 index.js create mode 100644 test/index.test.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..44852f7 --- /dev/null +++ b/index.js @@ -0,0 +1,252 @@ +'use strict' +const { context, trace, SpanStatusCode } = require('@opentelemetry/api') +const { getRPCMetadata, RPCType } = require('@opentelemetry/core') +const { + InstrumentationBase, + InstrumentationNodeModuleDefinition +} = require('@opentelemetry/instrumentation') + +const fp = require('fastify-plugin') + +const { + version: PACKAGE_VERSION, + name: PACKAGE_NAME +} = require('./package.json') + +// Constants +const SUPPORTED_VERSIONS = '>=5.0.0 <6' +const FASTIFY_HOOKS = [ + 'onRequest', + 'preParsing', + 'preValidation', + 'preHandler', + 'preSerialization', + 'onSend', + 'onResponse' +] +const ATTRIBUTE_NAMES = { + HOOK_NAME: 'hook.name', + FASTIFY_TYPE: 'fastify.type', + HOOK_CALLBACK_NAME: 'hook.callback.name' +} +const HOOK_TYPES = { + ROUTE: 'route-hook', + INSTANCE: 'hook', + HANDLER: 'request-handler' +} +const ANONYMOUS_FUNCTION_NAME = 'anonymous' + +// Symbols +const kInstrumentation = Symbol('fastify instrumentation instance') +const kRequestSpans = Symbol('fastify instrumentation request spans') + +class FastifyInstrumentation extends InstrumentationBase { + static FastifyInstrumentation = FastifyInstrumentation + static default = FastifyInstrumentation + + constructor (config) { + super(PACKAGE_NAME, PACKAGE_VERSION, config) + } + + init () { + return [ + new InstrumentationNodeModuleDefinition('fastify', [SUPPORTED_VERSIONS]) + ] + } + + plugin () { + const instrumentation = this + + return fp(FastifyInstrumentationPlugin, { + fastify: SUPPORTED_VERSIONS, + name: `@fastify/otel@${PACKAGE_VERSION}` + }) + + function FastifyInstrumentationPlugin (instance, opts, done) { + const addHookOriginal = instance.addHook.bind(instance) + + instance.decorate(kInstrumentation, instrumentation) + instance.decorateRequest(kRequestSpans, null) + + instance.addHook('onRoute', function (routeOptions) { + for (const hook of FASTIFY_HOOKS) { + if (routeOptions[hook] != null) { + const handlerLike = routeOptions[hook] + + if (typeof handlerLike === 'function') { + routeOptions[hook] = handlerWrapper(handlerLike, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `route-${hook}`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + handlerLike.name ?? ANONYMOUS_FUNCTION_NAME + }) + } else if (Array.isArray(handlerLike)) { + const wrappedHandlers = [] + + for (const handler of handlerLike) { + wrappedHandlers.push( + handlerWrapper(handler, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `route-${hook}`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + handler.name ?? ANONYMOUS_FUNCTION_NAME + }) + ) + } + + routeOptions[hook] = wrappedHandlers + } + } + } + + routeOptions.handler = handlerWrapper(routeOptions.handler, { + [ATTRIBUTE_NAMES.HOOK_NAME]: 'route-handler', + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.HANDLER, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + routeOptions.handler.name ?? ANONYMOUS_FUNCTION_NAME + }) + }) + + instance.addHook('onRequest', function (request, _reply, hookDone) { + if (this[kInstrumentation].isEnabled() === true) { + const rpcMetadata = getRPCMetadata(context.active()) + + if ( + request.routeOptions.url != null && + rpcMetadata?.type === RPCType.HTTP + ) { + rpcMetadata.route = request.routeOptions.url + } + + const span = this[kInstrumentation].tracer.startSpan('request', { + attributes: { + // TODO: abstract to constants + 'hook.name': 'onRequest', + 'fastify.type': 'hook', + 'plugin.name': '@fastify/otel' + } + }) + + request[kRequestSpans] = [span] + } + + hookDone() + }) + + instance.addHook('onResponse', function (request, _reply, hookDone) { + const spans = request[kRequestSpans] + + if (spans != null && spans.length !== 0) { + for (const span of spans) { + span.setStatus({ + code: SpanStatusCode.OK, + message: 'OK' + }) + span.end() + } + } + + request[kRequestSpans] = null + + hookDone() + }) + + instance.addHook('onError', function (request, _reply, error, hookDone) { + const spans = request[kRequestSpans] + + if (spans != null && spans.length !== 0) { + for (const span of spans) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + span.recordException(error) + span.end() + } + } + + request[kRequestSpans] = null + + hookDone() + }) + + instance.addHook = addHookPatched.bind(instance) + + done() + + function addHookPatched (name, hook) { + if (FASTIFY_HOOKS.includes(name)) { + addHookOriginal( + name, + handlerWrapper(hook, { + [ATTRIBUTE_NAMES.HOOK_NAME]: name, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + hook.name ?? ANONYMOUS_FUNCTION_NAME + }) + ) + } else { + addHookOriginal(name, hook) + } + } + + function handlerWrapper (handler, spanAttributes = {}) { + return function (...args) { + const instrumenation = this[kInstrumentation] + + if (instrumenation.isEnabled() === false) { + return handler.call(this, ...args) + } + + const span = instrumenation.tracer.startSpan('request', { + attributes: spanAttributes + }) + + console.log(args) + args[0][kRequestSpans].push(span) + + return context.with( + context.active(), + trace.setSpan(context.active(), span), + function () { + try { + const res = handler.call(this, ...args) + + if (typeof res?.then === 'function') { + return res.then( + result => { + span.end() + return result + }, + error => { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + span.recordException(error) + span.end() + return Promise.reject(error) + } + ) + } + + span.end() + return res + } catch (error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + span.recordException(error) + span.end() + } + }, + this + ) + } + } + } + } +} + +module.exports = FastifyInstrumentation diff --git a/package.json b/package.json index 628e4ac..d3518d5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@fastify/otel", "version": "0.0.0", - "description": "Official Fastifyt OpenTelemetry Instrumentation", + "description": "Official Fastify OpenTelemetry Instrumentation", "main": "index.js", "type": "commonjs", "types": "types/index.d.ts", @@ -34,12 +34,26 @@ "devDependencies": { "@fastify/pre-commit": "^2.1.0", "@fastify/type-provider-typebox": "^5.0.0-pre.fv5.1", + "@opentelemetry/context-async-hooks": "^1.29.0", + "@opentelemetry/contrib-test-utils": "^0.44.0", + "@opentelemetry/instrumentation-http": "^0.56.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/sdk-trace-node": "^1.29.0", "@types/node": "^22.0.0", "c8": "^10.1.2", "eslint": "^9.16.0", - "fastify": "^5.0.0", + "fastify": "^5.1.0", "neostandard": "^0.11.9", "proxyquire": "^2.1.3", "tsd": "^0.31.0" + }, + "dependencies": { + "@opentelemetry/core": "^1.29.0", + "@opentelemetry/instrumentation": "^0.56.0", + "@opentelemetry/semantic-conventions": "^1.28.0", + "fastify-plugin": "^5.0.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" } } diff --git a/test/index.test.js b/test/index.test.js new file mode 100644 index 0000000..4e01808 --- /dev/null +++ b/test/index.test.js @@ -0,0 +1,155 @@ +const { + test, + describe, + before, + after, + afterEach, + beforeEach +} = require('node:test') +// const http = require('node:http') + +const { InstrumentationBase } = require('@opentelemetry/instrumentation') +const { + AsyncHooksContextManager +} = require('@opentelemetry/context-async-hooks') +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') +const { + InMemorySpanExporter, + ReadableSpan, + SimpleSpanProcessor +} = require('@opentelemetry/sdk-trace-base') +const { Span, context, SpanStatusCode } = require('@opentelemetry/api') +const { + getPackageVersion, + runTestFixture, + TestCollector +} = require('@opentelemetry/contrib-test-utils') +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http') +const semver = require('semver') +const Fastify = require('fastify') + +const FastifyInstrumentation = require('..') + +describe('Interface', () => { + test('should exports support', t => { + t.assert.equal(FastifyInstrumentation.name, 'FastifyInstrumentation') + t.assert.equal( + FastifyInstrumentation.default.name, + 'FastifyInstrumentation' + ) + t.assert.equal( + FastifyInstrumentation.FastifyInstrumentation.name, + 'FastifyInstrumentation' + ) + t.assert.strictEqual( + Object.getPrototypeOf(FastifyInstrumentation), + InstrumentationBase + ) + }) + + test('FastifyInstrumentation#plugin should return a valid Fastify Plugin', async t => { + const app = Fastify() + const instrumentation = new FastifyInstrumentation() + const plugin = instrumentation.plugin() + + t.assert.equal(typeof plugin, 'function') + t.assert.equal(plugin.length, 3) + + app.register(plugin) + + await app.ready() + }) +}) + +describe('FastifyInstrumentation', () => { + const httpInstrumentation = new HttpInstrumentation() + const instrumentation = new FastifyInstrumentation() + const contextManager = new AsyncHooksContextManager() + const memoryExporter = new InMemorySpanExporter() + const provider = new NodeTracerProvider() + const spanProcessor = new SimpleSpanProcessor(memoryExporter) + describe('Instrumentation#disabled', () => { + instrumentation.setTracerProvider(provider) + httpInstrumentation.setTracerProvider(provider) + context.setGlobalContextManager(contextManager) + + test('should not create spans if disabled', async t => { + before(() => { + contextManager.enable() + }) + + after(() => { + contextManager.disable() + spanProcessor.forceFlush() + memoryExporter.reset() + instrumentation.disable() + httpInstrumentation.disable() + }) + + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get('/', async (request, reply) => 'hello world') + + instrumentation.disable() + + t.plan(3) + + const response = await app.inject({ + method: 'GET', + url: '/' + }) + + const spans = memoryExporter + .getFinishedSpans() + .find(span => span.instrumentationLibrary.name === '@fastify/otel') + + t.assert.ok(spans == null) + t.assert.equal(response.statusCode, 200) + t.assert.equal(response.body, 'hello world') + }) + }) + + describe('Instrumentation#enabled', () => { + beforeEach(() => { + contextManager.enable() + }) + + afterEach(() => { + contextManager.disable() + instrumentation.disable() + httpInstrumentation.disable() + spanProcessor.forceFlush() + memoryExporter.reset() + }) + + test('should anonymous span', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get('/', async (request, reply) => 'hello world') + + t.plan(3) + + // Might need to call HTTP + const response = await app.inject({ + method: 'GET', + url: '/' + }) + + const spans = memoryExporter + .getFinishedSpans() + .find(span => span.instrumentationLibrary.name === '@fastify/otel') + + console.log(spans) + + t.assert.ok(spans == null) + t.assert.equal(response.statusCode, 200) + t.assert.equal(response.body, 'hello world') + }) + }) +}) From 6e7b228ac84e9dc211ee2d7a9c6d6cf30571248f Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 13 Dec 2024 12:45:28 +0100 Subject: [PATCH 04/20] feat: support more cases --- index.js | 83 ++++++++++++++------- test/index.test.js | 181 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 222 insertions(+), 42 deletions(-) diff --git a/index.js b/index.js index 44852f7..70cffb1 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ 'use strict' const { context, trace, SpanStatusCode } = require('@opentelemetry/api') const { getRPCMetadata, RPCType } = require('@opentelemetry/core') +const { ATTR_HTTP_ROUTE } = require('@opentelemetry/semantic-conventions') const { InstrumentationBase, InstrumentationNodeModuleDefinition @@ -22,7 +23,8 @@ const FASTIFY_HOOKS = [ 'preHandler', 'preSerialization', 'onSend', - 'onResponse' + 'onResponse', + 'onError' ] const ATTRIBUTE_NAMES = { HOOK_NAME: 'hook.name', @@ -75,8 +77,9 @@ class FastifyInstrumentation extends InstrumentationBase { if (typeof handlerLike === 'function') { routeOptions[hook] = handlerWrapper(handlerLike, { - [ATTRIBUTE_NAMES.HOOK_NAME]: `route-${hook}`, + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route -> ${hook}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, + [ATTR_HTTP_ROUTE]: routeOptions.url, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: handlerLike.name ?? ANONYMOUS_FUNCTION_NAME }) @@ -86,8 +89,9 @@ class FastifyInstrumentation extends InstrumentationBase { for (const handler of handlerLike) { wrappedHandlers.push( handlerWrapper(handler, { - [ATTRIBUTE_NAMES.HOOK_NAME]: `route-${hook}`, + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route -> ${hook}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, + [ATTR_HTTP_ROUTE]: routeOptions.url, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: handler.name ?? ANONYMOUS_FUNCTION_NAME }) @@ -99,11 +103,32 @@ class FastifyInstrumentation extends InstrumentationBase { } } + // We always want to add the onSend hook to the route to be executed last + if (routeOptions.onSend != null) { + routeOptions.onSend = Array.isArray(routeOptions.onSend) + ? [...routeOptions.onSend, onSendHook] + : [routeOptions.onSend, onSendHook] + } else { + routeOptions.onSend = onSendHook + } + + // We always want to add the onError hook to the route to be executed last + if (routeOptions.onError != null) { + routeOptions.onError = Array.isArray(routeOptions.onError) + ? [...routeOptions.onError, onErrorHook] + : [routeOptions.onError, onErrorHook] + } else { + routeOptions.onError = onErrorHook + } + routeOptions.handler = handlerWrapper(routeOptions.handler, { - [ATTRIBUTE_NAMES.HOOK_NAME]: 'route-handler', + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.HANDLER, + [ATTR_HTTP_ROUTE]: routeOptions.url, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - routeOptions.handler.name ?? ANONYMOUS_FUNCTION_NAME + routeOptions.handler.name.length > 0 + ? routeOptions.handler.name + : ANONYMOUS_FUNCTION_NAME }) }) @@ -121,9 +146,10 @@ class FastifyInstrumentation extends InstrumentationBase { const span = this[kInstrumentation].tracer.startSpan('request', { attributes: { // TODO: abstract to constants - 'hook.name': 'onRequest', - 'fastify.type': 'hook', - 'plugin.name': '@fastify/otel' + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - onRequest`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: 'hook', + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: '@fastify/otel', + [ATTR_HTTP_ROUTE]: request.routeOptions.url } }) @@ -133,7 +159,11 @@ class FastifyInstrumentation extends InstrumentationBase { hookDone() }) - instance.addHook('onResponse', function (request, _reply, hookDone) { + instance.addHook = addHookPatched.bind(instance) + + done() + + function onSendHook (request, _reply, payload, hookDone) { const spans = request[kRequestSpans] if (spans != null && spans.length !== 0) { @@ -148,10 +178,10 @@ class FastifyInstrumentation extends InstrumentationBase { request[kRequestSpans] = null - hookDone() - }) + hookDone(null, payload) + } - instance.addHook('onError', function (request, _reply, error, hookDone) { + function onErrorHook (request, reply, error, hookDone) { const spans = request[kRequestSpans] if (spans != null && spans.length !== 0) { @@ -168,18 +198,14 @@ class FastifyInstrumentation extends InstrumentationBase { request[kRequestSpans] = null hookDone() - }) - - instance.addHook = addHookPatched.bind(instance) - - done() + } function addHookPatched (name, hook) { if (FASTIFY_HOOKS.includes(name)) { addHookOriginal( name, handlerWrapper(hook, { - [ATTRIBUTE_NAMES.HOOK_NAME]: name, + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - ${name}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: hook.name ?? ANONYMOUS_FUNCTION_NAME @@ -191,22 +217,27 @@ class FastifyInstrumentation extends InstrumentationBase { } function handlerWrapper (handler, spanAttributes = {}) { - return function (...args) { - const instrumenation = this[kInstrumentation] + return function handlerWrapped (...args) { + const instrumentation = this[kInstrumentation] - if (instrumenation.isEnabled() === false) { + if (instrumentation.isEnabled() === false) { return handler.call(this, ...args) } - const span = instrumenation.tracer.startSpan('request', { - attributes: spanAttributes - }) + const span = instrumentation.tracer.startSpan( + `handler - ${ + handler.name?.length > 0 + ? handler.name + : this.pluginName ?? ANONYMOUS_FUNCTION_NAME + }`, + { + attributes: spanAttributes + } + ) - console.log(args) args[0][kRequestSpans].push(span) return context.with( - context.active(), trace.setSpan(context.active(), span), function () { try { diff --git a/test/index.test.js b/test/index.test.js index 4e01808..01988f3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -68,11 +68,13 @@ describe('FastifyInstrumentation', () => { const memoryExporter = new InMemorySpanExporter() const provider = new NodeTracerProvider() const spanProcessor = new SimpleSpanProcessor(memoryExporter) - describe('Instrumentation#disabled', () => { - instrumentation.setTracerProvider(provider) - httpInstrumentation.setTracerProvider(provider) - context.setGlobalContextManager(contextManager) + provider.addSpanProcessor(spanProcessor) + context.setGlobalContextManager(contextManager) + httpInstrumentation.setTracerProvider(provider) + instrumentation.setTracerProvider(provider) + + describe('Instrumentation#disabled', () => { test('should not create spans if disabled', async t => { before(() => { contextManager.enable() @@ -112,8 +114,10 @@ describe('FastifyInstrumentation', () => { }) }) - describe('Instrumentation#enabled', () => { + describe('Instrumentation#enabled', { only: true }, () => { beforeEach(() => { + instrumentation.enable() + httpInstrumentation.enable() contextManager.enable() }) @@ -125,7 +129,7 @@ describe('FastifyInstrumentation', () => { memoryExporter.reset() }) - test('should anonymous span', async t => { + test('should create anonymous span (simple case)', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -133,23 +137,168 @@ describe('FastifyInstrumentation', () => { app.get('/', async (request, reply) => 'hello world') - t.plan(3) + await app.listen() - // Might need to call HTTP - const response = await app.inject({ - method: 'GET', - url: '/' + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start] = spans + + t.plan(6) + t.assert.equal(spans.length, 2) + t.assert.ok( + spans.every( + span => + span.ended === true && + span.spanContext().spanId !== null && + ['request-handler', 'hook'].includes( + span.attributes['fastify.type'] + ) + ) + ) + t.assert.deepStrictEqual(start.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', + 'fastify.type': 'hook', + 'hook.callback.name': '@fastify/otel', + 'http.route': '/' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'anonymous' + }) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should create named span (simple case)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get('/', async function helloworld () { + return 'hello world' + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start] = spans + + t.plan(6) + t.assert.equal(spans.length, 2) + t.assert.ok( + spans.every( + span => + span.ended === true && + span.spanContext().spanId !== null && + ['request-handler', 'hook'].includes( + span.attributes['fastify.type'] + ) + ) + ) + t.assert.deepStrictEqual(start.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', + 'fastify.type': 'hook', + 'hook.callback.name': '@fastify/otel', + 'http.route': '/' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' }) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should end spans upon error', { only: true }, async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + onError: function decorated (request, reply, error, done) { + done(error) + } + }, + async function helloworld () { + throw new Error('error') + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) const spans = memoryExporter .getFinishedSpans() - .find(span => span.instrumentationLibrary.name === '@fastify/otel') + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') - console.log(spans) + const [end, start, error] = spans - t.assert.ok(spans == null) - t.assert.equal(response.statusCode, 200) - t.assert.equal(response.body, 'hello world') + t.plan(7) + t.assert.equal(spans.length, 3) + t.assert.ok( + spans.every( + span => + span.ended === true && + span.spanContext().spanId !== null && + ['request-handler', 'hook', 'route-hook'].includes( + span.attributes['fastify.type'] + ) + ) + ) + t.assert.deepStrictEqual(start.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', + 'fastify.type': 'hook', + 'hook.callback.name': '@fastify/otel', + 'http.route': '/' + }) + t.assert.deepStrictEqual(error.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', + 'fastify.type': 'route-hook', + 'hook.callback.name': 'decorated', + 'http.route': '/' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(response.status, 500) + t.assert.deepStrictEqual(await response.json(), { + statusCode: 500, + error: 'Internal Server Error', + message: 'error' + }) }) }) }) From 8f36917606ca0b1a7e4d77f9af4ee1a2541da0bf Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 13 Dec 2024 12:50:47 +0100 Subject: [PATCH 05/20] chore: small helper --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 70cffb1..0648c03 100644 --- a/index.js +++ b/index.js @@ -218,6 +218,7 @@ class FastifyInstrumentation extends InstrumentationBase { function handlerWrapper (handler, spanAttributes = {}) { return function handlerWrapped (...args) { + /** @type {FastifyInstrumentation} */ const instrumentation = this[kInstrumentation] if (instrumentation.isEnabled() === false) { From f6725131ebec73e8244c420ecfb225260ed4517d Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 15 Dec 2024 13:04:51 +0100 Subject: [PATCH 06/20] refactor: change attributes of parent span --- index.js | 17 +++++++------ test/index.test.js | 61 ++++++++++++---------------------------------- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/index.js b/index.js index 0648c03..7b649ef 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,11 @@ 'use strict' const { context, trace, SpanStatusCode } = require('@opentelemetry/api') const { getRPCMetadata, RPCType } = require('@opentelemetry/core') -const { ATTR_HTTP_ROUTE } = require('@opentelemetry/semantic-conventions') +const { + ATTR_HTTP_ROUTE, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_HTTP_REQUEST_METHOD +} = require('@opentelemetry/semantic-conventions') const { InstrumentationBase, InstrumentationNodeModuleDefinition @@ -29,7 +33,8 @@ const FASTIFY_HOOKS = [ const ATTRIBUTE_NAMES = { HOOK_NAME: 'hook.name', FASTIFY_TYPE: 'fastify.type', - HOOK_CALLBACK_NAME: 'hook.callback.name' + HOOK_CALLBACK_NAME: 'hook.callback.name', + ROOT: 'fastify.root', } const HOOK_TYPES = { ROUTE: 'route-hook', @@ -145,11 +150,9 @@ class FastifyInstrumentation extends InstrumentationBase { const span = this[kInstrumentation].tracer.startSpan('request', { attributes: { - // TODO: abstract to constants - [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - onRequest`, - [ATTRIBUTE_NAMES.FASTIFY_TYPE]: 'hook', - [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: '@fastify/otel', - [ATTR_HTTP_ROUTE]: request.routeOptions.url + [ATTRIBUTE_NAMES.ROOT]: '@fastify/otel', + [ATTR_HTTP_ROUTE]: request.url, + [ATTR_HTTP_REQUEST_METHOD]: request.method } }) diff --git a/test/index.test.js b/test/index.test.js index 01988f3..278de59 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -114,7 +114,7 @@ describe('FastifyInstrumentation', () => { }) }) - describe('Instrumentation#enabled', { only: true }, () => { + describe('Instrumentation#enabled', () => { beforeEach(() => { instrumentation.enable() httpInstrumentation.enable() @@ -151,23 +151,13 @@ describe('FastifyInstrumentation', () => { const [end, start] = spans - t.plan(6) + t.plan(5) t.assert.equal(spans.length, 2) - t.assert.ok( - spans.every( - span => - span.ended === true && - span.spanContext().spanId !== null && - ['request-handler', 'hook'].includes( - span.attributes['fastify.type'] - ) - ) - ) t.assert.deepStrictEqual(start.attributes, { - 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', - 'fastify.type': 'hook', - 'hook.callback.name': '@fastify/otel', - 'http.route': '/' + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', @@ -205,21 +195,11 @@ describe('FastifyInstrumentation', () => { t.plan(6) t.assert.equal(spans.length, 2) - t.assert.ok( - spans.every( - span => - span.ended === true && - span.spanContext().spanId !== null && - ['request-handler', 'hook'].includes( - span.attributes['fastify.type'] - ) - ) - ) t.assert.deepStrictEqual(start.attributes, { - 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', - 'fastify.type': 'hook', - 'hook.callback.name': '@fastify/otel', - 'http.route': '/' + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', @@ -227,6 +207,7 @@ describe('FastifyInstrumentation', () => { 'http.route': '/', 'hook.callback.name': 'helloworld' }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) t.assert.equal(response.status, 200) t.assert.equal(await response.text(), 'hello world') }) @@ -263,23 +244,13 @@ describe('FastifyInstrumentation', () => { const [end, start, error] = spans - t.plan(7) + t.plan(8) t.assert.equal(spans.length, 3) - t.assert.ok( - spans.every( - span => - span.ended === true && - span.spanContext().spanId !== null && - ['request-handler', 'hook', 'route-hook'].includes( - span.attributes['fastify.type'] - ) - ) - ) t.assert.deepStrictEqual(start.attributes, { - 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onRequest', - 'fastify.type': 'hook', - 'hook.callback.name': '@fastify/otel', - 'http.route': '/' + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 500 }) t.assert.deepStrictEqual(error.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', From df998b6a39704875c97298d90ec86f502238adae Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 15 Dec 2024 13:05:40 +0100 Subject: [PATCH 07/20] feat: propagate context --- index.js | 56 +++++++++++++++++++++++++--------------------- test/index.test.js | 2 ++ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index 7b649ef..ed82ed7 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,7 @@ const ANONYMOUS_FUNCTION_NAME = 'anonymous' // Symbols const kInstrumentation = Symbol('fastify instrumentation instance') const kRequestSpans = Symbol('fastify instrumentation request spans') +const kRequestContext = Symbol('fastify instrumentation request context') class FastifyInstrumentation extends InstrumentationBase { static FastifyInstrumentation = FastifyInstrumentation @@ -71,9 +72,12 @@ class FastifyInstrumentation extends InstrumentationBase { function FastifyInstrumentationPlugin (instance, opts, done) { const addHookOriginal = instance.addHook.bind(instance) + const setNotFoundHandlerOriginal = + instance.setNotFoundHandler.bind(instance) instance.decorate(kInstrumentation, instrumentation) instance.decorateRequest(kRequestSpans, null) + instance.decorateRequest(kRequestContext, null) instance.addHook('onRoute', function (routeOptions) { for (const hook of FASTIFY_HOOKS) { @@ -156,7 +160,8 @@ class FastifyInstrumentation extends InstrumentationBase { } }) - request[kRequestSpans] = [span] + request[kRequestContext] = trace.setSpan(context.active(), span) + request[kRequestSpans] = span } hookDone() @@ -166,17 +171,19 @@ class FastifyInstrumentation extends InstrumentationBase { done() - function onSendHook (request, _reply, payload, hookDone) { - const spans = request[kRequestSpans] + function onSendHook (request, reply, payload, hookDone) { + /** @type {import('@opentelemetry/api').Span} */ + const span = request[kRequestSpans] - if (spans != null && spans.length !== 0) { - for (const span of spans) { - span.setStatus({ - code: SpanStatusCode.OK, - message: 'OK' - }) - span.end() - } + if (span != null) { + span.setStatus({ + code: SpanStatusCode.OK, + message: 'OK' + }) + span.setAttributes({ + [ATTR_HTTP_RESPONSE_STATUS_CODE]: reply.statusCode + }) + span.end() } request[kRequestSpans] = null @@ -185,17 +192,15 @@ class FastifyInstrumentation extends InstrumentationBase { } function onErrorHook (request, reply, error, hookDone) { - const spans = request[kRequestSpans] + const span = request[kRequestSpans] - if (spans != null && spans.length !== 0) { - for (const span of spans) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message - }) - span.recordException(error) - span.end() - } + if (span != null) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + span.recordException(error) + span.end() } request[kRequestSpans] = null @@ -223,11 +228,13 @@ class FastifyInstrumentation extends InstrumentationBase { return function handlerWrapped (...args) { /** @type {FastifyInstrumentation} */ const instrumentation = this[kInstrumentation] + const [request] = args if (instrumentation.isEnabled() === false) { return handler.call(this, ...args) } + const ctx = request[kRequestContext] const span = instrumentation.tracer.startSpan( `handler - ${ handler.name?.length > 0 @@ -236,13 +243,12 @@ class FastifyInstrumentation extends InstrumentationBase { }`, { attributes: spanAttributes - } + }, + ctx ) - args[0][kRequestSpans].push(span) - return context.with( - trace.setSpan(context.active(), span), + trace.setSpan(ctx, span), function () { try { const res = handler.call(this, ...args) diff --git a/test/index.test.js b/test/index.test.js index 278de59..0ec713b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -264,6 +264,8 @@ describe('FastifyInstrumentation', () => { 'http.route': '/', 'hook.callback.name': 'helloworld' }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(error.parentSpanId, start.spanContext().spanId) t.assert.equal(response.status, 500) t.assert.deepStrictEqual(await response.json(), { statusCode: 500, From c656a905e7d4a6743d992ec43f56ec30d89226aa Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 15 Dec 2024 13:05:57 +0100 Subject: [PATCH 08/20] feat: add support for not found handler --- index.js | 60 ++++++++++++++++ test/index.test.js | 172 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 231 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ed82ed7..eb7b3db 100644 --- a/index.js +++ b/index.js @@ -152,6 +152,7 @@ class FastifyInstrumentation extends InstrumentationBase { rpcMetadata.route = request.routeOptions.url } + /** @type {Span} */ const span = this[kInstrumentation].tracer.startSpan('request', { attributes: { [ATTRIBUTE_NAMES.ROOT]: '@fastify/otel', @@ -167,7 +168,28 @@ class FastifyInstrumentation extends InstrumentationBase { hookDone() }) + // onResponse is the last hook to be executed, only added for 404 handlers + instance.addHook('onResponse', function (request, reply, hookDone) { + const span = request[kRequestSpans] + + if (span != null) { + span.setStatus({ + code: SpanStatusCode.OK, + message: 'OK' + }) + span.setAttributes({ + [ATTR_HTTP_RESPONSE_STATUS_CODE]: 404 + }) + span.end() + } + + request[kRequestSpans] = null + + hookDone() + }) + instance.addHook = addHookPatched.bind(instance) + instance.setNotFoundHandler = setNotFoundHandlerPatched.bind(instance) done() @@ -224,6 +246,44 @@ class FastifyInstrumentation extends InstrumentationBase { } } + function setNotFoundHandlerPatched (hooks, handler) { + if (typeof hooks === 'function') { + handler = handlerWrapper(hooks, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + hooks.name ?? ANONYMOUS_FUNCTION_NAME + }) + setNotFoundHandlerOriginal(handler) + } else { + if (hooks.preValidation != null) { + hooks.preValidation = handlerWrapper(hooks.preValidation, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preValidation`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + hooks.preValidation.name ?? ANONYMOUS_FUNCTION_NAME + }) + } + + if (hooks.preHandler != null) { + hooks.preHandler = handlerWrapper(hooks.preHandler, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preHandler`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + hooks.preHandler.name ?? ANONYMOUS_FUNCTION_NAME + }) + } + + handler = handlerWrapper(handler, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + hooks.name ?? ANONYMOUS_FUNCTION_NAME + }) + setNotFoundHandlerOriginal(hooks, handler) + } + } + function handlerWrapper (handler, spanAttributes = {}) { return function handlerWrapped (...args) { /** @type {FastifyInstrumentation} */ diff --git a/test/index.test.js b/test/index.test.js index 0ec713b..ed8aaf9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -212,7 +212,177 @@ describe('FastifyInstrumentation', () => { t.assert.equal(await response.text(), 'hello world') }) - test('should end spans upon error', { only: true }, async t => { + test('should create named span (404)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get('/', async function helloworld () { + return 'hello world' + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/`, + { method: 'POST' } + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [start] = spans + + t.plan(3) + t.assert.equal(response.status, 404) + t.assert.equal(spans.length, 1) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'POST', + 'http.response.status_code': 404 + }) + }) + + test('should create named span (404 - customized)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.setNotFoundHandler(async function notFoundHandler (request, reply) { + reply.code(404).send('not found') + }) + + app.get('/', async function helloworld () { + return 'hello world' + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/`, + { method: 'POST' } + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [start, fof] = spans + + t.plan(4) + t.assert.equal(response.status, 404) + t.assert.equal(spans.length, 2) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'POST', + 'http.response.status_code': 404 + }) + t.assert.deepStrictEqual(fof.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', + 'fastify.type': 'hook', + 'hook.callback.name': 'notFoundHandler' + }) + }) + + test('should create named span (404 - customized with hooks)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.setNotFoundHandler( + { + preHandler: function preHandler (request, reply, done) { + done() + }, + preValidation: function preValidation (request, reply, done) { + done() + } + }, + async function notFoundHandler (request, reply) { + reply.code(404).send('not found') + } + ) + + app.get( + '/', + { + schema: { + headers: { + type: 'object', + properties: { + 'x-foo': { type: 'string' } + } + } + } + }, + async function helloworld () { + return 'hello world' + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/`, + { method: 'POST' } + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preHandler, preValidation, start, fof] = spans + + t.plan(9) + t.assert.equal(response.status, 404) + t.assert.equal(spans.length, 4) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'POST', + 'http.response.status_code': 404 + }) + t.assert.deepStrictEqual(preHandler.attributes, { + 'hook.name': + 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preHandler', + 'fastify.type': 'hook', + 'hook.callback.name': 'preHandler' + }) + t.assert.deepStrictEqual(preValidation.attributes, { + 'hook.name': + 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preValidation', + 'fastify.type': 'hook', + 'hook.callback.name': 'preValidation' + }) + t.assert.deepStrictEqual(fof.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous' + }) + t.assert.equal(fof.parentSpanId, start.spanContext().spanId) + t.assert.equal(preValidation.parentSpanId, start.spanContext().spanId) + t.assert.equal(preHandler.parentSpanId, start.spanContext().spanId) + }) + + /** + * Note: Spans does not seem yet to be connected through the parent + * Most likely as we are not using the span made on the onRequest hook + * and setting it as parent + * Find a way to link the root span made from the onRequst down to the childs + */ + test('should end spans upon error', async t => { const app = Fastify() const plugin = instrumentation.plugin() From 26979f6cf7e0c29330a901f249c03bdcd0a03fca Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 16 Dec 2024 12:44:00 +0100 Subject: [PATCH 09/20] feat: better support for onError --- index.js | 77 +++++++++++--- test/index.test.js | 248 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 300 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index eb7b3db..7aed22a 100644 --- a/index.js +++ b/index.js @@ -27,14 +27,13 @@ const FASTIFY_HOOKS = [ 'preHandler', 'preSerialization', 'onSend', - 'onResponse', - 'onError' + 'onResponse' ] const ATTRIBUTE_NAMES = { HOOK_NAME: 'hook.name', FASTIFY_TYPE: 'fastify.type', HOOK_CALLBACK_NAME: 'hook.callback.name', - ROOT: 'fastify.root', + ROOT: 'fastify.root' } const HOOK_TYPES = { ROUTE: 'route-hook', @@ -90,7 +89,9 @@ class FastifyInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, [ATTR_HTTP_ROUTE]: routeOptions.url, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - handlerLike.name ?? ANONYMOUS_FUNCTION_NAME + handlerLike.name?.length > 0 + ? handlerLike.name + : ANONYMOUS_FUNCTION_NAME }) } else if (Array.isArray(handlerLike)) { const wrappedHandlers = [] @@ -102,7 +103,9 @@ class FastifyInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, [ATTR_HTTP_ROUTE]: routeOptions.url, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - handler.name ?? ANONYMOUS_FUNCTION_NAME + handler.name?.length > 0 + ? handler.name + : ANONYMOUS_FUNCTION_NAME }) ) } @@ -123,9 +126,15 @@ class FastifyInstrumentation extends InstrumentationBase { // We always want to add the onError hook to the route to be executed last if (routeOptions.onError != null) { - routeOptions.onError = Array.isArray(routeOptions.onError) - ? [...routeOptions.onError, onErrorHook] - : [routeOptions.onError, onErrorHook] + routeOptions.onError = onErrorHookWrapper(routeOptions.onError, { + [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - onError`, + [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, + [ATTR_HTTP_ROUTE]: routeOptions.url, + [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: + routeOptions.onError.name?.length > 0 + ? routeOptions.onError.name + : ANONYMOUS_FUNCTION_NAME + }) } else { routeOptions.onError = onErrorHook } @@ -214,6 +223,7 @@ class FastifyInstrumentation extends InstrumentationBase { } function onErrorHook (request, reply, error, hookDone) { + /** @type {Span} */ const span = request[kRequestSpans] if (span != null) { @@ -222,14 +232,58 @@ class FastifyInstrumentation extends InstrumentationBase { message: error.message }) span.recordException(error) - span.end() } - request[kRequestSpans] = null - hookDone() } + // TODO: replace and test out + function onErrorHookWrapper (hook, attr) { + const wrapped = handlerWrapper(hook, attr) + + return function onErrorHookWrapped (request, reply, error, hookDone) { + /** @type {Span} */ + const span = request[kRequestSpans] + + if (span == null) { + return wrapped.call(this, request, reply, error, hookDone) + } + + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message + }) + span.recordException(error) + + try { + if (hook.length === 4) { + const wrappedDone = () => { + hookDone() + } + + wrapped.call(this, request, reply, error, wrappedDone) + return + } + + wrapped.call(this, request, reply, error).then( + () => { + span.end() + hookDone() + }, + () => { + span.end() + hookDone() + } + ) + } catch (err) { + span.recordException(err) + span.end() + } + + request[kRequestSpans] = null + } + } + function addHookPatched (name, hook) { if (FASTIFY_HOOKS.includes(name)) { addHookOriginal( @@ -332,7 +386,6 @@ class FastifyInstrumentation extends InstrumentationBase { } span.end() - return res } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, diff --git a/test/index.test.js b/test/index.test.js index ed8aaf9..ed59c22 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -212,6 +212,89 @@ describe('FastifyInstrumentation', () => { t.assert.equal(await response.text(), 'hello world') }) + test('should create span for different hooks', { only: true }, async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + preHandler: function preHandler (request, reply, done) { + done() + }, + onRequest: [ + function onRequest1 (request, reply, done) { + done() + }, + function (request, reply, done) { + done() + } + ] + }, + async function helloworld () { + return 'hello world' + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preHandler, onReq2, onReq1, end, start] = spans + + t.plan(10) + t.assert.equal(spans.length, 5) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(onReq1.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'onRequest1', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onRequest', + 'http.route': '/' + }) + t.assert.deepStrictEqual(onReq2.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onRequest', + 'http.route': '/' + }) + t.assert.deepStrictEqual(preHandler.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'preHandler', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> preHandler', + 'http.route': '/' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + test('should create named span (404)', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -376,23 +459,163 @@ describe('FastifyInstrumentation', () => { t.assert.equal(preHandler.parentSpanId, start.spanContext().spanId) }) - /** - * Note: Spans does not seem yet to be connected through the parent - * Most likely as we are not using the span made on the onRequest hook - * and setting it as parent - * Find a way to link the root span made from the onRequst down to the childs - */ - test('should end spans upon error', async t => { + test('should create named span (404 - customized with hooks)', async t => { const app = Fastify() const plugin = instrumentation.plugin() await app.register(plugin) + app.setNotFoundHandler( + { + preHandler: function preHandler (request, reply, done) { + done() + }, + preValidation: function preValidation (request, reply, done) { + done() + } + }, + async function notFoundHandler (request, reply) { + reply.code(404).send('not found') + } + ) + app.get( '/', { - onError: function decorated (request, reply, error, done) { - done(error) + schema: { + headers: { + type: 'object', + properties: { + 'x-foo': { type: 'string' } + } + } + } + }, + async function helloworld () { + return 'hello world' + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/`, + { method: 'POST' } + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preHandler, preValidation, start, fof] = spans + + t.plan(9) + t.assert.equal(response.status, 404) + t.assert.equal(spans.length, 4) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'POST', + 'http.response.status_code': 404 + }) + t.assert.deepStrictEqual(preHandler.attributes, { + 'hook.name': + 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preHandler', + 'fastify.type': 'hook', + 'hook.callback.name': 'preHandler' + }) + t.assert.deepStrictEqual(preValidation.attributes, { + 'hook.name': + 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preValidation', + 'fastify.type': 'hook', + 'hook.callback.name': 'preValidation' + }) + t.assert.deepStrictEqual(fof.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous' + }) + t.assert.equal(fof.parentSpanId, start.spanContext().spanId) + t.assert.equal(preValidation.parentSpanId, start.spanContext().spanId) + t.assert.equal(preHandler.parentSpanId, start.spanContext().spanId) + }) + + test('should end spans upon error', { only: true }, async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + // onError: function decorated (request, reply, error, done) { + // console.log('error', error) + // done(error) + // }, + errorHandler: function errorHandler (error, request, reply) { + throw error + } + }, + async function helloworld () { + throw new Error('error') + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start] = spans + + t.plan(6) + t.assert.equal(spans.length, 2) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 500 + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 500) + t.assert.deepStrictEqual(await response.json(), { + statusCode: 500, + error: 'Internal Server Error', + message: 'error' + }) + }) + + test('should end spans upon error (with hook)', { only: true }, async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + // TODO: Test better; this should support an array of hooks + onError: function decorated (_request, _reply, _error, done) { + done() + }, + errorHandler: function errorHandler (error, request, reply) { + throw error } }, async function helloworld () { @@ -414,7 +637,7 @@ describe('FastifyInstrumentation', () => { const [end, start, error] = spans - t.plan(8) + t.plan(7) t.assert.equal(spans.length, 3) t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', @@ -423,9 +646,9 @@ describe('FastifyInstrumentation', () => { 'http.response.status_code': 500 }) t.assert.deepStrictEqual(error.attributes, { - 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', - 'fastify.type': 'route-hook', + 'fastify.type': 'hook', 'hook.callback.name': 'decorated', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onError', 'http.route': '/' }) t.assert.deepStrictEqual(end.attributes, { @@ -435,7 +658,6 @@ describe('FastifyInstrumentation', () => { 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) - t.assert.equal(error.parentSpanId, start.spanContext().spanId) t.assert.equal(response.status, 500) t.assert.deepStrictEqual(await response.json(), { statusCode: 500, From 9e410101626fbb803fe3e02967a6b3258dafce67 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Tue, 17 Dec 2024 11:13:16 +0100 Subject: [PATCH 10/20] fix: adjust implementation --- index.js | 66 +++++++------------------------------------------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/index.js b/index.js index 7aed22a..51bf0ad 100644 --- a/index.js +++ b/index.js @@ -27,7 +27,8 @@ const FASTIFY_HOOKS = [ 'preHandler', 'preSerialization', 'onSend', - 'onResponse' + 'onResponse', + 'onError' ] const ATTRIBUTE_NAMES = { HOOK_NAME: 'hook.name', @@ -126,15 +127,9 @@ class FastifyInstrumentation extends InstrumentationBase { // We always want to add the onError hook to the route to be executed last if (routeOptions.onError != null) { - routeOptions.onError = onErrorHookWrapper(routeOptions.onError, { - [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - onError`, - [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, - [ATTR_HTTP_ROUTE]: routeOptions.url, - [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - routeOptions.onError.name?.length > 0 - ? routeOptions.onError.name - : ANONYMOUS_FUNCTION_NAME - }) + routeOptions.onError = Array.isArray(routeOptions.onError) + ? [...routeOptions.onError, onErrorHook] + : [routeOptions.onError, onErrorHook] } else { routeOptions.onError = onErrorHook } @@ -237,53 +232,6 @@ class FastifyInstrumentation extends InstrumentationBase { hookDone() } - // TODO: replace and test out - function onErrorHookWrapper (hook, attr) { - const wrapped = handlerWrapper(hook, attr) - - return function onErrorHookWrapped (request, reply, error, hookDone) { - /** @type {Span} */ - const span = request[kRequestSpans] - - if (span == null) { - return wrapped.call(this, request, reply, error, hookDone) - } - - span.setStatus({ - code: SpanStatusCode.ERROR, - message: error.message - }) - span.recordException(error) - - try { - if (hook.length === 4) { - const wrappedDone = () => { - hookDone() - } - - wrapped.call(this, request, reply, error, wrappedDone) - return - } - - wrapped.call(this, request, reply, error).then( - () => { - span.end() - hookDone() - }, - () => { - span.end() - hookDone() - } - ) - } catch (err) { - span.recordException(err) - span.end() - } - - request[kRequestSpans] = null - } - } - function addHookPatched (name, hook) { if (FASTIFY_HOOKS.includes(name)) { addHookOriginal( @@ -292,7 +240,7 @@ class FastifyInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - ${name}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hook.name ?? ANONYMOUS_FUNCTION_NAME + hook.name?.length > 0 ? hook.name : ANONYMOUS_FUNCTION_NAME }) ) } else { @@ -386,6 +334,7 @@ class FastifyInstrumentation extends InstrumentationBase { } span.end() + return res } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, @@ -393,6 +342,7 @@ class FastifyInstrumentation extends InstrumentationBase { }) span.recordException(error) span.end() + throw error } }, this From c0ba7725ac7951c38bbecf42743765f542eb8d30 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Tue, 17 Dec 2024 11:13:36 +0100 Subject: [PATCH 11/20] test: extend for encapsulation --- test/index.test.js | 481 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 476 insertions(+), 5 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index ed59c22..cae30ae 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -212,7 +212,7 @@ describe('FastifyInstrumentation', () => { t.assert.equal(await response.text(), 'hello world') }) - test('should create span for different hooks', { only: true }, async t => { + test('should create span for different hooks', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -295,6 +295,129 @@ describe('FastifyInstrumentation', () => { t.assert.equal(await response.text(), 'hello world') }) + test('should create span for different hooks (patched)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + onSend: function onSend (request, reply, payload, done) { + done(null, payload) + } + }, + async function helloworld () { + return 'hello world' + } + ) + + app.addHook('preValidation', function (request, reply, done) { + done() + }) + + // Should not be patched + app.addHook('onReady', function (done) { + done() + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preValidation, end, start, onReq1] = spans + + t.plan(9) + t.assert.equal(spans.length, 4) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(onReq1.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'onSend', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onSend', + 'http.route': '/' + }) + t.assert.deepStrictEqual(preValidation.attributes, { + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preValidation' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should create span for different hooks (error scenario)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get('/', async function helloworld () { + return 'hello world' + }) + + app.addHook('preHandler', function (request, reply, done) { + throw new Error('error') + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preHandler, start] = spans + + t.plan(6) + t.assert.equal(spans.length, 2) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 500 + }) + t.assert.deepStrictEqual(preHandler.attributes, { + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preHandler' + }) + t.assert.equal(preHandler.status.code, SpanStatusCode.ERROR) + t.assert.equal(preHandler.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 500) + }) + test('should create named span (404)', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -542,7 +665,7 @@ describe('FastifyInstrumentation', () => { t.assert.equal(preHandler.parentSpanId, start.spanContext().spanId) }) - test('should end spans upon error', { only: true }, async t => { + test('should end spans upon error', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -601,7 +724,7 @@ describe('FastifyInstrumentation', () => { }) }) - test('should end spans upon error (with hook)', { only: true }, async t => { + test('should end spans upon error (with hook)', async t => { const app = Fastify() const plugin = instrumentation.plugin() @@ -646,9 +769,9 @@ describe('FastifyInstrumentation', () => { 'http.response.status_code': 500 }) t.assert.deepStrictEqual(error.attributes, { - 'fastify.type': 'hook', + 'fastify.type': 'route-hook', 'hook.callback.name': 'decorated', - 'hook.name': 'fastify -> @fastify/otel@0.0.0 - onError', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', 'http.route': '/' }) t.assert.deepStrictEqual(end.attributes, { @@ -665,5 +788,353 @@ describe('FastifyInstrumentation', () => { message: 'error' }) }) + + test('should end spans upon error (with hook [array])', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + app.get( + '/', + { + // TODO: Test better; this should support an array of hooks + onError: [ + function decorated (_request, _reply, _error, done) { + done() + }, + function decorated2 (_request, _reply, _error, done) { + done() + } + ], + errorHandler: function errorHandler (error, request, reply) { + throw error + } + }, + async function helloworld () { + throw new Error('error') + } + ) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start, error2, error] = spans + + t.plan(8) + t.assert.equal(spans.length, 4) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 500 + }) + t.assert.deepStrictEqual(error.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'decorated', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', + 'http.route': '/' + }) + t.assert.deepStrictEqual(error2.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'decorated2', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', + 'http.route': '/' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 500) + t.assert.deepStrictEqual(await response.json(), { + statusCode: 500, + error: 'Internal Server Error', + message: 'error' + }) + }) + }) + + describe('Encapulated Context', () => { + describe('Instrumentation#disabled', () => { + test('should not create spans if disabled', async t => { + before(() => { + contextManager.enable() + }) + + after(() => { + contextManager.disable() + spanProcessor.forceFlush() + memoryExporter.reset() + instrumentation.disable() + httpInstrumentation.disable() + }) + + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + await app.register(function plugin (instance, _opts, done) { + instance.get('/', async (request, reply) => 'hello world') + done() + }) + + instrumentation.disable() + + t.plan(3) + + const response = await app.inject({ + method: 'GET', + url: '/' + }) + + const spans = memoryExporter + .getFinishedSpans() + .find(span => span.instrumentationLibrary.name === '@fastify/otel') + + t.assert.ok(spans == null) + t.assert.equal(response.statusCode, 200) + t.assert.equal(response.body, 'hello world') + }) + }) + + describe('Instrumentation#enabled', () => { + beforeEach(() => { + instrumentation.enable() + httpInstrumentation.enable() + contextManager.enable() + }) + + afterEach(() => { + contextManager.disable() + instrumentation.disable() + httpInstrumentation.disable() + spanProcessor.forceFlush() + memoryExporter.reset() + }) + + test('should create anonymous span (simple case)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + await app.register(function plugin (instance, _opts, done) { + instance.get('/', async (request, reply) => 'hello world') + done() + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start] = spans + + t.plan(5) + t.assert.equal(spans.length, 2) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'plugin - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'anonymous' + }) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should create named span (simple case)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(async function nested (instance, _opts) { + await instance.register(plugin) + + instance.get('/', async function helloworld () { + return 'hello world' + }) + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [end, start] = spans + + t.plan(6) + t.assert.equal(spans.length, 2) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'nested -> @fastify/otel@0.0.0 - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should create span for different hooks (patched)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(plugin) + + await app.register(function nested (instance, _opts, done) { + instance.get( + '/', + { + onSend: function onSend (request, reply, payload, done) { + done(null, payload) + } + }, + async function helloworld () { + return 'hello world' + } + ) + + instance.addHook('preValidation', function (request, reply, done) { + done() + }) + + // Should not be patched + instance.addHook('onReady', function (done) { + done() + }) + + done() + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [preValidation, end, start, onReq1] = spans + + t.plan(9) + t.assert.equal(spans.length, 4) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 200 + }) + t.assert.deepStrictEqual(onReq1.attributes, { + 'fastify.type': 'route-hook', + 'hook.callback.name': 'onSend', + 'hook.name': 'nested - route -> onSend', + 'http.route': '/' + }) + t.assert.deepStrictEqual(preValidation.attributes, { + 'fastify.type': 'hook', + 'hook.callback.name': 'anonymous', + 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preValidation' + }) + t.assert.deepStrictEqual(end.attributes, { + 'hook.name': 'nested - route-handler', + 'fastify.type': 'request-handler', + 'http.route': '/', + 'hook.callback.name': 'helloworld' + }) + t.assert.equal(end.parentSpanId, start.spanContext().spanId) + t.assert.equal(response.status, 200) + t.assert.equal(await response.text(), 'hello world') + }) + + test('should respect context (error scenario)', async t => { + const app = Fastify() + const plugin = instrumentation.plugin() + + await app.register(async function nested (instance, _opts) { + await instance.register(plugin) + instance.get('/', async function helloworld () { + return 'hello world' + }) + }) + + // If registered under encapsulated context, hooks should be registered + // under the encapsulated context + app.addHook('preHandler', function (request, reply, done) { + throw new Error('error') + }) + + await app.listen() + + after(() => app.close()) + + const response = await fetch( + `http://localhost:${app.server.address().port}/` + ) + + const spans = memoryExporter + .getFinishedSpans() + .filter(span => span.instrumentationLibrary.name === '@fastify/otel') + + const [start] = spans + + t.plan(3) + t.assert.equal(spans.length, 1) + t.assert.deepStrictEqual(start.attributes, { + 'fastify.root': '@fastify/otel', + 'http.route': '/', + 'http.request.method': 'GET', + 'http.response.status_code': 500 + }) + t.assert.equal(response.status, 500) + }) + }) }) }) From 18ac98c1129e055ebd436f76c139205ce212baf6 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Tue, 17 Dec 2024 11:15:58 +0100 Subject: [PATCH 12/20] test: remove leftovers --- test/index.test.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index cae30ae..6f5ffba 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -18,14 +18,10 @@ const { ReadableSpan, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base') -const { Span, context, SpanStatusCode } = require('@opentelemetry/api') -const { - getPackageVersion, - runTestFixture, - TestCollector -} = require('@opentelemetry/contrib-test-utils') +const { context, SpanStatusCode } = require('@opentelemetry/api') + const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http') -const semver = require('semver') + const Fastify = require('fastify') const FastifyInstrumentation = require('..') @@ -674,10 +670,6 @@ describe('FastifyInstrumentation', () => { app.get( '/', { - // onError: function decorated (request, reply, error, done) { - // console.log('error', error) - // done(error) - // }, errorHandler: function errorHandler (error, request, reply) { throw error } @@ -733,7 +725,6 @@ describe('FastifyInstrumentation', () => { app.get( '/', { - // TODO: Test better; this should support an array of hooks onError: function decorated (_request, _reply, _error, done) { done() }, @@ -798,7 +789,6 @@ describe('FastifyInstrumentation', () => { app.get( '/', { - // TODO: Test better; this should support an array of hooks onError: [ function decorated (_request, _reply, _error, done) { done() From 57e5b50a6a22a46a631cf33f5981b0bc811ea351 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Thu, 19 Dec 2024 12:18:45 +0100 Subject: [PATCH 13/20] refactor:naming --- index.js | 26 +++++++++++++------------- package.json | 1 - test/index.test.js | 10 ++++------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 51bf0ad..1eba0f6 100644 --- a/index.js +++ b/index.js @@ -45,12 +45,12 @@ const ANONYMOUS_FUNCTION_NAME = 'anonymous' // Symbols const kInstrumentation = Symbol('fastify instrumentation instance') -const kRequestSpans = Symbol('fastify instrumentation request spans') +const kRequestSpan = Symbol('fastify instrumentation request spans') const kRequestContext = Symbol('fastify instrumentation request context') -class FastifyInstrumentation extends InstrumentationBase { - static FastifyInstrumentation = FastifyInstrumentation - static default = FastifyInstrumentation +class FastifyOtelInstrumentation extends InstrumentationBase { + static FastifyOtelInstrumentation = FastifyOtelInstrumentation + static default = FastifyOtelInstrumentation constructor (config) { super(PACKAGE_NAME, PACKAGE_VERSION, config) @@ -76,7 +76,7 @@ class FastifyInstrumentation extends InstrumentationBase { instance.setNotFoundHandler.bind(instance) instance.decorate(kInstrumentation, instrumentation) - instance.decorateRequest(kRequestSpans, null) + instance.decorateRequest(kRequestSpan, null) instance.decorateRequest(kRequestContext, null) instance.addHook('onRoute', function (routeOptions) { @@ -166,7 +166,7 @@ class FastifyInstrumentation extends InstrumentationBase { }) request[kRequestContext] = trace.setSpan(context.active(), span) - request[kRequestSpans] = span + request[kRequestSpan] = span } hookDone() @@ -174,7 +174,7 @@ class FastifyInstrumentation extends InstrumentationBase { // onResponse is the last hook to be executed, only added for 404 handlers instance.addHook('onResponse', function (request, reply, hookDone) { - const span = request[kRequestSpans] + const span = request[kRequestSpan] if (span != null) { span.setStatus({ @@ -187,7 +187,7 @@ class FastifyInstrumentation extends InstrumentationBase { span.end() } - request[kRequestSpans] = null + request[kRequestSpan] = null hookDone() }) @@ -199,7 +199,7 @@ class FastifyInstrumentation extends InstrumentationBase { function onSendHook (request, reply, payload, hookDone) { /** @type {import('@opentelemetry/api').Span} */ - const span = request[kRequestSpans] + const span = request[kRequestSpan] if (span != null) { span.setStatus({ @@ -212,14 +212,14 @@ class FastifyInstrumentation extends InstrumentationBase { span.end() } - request[kRequestSpans] = null + request[kRequestSpan] = null hookDone(null, payload) } function onErrorHook (request, reply, error, hookDone) { /** @type {Span} */ - const span = request[kRequestSpans] + const span = request[kRequestSpan] if (span != null) { span.setStatus({ @@ -288,7 +288,7 @@ class FastifyInstrumentation extends InstrumentationBase { function handlerWrapper (handler, spanAttributes = {}) { return function handlerWrapped (...args) { - /** @type {FastifyInstrumentation} */ + /** @type {FastifyOtelInstrumentation} */ const instrumentation = this[kInstrumentation] const [request] = args @@ -353,4 +353,4 @@ class FastifyInstrumentation extends InstrumentationBase { } } -module.exports = FastifyInstrumentation +module.exports = FastifyOtelInstrumentation diff --git a/package.json b/package.json index d3518d5..d0cee48 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "eslint": "^9.16.0", "fastify": "^5.1.0", "neostandard": "^0.11.9", - "proxyquire": "^2.1.3", "tsd": "^0.31.0" }, "dependencies": { diff --git a/test/index.test.js b/test/index.test.js index 6f5ffba..233dadf 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -6,7 +6,6 @@ const { afterEach, beforeEach } = require('node:test') -// const http = require('node:http') const { InstrumentationBase } = require('@opentelemetry/instrumentation') const { @@ -15,7 +14,6 @@ const { const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') const { InMemorySpanExporter, - ReadableSpan, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base') const { context, SpanStatusCode } = require('@opentelemetry/api') @@ -28,14 +26,14 @@ const FastifyInstrumentation = require('..') describe('Interface', () => { test('should exports support', t => { - t.assert.equal(FastifyInstrumentation.name, 'FastifyInstrumentation') + t.assert.equal(FastifyInstrumentation.name, 'FastifyOtelInstrumentation') t.assert.equal( FastifyInstrumentation.default.name, - 'FastifyInstrumentation' + 'FastifyOtelInstrumentation' ) t.assert.equal( - FastifyInstrumentation.FastifyInstrumentation.name, - 'FastifyInstrumentation' + FastifyInstrumentation.FastifyOtelInstrumentation.name, + 'FastifyOtelInstrumentation' ) t.assert.strictEqual( Object.getPrototypeOf(FastifyInstrumentation), From 9e352a703207362cd6b37505ae0999959e226896 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Thu, 19 Dec 2024 12:34:21 +0100 Subject: [PATCH 14/20] feat(types): add typescript --- index.d.ts | 17 +++++++++++++++++ package.json | 5 ++++- test/index.test-d.ts | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 index.d.ts create mode 100644 test/index.test-d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..1ba0f4f --- /dev/null +++ b/index.d.ts @@ -0,0 +1,17 @@ +/// + +import { InstrumentationBase, InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' +import { FastifyInstance } from 'fastify' + +export interface FastifyOtelOptions {} +export interface FastifyOtelInstrumentationOpts extends InstrumentationConfig {} + +declare class FastifyOtelInstrumentation extends InstrumentationBase { + static FastifyInstrumentation: FastifyOtelInstrumentation + constructor (config?: FastifyOtelInstrumentationOpts) + init (): InstrumentationNodeModuleDefinition[] + plugin (instance: FastifyInstance, opts: FastifyOtelOptions, done: (err?: Error) => void): void +} + +export default FastifyOtelInstrumentation +export { FastifyOtelInstrumentation } diff --git a/package.json b/package.json index d0cee48..d814c9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Official Fastify OpenTelemetry Instrumentation", "main": "index.js", "type": "commonjs", - "types": "types/index.d.ts", + "types": "index.d.ts", "scripts": { "lint": "eslint", "lint:fix": "eslint --fix", @@ -54,5 +54,8 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" + }, + "tsd": { + "directory": "test" } } diff --git a/test/index.test-d.ts b/test/index.test-d.ts new file mode 100644 index 0000000..19dacf2 --- /dev/null +++ b/test/index.test-d.ts @@ -0,0 +1,15 @@ +import { expectAssignable } from 'tsd' +import { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation' +import { fastify as Fastify } from 'fastify' + +import { FastifyOtelInstrumentation, FastifyOtelInstrumentationOpts } from '..' + +expectAssignable(new FastifyOtelInstrumentation()) +expectAssignable({} as FastifyOtelInstrumentationOpts) + +const app = Fastify() +app.register(new FastifyOtelInstrumentation().plugin) +app.register((nested, opts, done) => { + nested.register(new FastifyOtelInstrumentation().plugin) + done() +}) From 17ba9ba1d823edd8a7145ba2ee558b8419b59ac4 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Thu, 19 Dec 2024 12:41:26 +0100 Subject: [PATCH 15/20] test: remove redundant checks --- index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 1eba0f6..3984b17 100644 --- a/index.js +++ b/index.js @@ -92,7 +92,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: handlerLike.name?.length > 0 ? handlerLike.name - : ANONYMOUS_FUNCTION_NAME + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) } else if (Array.isArray(handlerLike)) { const wrappedHandlers = [] @@ -240,7 +240,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - ${name}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hook.name?.length > 0 ? hook.name : ANONYMOUS_FUNCTION_NAME + hook.name?.length > 0 ? hook.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) ) } else { @@ -254,7 +254,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.name ?? ANONYMOUS_FUNCTION_NAME + hooks.name?.length > 0 ? hooks.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) setNotFoundHandlerOriginal(handler) } else { @@ -263,7 +263,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preValidation`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.preValidation.name ?? ANONYMOUS_FUNCTION_NAME + hooks.preValidation.name?.length > 0 ? hooks.preValidation.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) } @@ -272,7 +272,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preHandler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.preHandler.name ?? ANONYMOUS_FUNCTION_NAME + hooks.preHandler.name?.length > 0 ? hooks.preHandler.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) } @@ -280,7 +280,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.name ?? ANONYMOUS_FUNCTION_NAME + hooks.name?.length > 0 ? hooks.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) setNotFoundHandlerOriginal(hooks, handler) } @@ -301,7 +301,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { `handler - ${ handler.name?.length > 0 ? handler.name - : this.pluginName ?? ANONYMOUS_FUNCTION_NAME + : this.pluginName ?? ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }`, { attributes: spanAttributes From a591e467682d31d08481b75d16b0a2445ca08997 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 20 Dec 2024 10:59:58 +0100 Subject: [PATCH 16/20] docs: add README --- README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79d8e21..a029019 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,80 @@ -# otel -OpenTelemetry instrumentation library. +# @fastify/otel + +[![CI](https://github.com/fastify/otel/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/otel/actions/workflows/ci.yml) + + + +[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) + +`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify). + +OpenTelemetry auto-instrumentation library. + +## Install + +```sh +npm i @fastify/otel +``` + +## Usage + +`@fastify/otel` works as a metric creator as well as instrumentation for you fastify application. + +Its usage requires to be set before you start defining your routes and other plugins in order to cover up the most of your fatify server. + +- It automatically wraps the main request handler +- Instruments all route hooks (defined at instance and route definition level) + - `onRequest` + - `preParsing` + - `preValidation` + - `preHandler` + - `preSerialization` + - `onSend` + - `onResponse` + - `onError` +- Instruments automatically custom 404 Not Found handler + +Example: + +```js +// ... in your OTEL setup +const FastifyInstrumentation = require('@fastify/otel'); + +const fastifyInstrumentation = new FastifyInstrumentation(); +fastifyInstrumentation.setTraceProvider(provider) + +module.exports = { fastifyInstrumentation } + +// ... in your Fastify definition +const { fastifyInstrumentation } = require('./otel.js'); +const Fastify = require('fastify'); + +const app = fastify(); +// It is necessary to await for its register as it requires to be able +// to intercept all route definitions +await app.register(fastifyInstrumentation.plugin()); + +// automatically all your routes will be instrumented +app.get('/', () => 'hello world') +// as well as your instance level hooks. +app.addHook('onError', () => /* do something */) + +// you can also scope your instrumentation to only be enabled on a sub context +// of your application +app.register((instance, opts, done) => { + instance.register(fastifyInstrumentation.plugin()); + // If only enabled in your encapsulated context + // the parent context won't be instrumented + app.get('/', () => 'hello world') + +}, { prefix: '/nested' }) +``` + +> **Notes**: +> +> - This instrumentation requires `@opentelemetry/http-instrumentation` to be able to propagate the traces all the way back to upstream +> - The HTTP instrumentation might cover all your routes although `@fastify/otel` just covers a subset of your application + +## License + +Licensed under [MIT](./LICENSE). From 66f7bacf62879a852ba1ba856f3e7355e26e244e Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 20 Dec 2024 11:00:12 +0100 Subject: [PATCH 17/20] types; adjust typing --- index.d.ts | 2 +- test/index.test-d.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1ba0f4f..2b1c9e8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,7 +10,7 @@ declare class FastifyOtelInstrumentation void): void + plugin (): (instance: FastifyInstance, opts: FastifyOtelOptions, done: (err?: Error) => void) => void } export default FastifyOtelInstrumentation diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 19dacf2..3baccd5 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -8,8 +8,8 @@ expectAssignable(new FastifyOtelInstrumentation()) expectAssignable({} as FastifyOtelInstrumentationOpts) const app = Fastify() -app.register(new FastifyOtelInstrumentation().plugin) -app.register((nested, opts, done) => { - nested.register(new FastifyOtelInstrumentation().plugin) +app.register(new FastifyOtelInstrumentation().plugin()) +app.register((nested, _opts, done) => { + nested.register(new FastifyOtelInstrumentation().plugin()) done() }) From 4841fd4a841d910678cb2a702351a12788176215 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 22 Dec 2024 11:01:56 +0100 Subject: [PATCH 18/20] docs: adjust --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a029019..b049bc0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,5 @@ # @fastify/otel -[![CI](https://github.com/fastify/otel/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/otel/actions/workflows/ci.yml) - - - -[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) - -`fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify). - OpenTelemetry auto-instrumentation library. ## Install @@ -18,9 +10,9 @@ npm i @fastify/otel ## Usage -`@fastify/otel` works as a metric creator as well as instrumentation for you fastify application. +`@fastify/otel` works as a metric creator as well as application performance monitor for your Fastify application. -Its usage requires to be set before you start defining your routes and other plugins in order to cover up the most of your fatify server. +It must be configured before defining routes and other plugins in order to cover the most of your Fastify server. - It automatically wraps the main request handler - Instruments all route hooks (defined at instance and route definition level) @@ -75,6 +67,8 @@ app.register((instance, opts, done) => { > - This instrumentation requires `@opentelemetry/http-instrumentation` to be able to propagate the traces all the way back to upstream > - The HTTP instrumentation might cover all your routes although `@fastify/otel` just covers a subset of your application +For more information about OpenTelemetry, please refer to the [OpenTelemetry JavaScript](https://opentelemetry.io/docs/languages/js/) documentation. + ## License Licensed under [MIT](./LICENSE). From 3176e833e3df0ced23e8a0292f72a2051e65c553 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 3 Jan 2025 11:44:07 +0100 Subject: [PATCH 19/20] refactor: small refactor --- index.d.ts | 4 +++- index.js | 53 ++++++++++++++++++++++++++++++------------ test/index.test.js | 57 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/index.d.ts b/index.d.ts index 2b1c9e8..72f56bb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,7 +4,9 @@ import { InstrumentationBase, InstrumentationConfig, InstrumentationNodeModuleDe import { FastifyInstance } from 'fastify' export interface FastifyOtelOptions {} -export interface FastifyOtelInstrumentationOpts extends InstrumentationConfig {} +export interface FastifyOtelInstrumentationOpts extends InstrumentationConfig { + servername?: string +} declare class FastifyOtelInstrumentation extends InstrumentationBase { static FastifyInstrumentation: FastifyOtelInstrumentation diff --git a/index.js b/index.js index 3984b17..9f7cb65 100644 --- a/index.js +++ b/index.js @@ -4,12 +4,10 @@ const { getRPCMetadata, RPCType } = require('@opentelemetry/core') const { ATTR_HTTP_ROUTE, ATTR_HTTP_RESPONSE_STATUS_CODE, - ATTR_HTTP_REQUEST_METHOD + ATTR_HTTP_REQUEST_METHOD, + ATTR_SERVICE_NAME } = require('@opentelemetry/semantic-conventions') -const { - InstrumentationBase, - InstrumentationNodeModuleDefinition -} = require('@opentelemetry/instrumentation') +const { InstrumentationBase } = require('@opentelemetry/instrumentation') const fp = require('fastify-plugin') @@ -51,15 +49,16 @@ const kRequestContext = Symbol('fastify instrumentation request context') class FastifyOtelInstrumentation extends InstrumentationBase { static FastifyOtelInstrumentation = FastifyOtelInstrumentation static default = FastifyOtelInstrumentation + servername = '' constructor (config) { super(PACKAGE_NAME, PACKAGE_VERSION, config) + this.servername = config?.servername ?? 'fastify' } + // We do not do patching in this instrumentation init () { - return [ - new InstrumentationNodeModuleDefinition('fastify', [SUPPORTED_VERSIONS]) - ] + return [] } plugin () { @@ -86,6 +85,8 @@ class FastifyOtelInstrumentation extends InstrumentationBase { if (typeof handlerLike === 'function') { routeOptions[hook] = handlerWrapper(handlerLike, { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route -> ${hook}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, [ATTR_HTTP_ROUTE]: routeOptions.url, @@ -100,6 +101,8 @@ class FastifyOtelInstrumentation extends InstrumentationBase { for (const handler of handlerLike) { wrappedHandlers.push( handlerWrapper(handler, { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route -> ${hook}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.ROUTE, [ATTR_HTTP_ROUTE]: routeOptions.url, @@ -135,6 +138,7 @@ class FastifyOtelInstrumentation extends InstrumentationBase { } routeOptions.handler = handlerWrapper(routeOptions.handler, { + [ATTR_SERVICE_NAME]: instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - route-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.HANDLER, [ATTR_HTTP_ROUTE]: routeOptions.url, @@ -159,6 +163,8 @@ class FastifyOtelInstrumentation extends InstrumentationBase { /** @type {Span} */ const span = this[kInstrumentation].tracer.startSpan('request', { attributes: { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.ROOT]: '@fastify/otel', [ATTR_HTTP_ROUTE]: request.url, [ATTR_HTTP_REQUEST_METHOD]: request.method @@ -237,10 +243,14 @@ class FastifyOtelInstrumentation extends InstrumentationBase { addHookOriginal( name, handlerWrapper(hook, { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - ${name}`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hook.name?.length > 0 ? hook.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + hook.name?.length > 0 + ? hook.name + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) ) } else { @@ -251,36 +261,50 @@ class FastifyOtelInstrumentation extends InstrumentationBase { function setNotFoundHandlerPatched (hooks, handler) { if (typeof hooks === 'function') { handler = handlerWrapper(hooks, { + [ATTR_SERVICE_NAME]: instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.name?.length > 0 ? hooks.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + hooks.name?.length > 0 + ? hooks.name + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) setNotFoundHandlerOriginal(handler) } else { if (hooks.preValidation != null) { hooks.preValidation = handlerWrapper(hooks.preValidation, { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preValidation`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.preValidation.name?.length > 0 ? hooks.preValidation.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + hooks.preValidation.name?.length > 0 + ? hooks.preValidation.name + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) } if (hooks.preHandler != null) { hooks.preHandler = handlerWrapper(hooks.preHandler, { + [ATTR_SERVICE_NAME]: + instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler - preHandler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.preHandler.name?.length > 0 ? hooks.preHandler.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + hooks.preHandler.name?.length > 0 + ? hooks.preHandler.name + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) } handler = handlerWrapper(handler, { + [ATTR_SERVICE_NAME]: instance[kInstrumentation].servername, [ATTRIBUTE_NAMES.HOOK_NAME]: `${this.pluginName} - not-found-handler`, [ATTRIBUTE_NAMES.FASTIFY_TYPE]: HOOK_TYPES.INSTANCE, [ATTRIBUTE_NAMES.HOOK_CALLBACK_NAME]: - hooks.name?.length > 0 ? hooks.name : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + hooks.name?.length > 0 + ? hooks.name + : ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }) setNotFoundHandlerOriginal(hooks, handler) } @@ -301,7 +325,8 @@ class FastifyOtelInstrumentation extends InstrumentationBase { `handler - ${ handler.name?.length > 0 ? handler.name - : this.pluginName ?? ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ + : this.pluginName ?? + ANONYMOUS_FUNCTION_NAME /* c8 ignore next */ }`, { attributes: spanAttributes diff --git a/test/index.test.js b/test/index.test.js index 233dadf..e9b7a5f 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -150,6 +150,7 @@ describe('FastifyInstrumentation', () => { t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', + 'service.name': 'fastify', 'http.request.method': 'GET', 'http.response.status_code': 200 }) @@ -157,6 +158,7 @@ describe('FastifyInstrumentation', () => { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'anonymous' }) t.assert.equal(response.status, 200) @@ -192,6 +194,7 @@ describe('FastifyInstrumentation', () => { t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', + 'service.name': 'fastify', 'http.request.method': 'GET', 'http.response.status_code': 200 }) @@ -199,6 +202,7 @@ describe('FastifyInstrumentation', () => { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -251,12 +255,14 @@ describe('FastifyInstrumentation', () => { t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', + 'service.name': 'fastify', 'http.request.method': 'GET', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', + 'service.name': 'fastify', 'http.request.method': 'GET', 'http.response.status_code': 200 }) @@ -264,25 +270,29 @@ describe('FastifyInstrumentation', () => { 'fastify.type': 'route-hook', 'hook.callback.name': 'onRequest1', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onRequest', - 'http.route': '/' + 'http.route': '/', + 'service.name': 'fastify', }) t.assert.deepStrictEqual(onReq2.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'anonymous', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onRequest', - 'http.route': '/' + 'http.route': '/', + 'service.name': 'fastify', }) t.assert.deepStrictEqual(preHandler.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'preHandler', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> preHandler', - 'http.route': '/' + 'http.route': '/', + 'service.name': 'fastify', }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', - 'hook.callback.name': 'helloworld' + 'hook.callback.name': 'helloworld', + 'service.name': 'fastify', }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) t.assert.equal(response.status, 200) @@ -336,29 +346,34 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(onReq1.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'onSend', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onSend', + 'service.name': 'fastify', 'http.route': '/' }) t.assert.deepStrictEqual(preValidation.attributes, { 'fastify.type': 'hook', 'hook.callback.name': 'anonymous', + 'service.name': 'fastify', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preValidation' }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -400,11 +415,13 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 500 }) t.assert.deepStrictEqual(preHandler.attributes, { 'fastify.type': 'hook', 'hook.callback.name': 'anonymous', + 'service.name': 'fastify', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preHandler' }) t.assert.equal(preHandler.status.code, SpanStatusCode.ERROR) @@ -444,6 +461,7 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'POST', + 'service.name': 'fastify', 'http.response.status_code': 404 }) }) @@ -484,11 +502,13 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'POST', + 'service.name': 'fastify', 'http.response.status_code': 404 }) t.assert.deepStrictEqual(fof.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'notFoundHandler' }) }) @@ -552,23 +572,27 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'POST', + 'service.name': 'fastify', 'http.response.status_code': 404 }) t.assert.deepStrictEqual(preHandler.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preHandler', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'preHandler' }) t.assert.deepStrictEqual(preValidation.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preValidation', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'preValidation' }) t.assert.deepStrictEqual(fof.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'anonymous' }) t.assert.equal(fof.parentSpanId, start.spanContext().spanId) @@ -635,23 +659,27 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'POST', + 'service.name': 'fastify', 'http.response.status_code': 404 }) t.assert.deepStrictEqual(preHandler.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preHandler', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'preHandler' }) t.assert.deepStrictEqual(preValidation.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler - preValidation', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'preValidation' }) t.assert.deepStrictEqual(fof.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - not-found-handler', 'fastify.type': 'hook', + 'service.name': 'fastify', 'hook.callback.name': 'anonymous' }) t.assert.equal(fof.parentSpanId, start.spanContext().spanId) @@ -697,12 +725,14 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 500 }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -755,18 +785,21 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 500 }) t.assert.deepStrictEqual(error.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'decorated', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', - 'http.route': '/' + 'http.route': '/', + 'service.name': 'fastify', }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -824,24 +857,28 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 500 }) t.assert.deepStrictEqual(error.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'decorated', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', + 'service.name': 'fastify', 'http.route': '/' }) t.assert.deepStrictEqual(error2.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'decorated2', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route -> onError', + 'service.name': 'fastify', 'http.route': '/' }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'fastify -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -944,11 +981,13 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'plugin - route-handler', 'fastify.type': 'request-handler', + 'service.name': 'fastify', 'http.route': '/', 'hook.callback.name': 'anonymous' }) @@ -988,12 +1027,14 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'nested -> @fastify/otel@0.0.0 - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -1052,29 +1093,34 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(start.attributes, { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 200 }) t.assert.deepStrictEqual(onReq1.attributes, { 'fastify.type': 'route-hook', 'hook.callback.name': 'onSend', 'hook.name': 'nested - route -> onSend', + 'service.name': 'fastify', 'http.route': '/' }) t.assert.deepStrictEqual(preValidation.attributes, { 'fastify.type': 'hook', 'hook.callback.name': 'anonymous', + 'service.name': 'fastify', 'hook.name': 'fastify -> @fastify/otel@0.0.0 - preValidation' }) t.assert.deepStrictEqual(end.attributes, { 'hook.name': 'nested - route-handler', 'fastify.type': 'request-handler', 'http.route': '/', + 'service.name': 'fastify', 'hook.callback.name': 'helloworld' }) t.assert.equal(end.parentSpanId, start.spanContext().spanId) @@ -1119,6 +1165,7 @@ describe('FastifyInstrumentation', () => { 'fastify.root': '@fastify/otel', 'http.route': '/', 'http.request.method': 'GET', + 'service.name': 'fastify', 'http.response.status_code': 500 }) t.assert.equal(response.status, 500) From 97d974613541ffac68b4c88d12ec3e164e565fce Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 3 Jan 2025 12:44:33 +0100 Subject: [PATCH 20/20] test: add ts --- test/index.test-d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/index.test-d.ts b/test/index.test-d.ts index 3baccd5..0414bdd 100644 --- a/test/index.test-d.ts +++ b/test/index.test-d.ts @@ -5,6 +5,7 @@ import { fastify as Fastify } from 'fastify' import { FastifyOtelInstrumentation, FastifyOtelInstrumentationOpts } from '..' expectAssignable(new FastifyOtelInstrumentation()) +expectAssignable({ servername: 'server', enabled: true } as FastifyOtelInstrumentationOpts) expectAssignable({} as FastifyOtelInstrumentationOpts) const app = Fastify()