@fastify/otel
Advanced tools
+12
-0
@@ -5,2 +5,14 @@ # Changelog | ||
| ## [0.18.0](https://github.com/fastify/otel/compare/v0.17.1...v0.18.0) (2026-03-26) | ||
| ### Features | ||
| * remove setStatus(OK) calls to comply with OTel spec ([#135](https://github.com/fastify/otel/issues/135)) ([9ff035c](https://github.com/fastify/otel/commit/9ff035c60aa5ec72bee8cbad0837dbbc80ec4e2f)) | ||
| ### Bug Fixes | ||
| * support fastify/websocket handlers (fastify/otel[#136](https://github.com/fastify/otel/issues/136)) ([#137](https://github.com/fastify/otel/issues/137)) ([cc9c6b5](https://github.com/fastify/otel/commit/cc9c6b5ad8ae5c88da03e6537f8b494d3d506b8d)) | ||
| ### [0.17.1](https://github.com/fastify/otel/compare/v0.17.0...v0.17.1) (2026-03-03) | ||
@@ -7,0 +19,0 @@ |
+21
-11
@@ -323,8 +323,4 @@ 'use strict' | ||
| if (span != null) { | ||
| span.setStatus({ | ||
| code: SpanStatusCode.OK, | ||
| message: 'OK' | ||
| }) | ||
| span.setAttributes({ | ||
| [ATTR_HTTP_RESPONSE_STATUS_CODE]: 404 | ||
| [ATTR_HTTP_RESPONSE_STATUS_CODE]: reply.statusCode | ||
| }) | ||
@@ -349,7 +345,4 @@ span.end() | ||
| if (span != null) { | ||
| if (reply.statusCode < 500) { | ||
| span.setStatus({ | ||
| code: SpanStatusCode.OK, | ||
| message: 'OK' | ||
| }) | ||
| if (reply.statusCode >= 500) { | ||
| span.setStatus({ code: SpanStatusCode.ERROR }) | ||
| } | ||
@@ -453,2 +446,12 @@ | ||
| // regular handlers are (request, reply), @fastify/websocket handlers are (socket, request) | ||
| function getRequestFromArgs (args) { | ||
| for (const arg of args) { | ||
| if (arg?.routeOptions && arg.url && arg.method) { | ||
| return arg | ||
| } | ||
| } | ||
| return null | ||
| } | ||
| function handlerWrapper (handler, hookName, spanAttributes = {}) { | ||
@@ -458,4 +461,11 @@ return function handlerWrapped (...args) { | ||
| const instrumentation = this[kInstrumentation] | ||
| const [request] = args | ||
| const request = getRequestFromArgs(args) | ||
| if (request === null) { | ||
| instrumentation.logger.debug( | ||
| `Ignoring route instrumentation because ${hookName} was called without a Fastify request argument` | ||
| ) | ||
| return handler.call(this, ...args) | ||
| } | ||
| if (instrumentation.isEnabled() === false || request.routeOptions.config?.otel === false) { | ||
@@ -462,0 +472,0 @@ instrumentation.logger.debug( |
+1
-1
| { | ||
| "name": "@fastify/otel", | ||
| "version": "0.17.1", | ||
| "version": "0.18.0", | ||
| "description": "Official Fastify OpenTelemetry Instrumentation", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
+5
-0
@@ -74,2 +74,7 @@ # @fastify/otel | ||
| If the main request handler is not of the form `(request, reply)` (for example, `@fastify/websocket` | ||
| handlers take the form `(socket, request)`), the plugin attempts to find the Fastify request object | ||
| in any of the handler's arguments. If no request object can be found, instrumentation is skipped and | ||
| a debug level message is logged. | ||
| ### Registration using OpenTelemetry Node SDK | ||
@@ -76,0 +81,0 @@ |
+102
-0
@@ -339,2 +339,4 @@ 'use strict' | ||
| assert.equal(start.status.code, SpanStatusCode.UNSET) | ||
| assert.equal(end.name, 'handler - fastify -> @fastify/otel') | ||
@@ -761,2 +763,3 @@ assert.deepStrictEqual(end.attributes, { | ||
| assert.equal(preHandler.status.code, SpanStatusCode.ERROR) | ||
| assert.equal(start.status.code, SpanStatusCode.ERROR) | ||
| assert.equal(preHandler.parentSpanContext.spanId, start.spanContext().spanId) | ||
@@ -799,2 +802,3 @@ assert.equal(response.status, 500) | ||
| }) | ||
| assert.equal(start.status.code, SpanStatusCode.UNSET) | ||
| }) | ||
@@ -1061,2 +1065,99 @@ | ||
| test('should create span when wrapped handler is invoked as (socket, request)', async t => { | ||
| const app = Fastify() | ||
| const plugin = instrumentation.plugin() | ||
| await app.register(plugin) | ||
| app.addHook('onRoute', (routeOptions) => { | ||
| const { handler } = routeOptions | ||
| routeOptions.handler = function websocketLikeHandler (request, _reply) { | ||
| const socket = { readyState: 1 } | ||
| return handler.call(this, socket, request) | ||
| } | ||
| }) | ||
| app.get('/', async function wsHandler (_socket, request) { | ||
| assert.equal(request.method, 'GET') | ||
| 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.instrumentationScope.name === '@fastify/otel') | ||
| const [end, start] = spans | ||
| assert.equal(spans.length, 2) | ||
| assert.deepStrictEqual(start.attributes, { | ||
| 'fastify.root': '@fastify/otel', | ||
| 'http.route': '/', | ||
| 'url.path': '/', | ||
| 'http.request.method': 'GET', | ||
| 'http.response.status_code': 200 | ||
| }) | ||
| assert.deepStrictEqual(end.attributes, { | ||
| 'hook.name': 'fastify -> @fastify/otel - route-handler', | ||
| 'fastify.type': 'request-handler', | ||
| 'http.route': '/', | ||
| 'hook.callback.name': 'wsHandler' | ||
| }) | ||
| assert.equal(end.parentSpanContext.spanId, start.spanContext().spanId) | ||
| assert.equal(response.status, 200) | ||
| assert.equal(await response.text(), 'hello world') | ||
| }) | ||
| test('should fallback when wrapped handler is invoked without request argument', async t => { | ||
| const app = Fastify() | ||
| const plugin = instrumentation.plugin() | ||
| await app.register(plugin) | ||
| app.addHook('onRoute', (routeOptions) => { | ||
| const { handler } = routeOptions | ||
| routeOptions.handler = function callWithoutRequest (_request, _reply) { | ||
| return handler.call(this) | ||
| } | ||
| }) | ||
| app.get('/', async function wsHandlerWithoutRequest () { | ||
| 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.instrumentationScope.name === '@fastify/otel') | ||
| const [start] = spans | ||
| assert.equal(spans.length, 1) | ||
| assert.deepStrictEqual(start.attributes, { | ||
| 'fastify.root': '@fastify/otel', | ||
| 'http.route': '/', | ||
| 'url.path': '/', | ||
| 'http.request.method': 'GET', | ||
| 'http.response.status_code': 200 | ||
| }) | ||
| assert.equal(response.status, 200) | ||
| assert.equal(await response.text(), 'hello world') | ||
| }) | ||
| test('should end spans upon error', async t => { | ||
@@ -1108,2 +1209,3 @@ const app = Fastify() | ||
| }) | ||
| assert.equal(start.status.code, SpanStatusCode.ERROR) | ||
| assert.equal(end.parentSpanContext.spanId, start.spanContext().spanId) | ||
@@ -1110,0 +1212,0 @@ assert.equal(response.status, 500) |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
118418
3.94%2620
3.43%258
1.98%32
6.67%