Skip to content

Commit 311fafe

Browse files
piehlukasholzer
andauthored
feat: start moving to SugaredTracer and collect spans manually to be able to create 'server-timing' header when debugging (#358)
* chore: wrap blob.get with individual trace, add more attributes * feat: add server-timing header to response when x-nf-debug-logging is enabled * chore: remove tmp extra attributes * chore: remove dev logs * fix: lint * test: update request-context tests * chore: convert to using SugaredTracer methods * chore: fix lint, remove more unneded manual error setting on spans * fix: let's not await await * Apply suggestions from code review Co-authored-by: Lukas Holzer <[email protected]> * chore: format with prettier * fix: rename types --------- Co-authored-by: Lukas Holzer <[email protected]> Co-authored-by: pieh <[email protected]>
1 parent 037b695 commit 311fafe

File tree

11 files changed

+260
-170
lines changed

11 files changed

+260
-170
lines changed

.eslintrc.cjs

+17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ module.exports = {
3333
},
3434
overrides: [
3535
...overrides,
36+
{
37+
files: ['src/run/**'],
38+
rules: {
39+
'no-restricted-imports': [
40+
'error',
41+
{
42+
paths: [
43+
{
44+
name: '@opentelemetry/api',
45+
importNames: ['trace'],
46+
message: 'Please use `getTracer()` from `./handlers/tracer.cjs` instead',
47+
},
48+
],
49+
},
50+
],
51+
},
52+
},
3653
{
3754
files: ['src/run/handlers/**'],
3855
rules: {

src/build/templates/handler-monorepo.tmpl.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import tracing, { trace } from '{{cwd}}/.netlify/dist/run/handlers/tracing.js'
1+
import {
2+
createRequestContext,
3+
runWithRequestContext,
4+
} from '{{cwd}}/.netlify/dist/run/handlers/request-context.cjs'
5+
import { getTracer } from '{{cwd}}/.netlify/dist/run/handlers/tracer.cjs'
6+
import tracing from '{{cwd}}/.netlify/dist/run/handlers/tracing.js'
27

38
process.chdir('{{cwd}}')
49

@@ -8,10 +13,11 @@ export default async function (req, context) {
813
tracing.start()
914
}
1015

11-
/** @type {import('@opentelemetry/api').Tracer} */
12-
const tracer = trace.getTracer('Next.js Runtime')
13-
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
14-
try {
16+
const requestContext = createRequestContext(req.headers.get('x-nf-debug-logging'))
17+
const tracer = getTracer()
18+
19+
const handlerResponse = await runWithRequestContext(requestContext, () => {
20+
return tracer.withActiveSpan('Next.js Server Handler', async (span) => {
1521
span.setAttributes({
1622
'account.id': context.account.id,
1723
'deploy.id': context.deploy.id,
@@ -31,16 +37,14 @@ export default async function (req, context) {
3137
'http.status_code': response.status,
3238
})
3339
return response
34-
} catch (error) {
35-
span.recordException(error)
36-
if (error instanceof Error) {
37-
span.addEvent({ name: error.name, message: error.message })
38-
}
39-
throw error
40-
} finally {
41-
span.end()
42-
}
40+
})
4341
})
42+
43+
if (requestContext.serverTiming) {
44+
handlerResponse.headers.set('server-timing', requestContext.serverTiming)
45+
}
46+
47+
return handlerResponse
4448
}
4549

4650
export const config = {

src/build/templates/handler.tmpl.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
import {
2+
createRequestContext,
3+
runWithRequestContext,
4+
} from './.netlify/dist/run/handlers/request-context.cjs'
15
import serverHandler from './.netlify/dist/run/handlers/server.js'
2-
import tracing, { trace } from './.netlify/dist/run/handlers/tracing.js'
6+
import { getTracer } from './.netlify/dist/run/handlers/tracer.cjs'
7+
import tracing from './.netlify/dist/run/handlers/tracing.js'
38

49
export default async function handler(req, context) {
510
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
611
tracing.start()
712
}
813

9-
/** @type {import('@opentelemetry/api').Tracer} */
10-
const tracer = trace.getTracer('Next.js Runtime')
11-
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
12-
try {
14+
const requestContext = createRequestContext(req.headers.get('x-nf-debug-logging'))
15+
const tracer = getTracer()
16+
17+
const handlerResponse = await runWithRequestContext(requestContext, () => {
18+
return tracer.withActiveSpan('Next.js Server Handler', async (span) => {
1319
span.setAttributes({
1420
'account.id': context.account.id,
1521
'deploy.id': context.deploy.id,
@@ -25,16 +31,14 @@ export default async function handler(req, context) {
2531
'http.status_code': response.status,
2632
})
2733
return response
28-
} catch (error) {
29-
span.recordException(error)
30-
if (error instanceof Error) {
31-
span.addEvent({ name: error.name, message: error.message })
32-
}
33-
throw error
34-
} finally {
35-
span.end()
36-
}
34+
})
3735
})
36+
37+
if (requestContext.serverTiming) {
38+
handlerResponse.headers.set('server-timing', requestContext.serverTiming)
39+
}
40+
41+
return handlerResponse
3842
}
3943

4044
export const config = {

src/run/handlers/cache.cts

+55-78
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'
55

66
import { getDeployStore, Store } from '@netlify/blobs'
77
import { purgeCache } from '@netlify/functions'
8-
import { trace, type Span, SpanStatusCode } from '@opentelemetry/api'
8+
import { type Span } from '@opentelemetry/api'
99
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'
1010
import type {
1111
CacheHandler,
@@ -15,6 +15,7 @@ import type {
1515
} from 'next/dist/server/lib/incremental-cache/index.js'
1616

1717
import { getRequestContext } from './request-context.cjs'
18+
import { getTracer } from './tracer.cjs'
1819

1920
type TagManifest = { revalidatedAt: number }
2021

@@ -26,7 +27,7 @@ export class NetlifyCacheHandler implements CacheHandler {
2627
options: CacheHandlerContext
2728
revalidatedTags: string[]
2829
blobStore: Store
29-
tracer = trace.getTracer('Netlify Cache Handler')
30+
tracer = getTracer()
3031
tagManifestsFetchedFromBlobStoreInCurrentRequest: TagManifestBlobCache
3132

3233
constructor(options: CacheHandlerContext) {
@@ -90,77 +91,67 @@ export class NetlifyCacheHandler implements CacheHandler {
9091
}
9192

9293
async get(...args: Parameters<CacheHandler['get']>): ReturnType<CacheHandler['get']> {
93-
return this.tracer.startActiveSpan('get cache key', async (span) => {
94-
try {
95-
const [key, ctx = {}] = args
96-
console.debug(`[NetlifyCacheHandler.get]: ${key}`)
97-
98-
const blobKey = await this.encodeBlobKey(key)
99-
span.setAttributes({ key, blobKey })
100-
const blob = (await this.blobStore.get(blobKey, {
101-
type: 'json',
102-
})) as CacheHandlerValue | null
94+
return this.tracer.withActiveSpan('get cache key', async (span) => {
95+
const [key, ctx = {}] = args
96+
console.debug(`[NetlifyCacheHandler.get]: ${key}`)
10397

104-
// if blob is null then we don't have a cache entry
105-
if (!blob) {
106-
span.addEvent('Cache miss', { key, blobKey })
107-
return null
108-
}
98+
const blobKey = await this.encodeBlobKey(key)
99+
span.setAttributes({ key, blobKey })
109100

110-
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
101+
const blob = (await this.tracer.withActiveSpan('blobStore.get', async (blobGetSpan) => {
102+
blobGetSpan.setAttributes({ key, blobKey })
103+
return await this.blobStore.get(blobKey, {
104+
type: 'json',
105+
})
106+
})) as CacheHandlerValue | null
111107

112-
if (staleByTags) {
113-
span.addEvent('Stale', { staleByTags })
114-
return null
115-
}
108+
// if blob is null then we don't have a cache entry
109+
if (!blob) {
110+
span.addEvent('Cache miss', { key, blobKey })
111+
return null
112+
}
116113

117-
this.captureResponseCacheLastModified(blob, key, span)
114+
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
118115

119-
switch (blob.value?.kind) {
120-
case 'FETCH':
121-
span.addEvent('FETCH', { lastModified: blob.lastModified, revalidate: ctx.revalidate })
122-
return {
123-
lastModified: blob.lastModified,
124-
value: blob.value,
125-
}
126-
127-
case 'ROUTE':
128-
span.addEvent('ROUTE', { lastModified: blob.lastModified, status: blob.value.status })
129-
return {
130-
lastModified: blob.lastModified,
131-
value: {
132-
...blob.value,
133-
body: Buffer.from(blob.value.body as unknown as string, 'base64'),
134-
},
135-
}
136-
case 'PAGE':
137-
span.addEvent('PAGE', { lastModified: blob.lastModified })
138-
return {
139-
lastModified: blob.lastModified,
140-
value: blob.value,
141-
}
142-
default:
143-
span.recordException(new Error(`Unknown cache entry kind: ${blob.value?.kind}`))
144-
// TODO: system level logging not implemented
145-
}
116+
if (staleByTags) {
117+
span.addEvent('Stale', { staleByTags })
146118
return null
147-
} catch (error) {
148-
if (error instanceof Error) {
149-
span.recordException(error)
150-
}
151-
span.setStatus({
152-
code: SpanStatusCode.ERROR,
153-
message: error instanceof Error ? error.message : String(error),
154-
})
155-
throw error
156-
} finally {
157-
span.end()
158119
}
120+
121+
this.captureResponseCacheLastModified(blob, key, span)
122+
123+
switch (blob.value?.kind) {
124+
case 'FETCH':
125+
span.addEvent('FETCH', { lastModified: blob.lastModified, revalidate: ctx.revalidate })
126+
return {
127+
lastModified: blob.lastModified,
128+
value: blob.value,
129+
}
130+
131+
case 'ROUTE':
132+
span.addEvent('ROUTE', { lastModified: blob.lastModified, status: blob.value.status })
133+
return {
134+
lastModified: blob.lastModified,
135+
value: {
136+
...blob.value,
137+
body: Buffer.from(blob.value.body as unknown as string, 'base64'),
138+
},
139+
}
140+
case 'PAGE':
141+
span.addEvent('PAGE', { lastModified: blob.lastModified })
142+
return {
143+
lastModified: blob.lastModified,
144+
value: blob.value,
145+
}
146+
default:
147+
span.recordException(new Error(`Unknown cache entry kind: ${blob.value?.kind}`))
148+
}
149+
return null
159150
})
160151
}
161152

162153
async set(...args: Parameters<IncrementalCache['set']>) {
163-
return this.tracer.startActiveSpan('set cache key', async (span) => {
154+
return this.tracer.withActiveSpan('set cache key', async (span) => {
164155
const [key, data] = args
165156
const blobKey = await this.encodeBlobKey(key)
166157
const lastModified = Date.now()
@@ -189,7 +180,6 @@ export class NetlifyCacheHandler implements CacheHandler {
189180
})
190181
}
191182
}
192-
span.end()
193183
})
194184
}
195185

@@ -264,22 +254,9 @@ export class NetlifyCacheHandler implements CacheHandler {
264254

265255
if (!tagManifestPromise) {
266256
tagManifestPromise = this.encodeBlobKey(tag).then((blobKey) => {
267-
return this.tracer.startActiveSpan(`get tag manifest`, async (span) => {
257+
return this.tracer.withActiveSpan(`get tag manifest`, async (span) => {
268258
span.setAttributes({ tag, blobKey })
269-
try {
270-
return await this.blobStore.get(blobKey, { type: 'json' })
271-
} catch (error) {
272-
if (error instanceof Error) {
273-
span.recordException(error)
274-
}
275-
span.setStatus({
276-
code: SpanStatusCode.ERROR,
277-
message: error instanceof Error ? error.message : String(error),
278-
})
279-
throw error
280-
} finally {
281-
span.end()
282-
}
259+
return this.blobStore.get(blobKey, { type: 'json' })
283260
})
284261
})
285262

src/run/handlers/request-context.cts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { AsyncLocalStorage } from 'node:async_hooks'
22

33
export type RequestContext = {
4+
debug: boolean
45
responseCacheGetLastModified?: number
56
responseCacheKey?: string
67
usedFsRead?: boolean
78
didPagesRouterOnDemandRevalidate?: boolean
9+
serverTiming?: string
810
}
911

1012
type RequestContextAsyncLocalStorage = AsyncLocalStorage<RequestContext>
1113

12-
export function createRequestContext(): RequestContext {
13-
return {}
14+
export function createRequestContext(debug = false): RequestContext {
15+
return {
16+
debug,
17+
}
1418
}
1519

1620
const REQUEST_CONTEXT_GLOBAL_KEY = Symbol.for('nf-request-context-async-local-storage')

0 commit comments

Comments
 (0)