Skip to content

Commit

Permalink
Add http.route into root otel span (#47392)
Browse files Browse the repository at this point in the history
Co-authored-by: Steven <[email protected]>
  • Loading branch information
jankaifer and styfle authored Mar 24, 2023
1 parent de8e4e9 commit adf8c7f
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 46 deletions.
38 changes: 32 additions & 6 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,22 +520,48 @@ export default abstract class Server<ServerOptions extends Options = Options> {
res: BaseNextResponse,
parsedUrl?: NextUrlWithParsedQuery
): Promise<void> {
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}`)
}
})
)
}

Expand Down Expand Up @@ -1998,7 +2024,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
{
spanName: `rendering page`,
attributes: {
'next.pathname': ctx.pathname,
'next.route': ctx.pathname,
},
},
async () => {
Expand Down
121 changes: 86 additions & 35 deletions packages/next/src/server/lib/trace/tracer.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -31,9 +37,10 @@ const closeSpanWithError = (span: Span, error?: Error) => {
span.end()
}

type TracerSpanOptions = SpanOptions & {
type TracerSpanOptions = Omit<SpanOptions, 'attributes'> & {
parentSpan?: Span
spanName?: string
attributes?: Partial<Record<AttributeNames, AttributeValue | undefined>>
}

interface NextTracer {
Expand Down Expand Up @@ -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<AttributeNames, AttributeValue | undefined>
>()
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.
Expand Down Expand Up @@ -186,49 +210,71 @@ 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,
'next.span_type': type,
...options.attributes,
}

const runWithContext = (actualFn: (span: Span) => T | Promise<T>) =>
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<T = (...args: Array<any>) => any>(type: SpanTypes, fn: T): T
Expand Down Expand Up @@ -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 = (() => {
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,7 @@ export default class NextNodeServer extends BaseServer {
params: Params | null
isAppPath: boolean
}): Promise<FindComponentsResult | null> {
getTracer().getRootSpanAttributes()?.set('next.route', pathname)
return getTracer().trace(
NextNodeServerSpan.findPageComponents,
{
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export default class Router {
RouterSpan.executeRoute,
{
attributes: {
route: route.name,
'next.route': route.name,
},
},
route.fn
Expand Down
16 changes: 12 additions & 4 deletions test/e2e/opentelemetry/opentelemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit adf8c7f

Please sign in to comment.