diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index aab94f1d6e94..a5fa60c99808 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -520,22 +520,48 @@ export default abstract class Server { res: BaseNextResponse, parsedUrl?: NextUrlWithParsedQuery ): Promise { + const method = req.method.toUpperCase() return getTracer().trace( BaseServerSpan.handleRequest, { - spanName: [req.method, req.url].join(' '), + spanName: `${method} ${req.url}`, kind: SpanKind.SERVER, attributes: { - 'http.method': req.method, + 'http.method': method, 'http.target': req.url, }, }, async (span) => - this.handleRequestImpl(req, res, parsedUrl).finally(() => - span?.setAttributes({ + this.handleRequestImpl(req, res, parsedUrl).finally(() => { + if (!span) return + span.setAttributes({ 'http.status_code': res.statusCode, }) - ) + const rootSpanAttributes = getTracer().getRootSpanAttributes() + // We were unable to get attributes, probably OTEL is not enabled + if (!rootSpanAttributes) return + + if ( + rootSpanAttributes.get('next.span_type') !== + BaseServerSpan.handleRequest + ) { + console.warn( + `Unexpected root span type '${rootSpanAttributes.get( + 'next.span_type' + )}'. Please report this Next.js issue https://github.com/vercel/next.js` + ) + return + } + + const route = rootSpanAttributes.get('next.route') + if (route) { + span.setAttributes({ + 'next.route': route, + 'http.route': route, + }) + span.updateName(`${method} ${route}`) + } + }) ) } @@ -1998,7 +2024,7 @@ export default abstract class Server { { spanName: `rendering page`, attributes: { - 'next.pathname': ctx.pathname, + 'next.route': ctx.pathname, }, }, async () => { diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index 45e8a229b704..88a8b0badb10 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -1,6 +1,12 @@ import { NextVanillaSpanAllowlist, SpanTypes } from './constants' -import type { ContextAPI, Span, SpanOptions, Tracer } from '@opentelemetry/api' +import type { + ContextAPI, + Span, + SpanOptions, + Tracer, + AttributeValue, +} from '@opentelemetry/api' let api: typeof import('@opentelemetry/api') @@ -31,9 +37,10 @@ const closeSpanWithError = (span: Span, error?: Error) => { span.end() } -type TracerSpanOptions = SpanOptions & { +type TracerSpanOptions = Omit & { parentSpan?: Span spanName?: string + attributes?: Partial> } interface NextTracer { @@ -117,6 +124,23 @@ interface NextTracer { getActiveScopeSpan(): Span | undefined } +type NextAttributeNames = + | 'next.route' + | 'next.page' + | 'next.span_name' + | 'next.span_type' +type OTELAttributeNames = `http.${string}` | `net.${string}` +type AttributeNames = NextAttributeNames | OTELAttributeNames + +/** we use this map to propagate attributes from nested spans to the top span */ +const rootSpanAttributesStore = new Map< + number, + Map +>() +const rootSpanIdKey = api.createContextKey('next.rootSpanId') +let lastSpanId = 0 +const getSpanId = () => lastSpanId++ + class NextTracerImpl implements NextTracer { /** * Returns an instance to the trace with configured name. @@ -186,9 +210,17 @@ class NextTracerImpl implements NextTracer { const spanName = options.spanName ?? type // Trying to get active scoped span to assign parent. If option specifies parent span manually, will try to use it. - const spanContext = this.getSpanContext( + let spanContext = this.getSpanContext( options?.parentSpan ?? this.getActiveScopeSpan() ) + let isRootSpan = false + + if (!spanContext) { + spanContext = api.ROOT_CONTEXT + isRootSpan = true + } + + const spanId = getSpanId() options.attributes = { 'next.span_name': spanName, @@ -196,39 +228,53 @@ class NextTracerImpl implements NextTracer { ...options.attributes, } - const runWithContext = (actualFn: (span: Span) => T | Promise) => - spanContext - ? this.getTracerInstance().startActiveSpan( - spanName, - options, - spanContext, - actualFn - ) - : this.getTracerInstance().startActiveSpan(spanName, options, actualFn) - - return runWithContext((span: Span) => { - try { - if (fn.length > 1) { - return fn(span, (err?: Error) => closeSpanWithError(span, err)) - } - - const result = fn(span) - - if (isPromise(result)) { - result.then( - () => span.end(), - (err) => closeSpanWithError(span, err) - ) - } else { - span.end() + return api.context.with(spanContext.setValue(rootSpanIdKey, spanId), () => + this.getTracerInstance().startActiveSpan( + spanName, + options, + (span: Span) => { + const onCleanup = () => { + rootSpanAttributesStore.delete(spanId) + } + if (isRootSpan) { + rootSpanAttributesStore.set( + spanId, + new Map( + Object.entries(options.attributes ?? {}) as [ + AttributeNames, + AttributeValue | undefined + ][] + ) + ) + } + try { + if (fn.length > 1) { + return fn(span, (err?: Error) => closeSpanWithError(span, err)) + } + + const result = fn(span) + + if (isPromise(result)) { + result + .then( + () => span.end(), + (err) => closeSpanWithError(span, err) + ) + .finally(onCleanup) + } else { + span.end() + onCleanup() + } + + return result + } catch (err: any) { + closeSpanWithError(span, err) + onCleanup() + throw err + } } - - return result - } catch (err: any) { - closeSpanWithError(span, err) - throw err - } - }) + ) + ) } public wrap) => any>(type: SpanTypes, fn: T): T @@ -297,6 +343,11 @@ class NextTracerImpl implements NextTracer { return spanContext } + + public getRootSpanAttributes() { + const spanId = context.active().getValue(rootSpanIdKey) as number + return rootSpanAttributesStore.get(spanId) + } } const getTracer = (() => { diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index b1331a1e5455..7940f04419ba 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -960,6 +960,7 @@ export default class NextNodeServer extends BaseServer { params: Params | null isAppPath: boolean }): Promise { + getTracer().getRootSpanAttributes()?.set('next.route', pathname) return getTracer().trace( NextNodeServerSpan.findPageComponents, { diff --git a/packages/next/src/server/router.ts b/packages/next/src/server/router.ts index 729586f6f201..92704a5e0d1e 100644 --- a/packages/next/src/server/router.ts +++ b/packages/next/src/server/router.ts @@ -260,7 +260,7 @@ export default class Router { RouterSpan.executeRoute, { attributes: { - route: route.name, + 'next.route': route.name, }, }, route.fn diff --git a/test/e2e/opentelemetry/opentelemetry.test.ts b/test/e2e/opentelemetry/opentelemetry.test.ts index a323ddc39db8..a785322bc6e5 100644 --- a/test/e2e/opentelemetry/opentelemetry.test.ts +++ b/test/e2e/opentelemetry/opentelemetry.test.ts @@ -116,13 +116,15 @@ createNextDescribe( Object { "attributes": Object { "http.method": "GET", + "http.route": "/app/[param]/rsc-fetch/page", "http.status_code": 200, "http.target": "/app/param/rsc-fetch", + "next.route": "/app/[param]/rsc-fetch/page", "next.span_name": "GET /app/param/rsc-fetch", "next.span_type": "BaseServer.handleRequest", }, "kind": 1, - "name": "GET /app/param/rsc-fetch", + "name": "GET /app/[param]/rsc-fetch/page", "parentId": undefined, "status": Object { "code": 0, @@ -178,13 +180,15 @@ createNextDescribe( Object { "attributes": Object { "http.method": "GET", + "http.route": "/api/app/[param]/data/route", "http.status_code": 200, "http.target": "/api/app/param/data", + "next.route": "/api/app/[param]/data/route", "next.span_name": "GET /api/app/param/data", "next.span_type": "BaseServer.handleRequest", }, "kind": 1, - "name": "GET /api/app/param/data", + "name": "GET /api/app/[param]/data/route", "parentId": undefined, "status": Object { "code": 0, @@ -204,13 +208,15 @@ createNextDescribe( Object { "attributes": Object { "http.method": "GET", + "http.route": "/pages/[param]/getServerSideProps", "http.status_code": 200, "http.target": "/pages/param/getServerSideProps", + "next.route": "/pages/[param]/getServerSideProps", "next.span_name": "GET /pages/param/getServerSideProps", "next.span_type": "BaseServer.handleRequest", }, "kind": 1, - "name": "GET /pages/param/getServerSideProps", + "name": "GET /pages/[param]/getServerSideProps", "parentId": undefined, "status": Object { "code": 0, @@ -252,13 +258,15 @@ createNextDescribe( Object { "attributes": Object { "http.method": "GET", + "http.route": "/pages/[param]/getStaticProps", "http.status_code": 200, "http.target": "/pages/param/getStaticProps", + "next.route": "/pages/[param]/getStaticProps", "next.span_name": "GET /pages/param/getStaticProps", "next.span_type": "BaseServer.handleRequest", }, "kind": 1, - "name": "GET /pages/param/getStaticProps", + "name": "GET /pages/[param]/getStaticProps", "parentId": undefined, "status": Object { "code": 0,