fix(otel): Use `req.routePath` when tagging spans (#1113)

* use routePath

Replace indexing into the matchedRoutes with a direct reference to routePath

* Update package.json

Bump version

* set span name and attributes after the request is handled

* revert version bump

* changeset

* add test to ensure subapps set the correct span name

---------

Co-authored-by: Milo Hansen <milo.hansen@avanade.com>
pull/1118/head
Milo Hansen 2025-04-10 01:40:54 -07:00 committed by GitHub
parent bebdfa2a88
commit 362b6701a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 41 additions and 14 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/otel': patch
---
Use `req.routePath` to augment spans with the path that handled the request.

View File

@ -26,6 +26,13 @@ describe('OpenTelemetry middleware', () => {
throw new Error('error message') throw new Error('error message')
}) })
const subapp = new Hono()
subapp.get('/hello', (c) => c.text('Hello from subapp!'))
subapp.get('*', (c) => c.text('Fallthrough'))
// mount subapp
app.route('/subapp', subapp)
it('Should make a span', async () => { it('Should make a span', async () => {
memoryExporter.reset() memoryExporter.reset()
const response = await app.request('http://localhost/foo') const response = await app.request('http://localhost/foo')
@ -71,4 +78,13 @@ describe('OpenTelemetry middleware', () => {
expect(span.name).toBe('GET /foo') expect(span.name).toBe('GET /foo')
expect(span.attributes[ATTR_HTTP_ROUTE]).toBe('/foo') expect(span.attributes[ATTR_HTTP_ROUTE]).toBe('/foo')
}) })
// Issue #1112
it('Should set the correct span name for subapp', async () => {
memoryExporter.reset()
await app.request('http://localhost/subapp/hello')
const spans = memoryExporter.getFinishedSpans()
const [span] = spans
expect(span.name).toBe('GET /subapp/hello')
})
}) })

View File

@ -1,5 +1,5 @@
import type { TracerProvider } from '@opentelemetry/api' import type { TracerProvider } from '@opentelemetry/api'
import { SpanKind, SpanStatusCode, trace } from '@opentelemetry/api' import { SpanKind, SpanStatusCode, trace } from '@opentelemetry/api'
import { import {
ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_HEADER,
ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD,
@ -10,17 +10,19 @@ import {
} from '@opentelemetry/semantic-conventions' } from '@opentelemetry/semantic-conventions'
import type { Env, Input } from 'hono' import type { Env, Input } from 'hono'
import { createMiddleware } from 'hono/factory' import { createMiddleware } from 'hono/factory'
import metadata from '../package.json' with { type: 'json'} import metadata from '../package.json' with { type: 'json' }
const PACKAGE_NAME = metadata.name const PACKAGE_NAME = metadata.name
const PACKAGE_VERSION = metadata.version const PACKAGE_VERSION = metadata.version
export type OtelOptions = { export type OtelOptions =
augmentSpan?: false; | {
tracerProvider?: TracerProvider augmentSpan?: false
} | { tracerProvider?: TracerProvider
augmentSpan: true; }
} | {
augmentSpan: true
}
export const otel = <E extends Env = any, P extends string = any, I extends Input = {}>( export const otel = <E extends Env = any, P extends string = any, I extends Input = {}>(
options: OtelOptions = {} options: OtelOptions = {}
@ -30,9 +32,9 @@ export const otel = <E extends Env = any, P extends string = any, I extends Inpu
const result = await next() const result = await next()
const span = trace.getActiveSpan() const span = trace.getActiveSpan()
if (span != null) { if (span != null) {
const route = c.req.matchedRoutes[c.req.matchedRoutes.length - 1] const routePath = c.req.routePath
span.setAttribute(ATTR_HTTP_ROUTE, route.path) span.setAttribute(ATTR_HTTP_ROUTE, routePath)
span.updateName(`${c.req.method} ${route.path}`) span.updateName(`${c.req.method} ${routePath}`)
} }
return result return result
}) })
@ -40,15 +42,15 @@ export const otel = <E extends Env = any, P extends string = any, I extends Inpu
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider() const tracerProvider = options.tracerProvider ?? trace.getTracerProvider()
const tracer = tracerProvider.getTracer(PACKAGE_NAME, PACKAGE_VERSION) const tracer = tracerProvider.getTracer(PACKAGE_NAME, PACKAGE_VERSION)
return createMiddleware<E, P, I>(async (c, next) => { return createMiddleware<E, P, I>(async (c, next) => {
const route = c.req.matchedRoutes[c.req.matchedRoutes.length - 1] const routePath = c.req.routePath
await tracer.startActiveSpan( await tracer.startActiveSpan(
`${c.req.method} ${route.path}`, `${c.req.method} ${c.req.routePath}`,
{ {
kind: SpanKind.SERVER, kind: SpanKind.SERVER,
attributes: { attributes: {
[ATTR_HTTP_REQUEST_METHOD]: c.req.method, [ATTR_HTTP_REQUEST_METHOD]: c.req.method,
[ATTR_URL_FULL]: c.req.url, [ATTR_URL_FULL]: c.req.url,
[ATTR_HTTP_ROUTE]: route.path, [ATTR_HTTP_ROUTE]: routePath,
}, },
}, },
async (span) => { async (span) => {
@ -57,6 +59,10 @@ export const otel = <E extends Env = any, P extends string = any, I extends Inpu
} }
try { try {
await next() await next()
// Update the span name and route path now that we have the response
// because the route path may have changed
span.updateName(`${c.req.method} ${c.req.routePath}`)
span.setAttribute(ATTR_HTTP_ROUTE, c.req.routePath)
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, c.res.status) span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, c.res.status)
for (const [name, value] of c.res.headers.entries()) { for (const [name, value] of c.res.headers.entries()) {
span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(name), value) span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(name), value)