Hide internal fetches OTel traces in dev mode and assert duplicate OTel spans are present only in dev mode (#47822)

This commit is contained in:
Jan Kaifer 2023-04-03 18:56:29 +02:00 committed by GitHub
parent da37c018f3
commit aeec6b5d0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 230 additions and 253 deletions

View file

@ -529,6 +529,7 @@ export async function apiResolver(
res.once('pipe', () => (wasPiped = true))
}
getTracer().getRootSpanAttributes()?.set('next.route', page)
// Call API route method
const apiRouteResult = await getTracer().trace(
NodeSpan.runHandler,

View file

@ -1226,6 +1226,7 @@ export async function renderToHTMLOrFlight(
)
}
getTracer().getRootSpanAttributes()?.set('next.route', pathname)
const bodyResult = getTracer().wrap(
AppRenderSpan.getBodyResult,
{

View file

@ -543,6 +543,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
'http.method': method,
'http.target': req.url,
},
// We will fire this from the renderer worker
hideSpan: this.serverOptions.dev && this.isRouterWorker,
},
async (span) =>
this.handleRequestImpl(req, res, parsedUrl).finally(() => {
@ -568,11 +570,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const route = rootSpanAttributes.get('next.route')
if (route) {
const newName = `${method} ${route}`
span.setAttributes({
'next.route': route,
'http.route': route,
'next.span_name': newName,
})
span.updateName(`${method} ${route}`)
span.updateName(newName)
}
})
)

View file

@ -136,7 +136,7 @@ export default class DevServer extends Server {
private edgeFunctions?: RoutingItem[]
private verifyingTypeScript?: boolean
private usingTypeScript?: boolean
private originalFetch?: typeof fetch
private originalFetch: typeof fetch
private staticPathsCache: LRUCache<
string,
UnwrapPromise<ReturnType<DevServer['getStaticPaths']>>
@ -187,7 +187,7 @@ export default class DevServer extends Server {
Error.stackTraceLimit = 50
} catch {}
super({ ...options, dev: true })
this.persistPatchedGlobals()
this.originalFetch = global.fetch
this.renderOpts.dev = true
this.renderOpts.appDirDevErrorLogger = (err: any) =>
this.logErrorWithOriginalStack(err, 'app-dir')
@ -1257,7 +1257,7 @@ export default class DevServer extends Server {
private async invokeIpcMethod(method: string, args: any[]): Promise<any> {
const ipcPort = process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT
if (ipcPort) {
const res = await fetch(
const res = await this.originalFetch(
`http://${this.hostname}:${ipcPort}?method=${
method as string
}&args=${encodeURIComponent(JSON.stringify(args))}`
@ -1703,12 +1703,8 @@ export default class DevServer extends Server {
return nextInvoke as NonNullable<typeof result>
}
private persistPatchedGlobals(): void {
this.originalFetch = global.fetch
}
private restorePatchedGlobals(): void {
global.fetch = this.originalFetch!
global.fetch = this.originalFetch
}
protected async ensurePage(opts: {

View file

@ -320,13 +320,13 @@ export class AppRouteRouteModule extends RouteModule<
}
)
const route = getPathnameFromAbsolutePath(this.resolvedPagePath)
getTracer().getRootSpanAttributes()?.set('next.route', route)
return getTracer().trace(
AppRouteRouteHandlersSpan.runHandler,
{
// TODO: propagate this pathname from route matcher
spanName: `executing api route (app) ${getPathnameFromAbsolutePath(
this.resolvedPagePath
)}`,
spanName: `executing api route (app) ${route}`,
},
() =>
handler(wrappedRequest, {

View file

@ -41,6 +41,7 @@ type TracerSpanOptions = Omit<SpanOptions, 'attributes'> & {
parentSpan?: Span
spanName?: string
attributes?: Partial<Record<AttributeNames, AttributeValue | undefined>>
hideSpan?: boolean
}
interface NextTracer {
@ -201,8 +202,9 @@ class NextTracerImpl implements NextTracer {
}
if (
!NextVanillaSpanAllowlist.includes(type) &&
process.env.NEXT_OTEL_VERBOSE !== '1'
(!NextVanillaSpanAllowlist.includes(type) &&
process.env.NEXT_OTEL_VERBOSE !== '1') ||
options.hideSpan
) {
return fn()
}

View file

@ -1054,13 +1054,18 @@ export default class NextNodeServer extends BaseServer {
params: Params | null
isAppPath: boolean
}): Promise<FindComponentsResult | null> {
getTracer().getRootSpanAttributes()?.set('next.route', pathname)
let route = pathname
if (isAppPath) {
// When in App we get page instead of route
route = pathname.replace(/\/[^/]*$/, '')
}
return getTracer().trace(
NextNodeServerSpan.findPageComponents,
{
spanName: `resolving page into components`,
attributes: {
'next.route': pathname,
'next.route': route,
},
},
() => this.findPageComponentsImpl({ pathname, query, params, isAppPath })

View file

@ -1361,6 +1361,7 @@ export async function renderToHTML(
}
}
getTracer().getRootSpanAttributes()?.set('next.route', renderOpts.pathname)
const documentResult = await getTracer().trace(
RenderSpan.renderDocument,
{

View file

@ -1,11 +1,11 @@
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
import {
NodeTracerProvider,
SimpleSpanProcessor,
SpanExporter,
ReadableSpan,
} from '@opentelemetry/sdk-trace-node'
} from '@opentelemetry/sdk-trace-base'
import {
ExportResult,
ExportResultCode,

View file

@ -1,9 +1,7 @@
import { Resource } from '@opentelemetry/resources'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import {
NodeTracerProvider,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node'
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
// You can use gRPC exporter instead
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'

View file

@ -10,12 +10,7 @@ createNextDescribe(
skipDeployment: true,
dependencies: require('./package.json').dependencies,
},
({ next }) => {
// TODO: remove after resolving dev expected behavior
// x-ref: https://github.com/vercel/next.js/pull/47822
if ((global as any).isNextDev) {
return it('should skip for dev for now', () => {})
}
({ next, isNextDev }) => {
const getTraces = async (): Promise<SavedSpan[]> => {
const traces = await next.readFile(traceFile)
return traces
@ -24,16 +19,6 @@ createNextDescribe(
.map((line) => JSON.parse(line))
}
const waitForRootSpan = async (numberOfRootTraces: number) => {
await check(async () => {
const spans = await getTraces()
const rootSpans = spans.filter((span) => !span.parentId)
return rootSpans.length >= numberOfRootTraces
? String(numberOfRootTraces)
: rootSpans.length
}, String(numberOfRootTraces))
}
/**
* Sanitize (modifies) span to make it ready for snapshot testing.
*/
@ -48,31 +33,29 @@ createNextDescribe(
return span
}
const sanitizeSpans = (spans: SavedSpan[]) => {
const seenSpans = new Set()
return spans
.sort((a, b) =>
(a.attributes?.['next.span_name'] ?? '').localeCompare(
b.attributes?.['next.span_name'] ?? ''
)
)
.sort((a, b) =>
(a.attributes?.['next.span_type'] ?? '').localeCompare(
b.attributes?.['next.span_type'] ?? ''
)
)
.map(sanitizeSpan)
.filter((span) => {
const target = span.attributes?.['http.target']
const result =
!span.attributes?.['http.url']?.startsWith('http://localhost') &&
!seenSpans.has(target)
if (target) {
seenSpans.add(target)
}
return result
})
}
const getSanitizedTraces = async (numberOfRootTraces: number) => {
await waitForRootSpan(numberOfRootTraces)
return sanitizeSpans(await getTraces())
let traces
await check(async () => {
traces = sanitizeSpans(await getTraces())
const rootSpans = traces.filter((span) => !span.parentId)
return String(rootSpans.length)
}, String(numberOfRootTraces))
return traces
}
const cleanTraces = async () => {
@ -87,130 +70,116 @@ createNextDescribe(
it('should handle RSC with fetch', async () => {
await next.fetch('/app/param/rsc-fetch')
await check(async () => {
const traces = await getSanitizedTraces(1)
for (const entry of [
{
attributes: {
'http.method': 'GET',
'http.url': 'https://vercel.com/',
'net.peer.name': 'vercel.com',
'next.span_name': 'fetch GET https://vercel.com/',
'next.span_type': 'AppRender.fetch',
expect(await getSanitizedTraces(1)).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"http.method": "GET",
"http.url": "https://vercel.com/",
"net.peer.name": "vercel.com",
"next.span_name": "fetch GET https://vercel.com/",
"next.span_type": "AppRender.fetch",
},
kind: 2,
name: 'fetch GET https://vercel.com/',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 2,
"name": "fetch GET https://vercel.com/",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.span_name': 'render route (app) /app/[param]/rsc-fetch',
'next.span_type': 'AppRender.getBodyResult',
Object {
"attributes": Object {
"next.span_name": "render route (app) /app/[param]/rsc-fetch",
"next.span_type": "AppRender.getBodyResult",
},
kind: 0,
name: 'render route (app) /app/[param]/rsc-fetch',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "render route (app) /app/[param]/rsc-fetch",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
{
attributes: {
'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',
Object {
"attributes": Object {
"http.method": "GET",
"http.route": "/app/[param]/rsc-fetch",
"http.status_code": 200,
"http.target": "/app/param/rsc-fetch",
"next.route": "/app/[param]/rsc-fetch",
"next.span_name": "GET /app/[param]/rsc-fetch",
"next.span_type": "BaseServer.handleRequest",
},
kind: 1,
name: 'GET /app/[param]/rsc-fetch/page',
parentId: undefined,
status: {
code: 0,
"kind": 1,
"name": "GET /app/[param]/rsc-fetch",
"parentId": undefined,
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.route': '/app/[param]/layout',
'next.span_name': 'generateMetadata /app/[param]/layout',
'next.span_type': 'ResolveMetadata.generateMetadata',
Object {
"attributes": Object {
"next.route": "/app/[param]/layout",
"next.span_name": "generateMetadata /app/[param]/layout",
"next.span_type": "ResolveMetadata.generateMetadata",
},
kind: 0,
name: 'generateMetadata /app/[param]/layout',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "generateMetadata /app/[param]/layout",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.route': '/app/[param]/rsc-fetch/page',
'next.span_name':
'generateMetadata /app/[param]/rsc-fetch/page',
'next.span_type': 'ResolveMetadata.generateMetadata',
Object {
"attributes": Object {
"next.route": "/app/[param]/rsc-fetch/page",
"next.span_name": "generateMetadata /app/[param]/rsc-fetch/page",
"next.span_type": "ResolveMetadata.generateMetadata",
},
kind: 0,
name: 'generateMetadata /app/[param]/rsc-fetch/page',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "generateMetadata /app/[param]/rsc-fetch/page",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
]) {
expect(traces).toContainEqual(entry)
}
return 'success'
}, 'success')
]
`)
})
it('should handle route handlers in app router', async () => {
await next.fetch('/api/app/param/data')
await check(async () => {
const traces = await getSanitizedTraces(1)
for (const entry of [
{
attributes: {
'next.span_name':
'executing api route (app) /api/app/[param]/data/route',
'next.span_type': 'AppRouteRouteHandlers.runHandler',
expect(await getSanitizedTraces(1)).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"next.span_name": "executing api route (app) /api/app/[param]/data/route",
"next.span_type": "AppRouteRouteHandlers.runHandler",
},
kind: 0,
name: 'executing api route (app) /api/app/[param]/data/route',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "executing api route (app) /api/app/[param]/data/route",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
{
attributes: {
'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',
Object {
"attributes": Object {
"http.method": "GET",
"http.status_code": 200,
"http.target": "/api/app/param/data",
"next.span_name": "GET /api/app/param/data",
"next.span_type": "BaseServer.handleRequest",
},
kind: 1,
name: 'GET /api/app/[param]/data/route',
parentId: undefined,
status: {
code: 0,
"kind": 1,
"name": "GET /api/app/param/data",
"parentId": undefined,
"status": Object {
"code": 0,
},
},
]) {
expect(traces).toContainEqual(entry)
}
return 'success'
}, 'success')
]
`)
})
})
@ -218,139 +187,139 @@ createNextDescribe(
it('should handle getServerSideProps', async () => {
await next.fetch('/pages/param/getServerSideProps')
await check(async () => {
const traces = await getSanitizedTraces(1)
for (const entry of [
{
attributes: {
'next.span_name':
'getServerSideProps /pages/[param]/getServerSideProps',
'next.span_type': 'Render.getServerSideProps',
expect(await getSanitizedTraces(1)).toMatchInlineSnapshot(`
Array [
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: 0,
name: 'getServerSideProps /pages/[param]/getServerSideProps',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 1,
"name": "GET /pages/[param]/getServerSideProps",
"parentId": undefined,
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.span_name':
'render route (pages) /pages/[param]/getServerSideProps',
'next.span_type': 'Render.renderDocument',
Object {
"attributes": Object {
"next.span_name": "getServerSideProps /pages/[param]/getServerSideProps",
"next.span_type": "Render.getServerSideProps",
},
kind: 0,
name: 'render route (pages) /pages/[param]/getServerSideProps',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "getServerSideProps /pages/[param]/getServerSideProps",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
]) {
expect(traces).toContainEqual(entry)
}
return 'success'
}, 'success')
Object {
"attributes": Object {
"next.span_name": "render route (pages) /pages/[param]/getServerSideProps",
"next.span_type": "Render.renderDocument",
},
"kind": 0,
"name": "render route (pages) /pages/[param]/getServerSideProps",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
]
`)
})
it("should handle getStaticProps when fallback: 'blocking'", async () => {
await next.fetch('/pages/param/getStaticProps')
await check(async () => {
const traces = await getSanitizedTraces(1)
for (const entry of [
{
attributes: {
'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',
expect(await getSanitizedTraces(1)).toMatchInlineSnapshot(`
Array [
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',
parentId: undefined,
status: {
code: 0,
"kind": 1,
"name": "GET /pages/[param]/getStaticProps",
"parentId": undefined,
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.span_name':
'getStaticProps /pages/[param]/getStaticProps',
'next.span_type': 'Render.getStaticProps',
Object {
"attributes": Object {
"next.span_name": "getStaticProps /pages/[param]/getStaticProps",
"next.span_type": "Render.getStaticProps",
},
kind: 0,
name: 'getStaticProps /pages/[param]/getStaticProps',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "getStaticProps /pages/[param]/getStaticProps",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.span_name':
'render route (pages) /pages/[param]/getStaticProps',
'next.span_type': 'Render.renderDocument',
Object {
"attributes": Object {
"next.span_name": "render route (pages) /pages/[param]/getStaticProps",
"next.span_type": "Render.renderDocument",
},
kind: 0,
name: 'render route (pages) /pages/[param]/getStaticProps',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "render route (pages) /pages/[param]/getStaticProps",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
]) {
expect(traces).toContainEqual(entry)
}
return 'success'
}, 'success')
]
`)
})
it('should handle api routes in pages', async () => {
await next.fetch('/api/pages/param/basic')
await check(async () => {
const traces = await getSanitizedTraces(1)
for (const entry of [
{
attributes: {
'http.method': 'GET',
'http.status_code': 200,
'http.target': '/api/pages/param/basic',
'next.span_name': 'GET /api/pages/param/basic',
'next.span_type': 'BaseServer.handleRequest',
expect(await getSanitizedTraces(1)).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"http.method": "GET",
"http.route": "/api/pages/[param]/basic",
"http.status_code": 200,
"http.target": "/api/pages/param/basic",
"next.route": "/api/pages/[param]/basic",
"next.span_name": "GET /api/pages/[param]/basic",
"next.span_type": "BaseServer.handleRequest",
},
kind: 1,
name: 'GET /api/pages/param/basic',
parentId: undefined,
status: {
code: 0,
"kind": 1,
"name": "GET /api/pages/[param]/basic",
"parentId": undefined,
"status": Object {
"code": 0,
},
},
{
attributes: {
'next.span_name':
'executing api route (pages) /api/pages/[param]/basic',
'next.span_type': 'Node.runHandler',
Object {
"attributes": Object {
"next.span_name": "executing api route (pages) /api/pages/[param]/basic",
"next.span_type": "Node.runHandler",
},
kind: 0,
name: 'executing api route (pages) /api/pages/[param]/basic',
parentId: '[parent-id]',
status: {
code: 0,
"kind": 0,
"name": "executing api route (pages) /api/pages/[param]/basic",
"parentId": "[parent-id]",
"status": Object {
"code": 0,
},
},
]) {
expect(traces).toContainEqual(entry)
}
return 'success'
}, 'success')
]
`)
})
})
}