Skip to content

Commit

Permalink
Add support for async instrumentation register (#48575)
Browse files Browse the repository at this point in the history
  • Loading branch information
jankaifer committed Apr 24, 2023
1 parent 2679ab6 commit db764c3
Show file tree
Hide file tree
Showing 12 changed files with 115 additions and 29 deletions.
14 changes: 7 additions & 7 deletions docs/advanced-features/instrumentation.md
Expand Up @@ -23,30 +23,30 @@ export function register() {
}
```

However, we recommend importing files with side effects using `require` from within your `register` function instead. The following example demonstrates a basic usage of `require` in a `register` function:
However, we recommend importing files with side effects using `import` from within your `register` function instead. The following example demonstrates a basic usage of `import` in a `register` function:

```ts
// /instrumentation.ts

export function register() {
require('package-with-side-effect')
export async function register() {
await import('package-with-side-effect')
}
```

By doing this, you can colocate all of your side effects in one place in your code, and avoid any unintended consequences from importing files.

We call `register` in all environments, so it's necessary to conditionally require any code that doesn't support both `edge` and `nodejs`. You can use the environment variable `NEXT_RUNTIME` to get the current environment. Importing an environment-specific code would look like this:
We call `register` in all environments, so it's necessary to conditionally import any code that doesn't support both `edge` and `nodejs`. You can use the environment variable `NEXT_RUNTIME` to get the current environment. Importing an environment-specific code would look like this:

```ts
// /instrumentation.ts

export function register() {
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
require('./instrumentation-node')
await import('./instrumentation-node')
}

if (process.env.NEXT_RUNTIME === 'edge') {
require('./instrumentation-edge')
await import('./instrumentation-edge')
}
}
```
8 changes: 4 additions & 4 deletions docs/advanced-features/open-telemetry.md
Expand Up @@ -60,13 +60,13 @@ npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sema
```

Now you can initialize `NodeSDK` in your `instrumentation.ts`.
OpenTelemetry APIs are not compatible with edge runtime, so you need to make sure that you are importing them only when `process.env.NEXT_RUNTIME === 'nodejs'`. Conditionally importing with an `require` doesn't play well with typescript. We recommend using a conditionally `require`ing new file `instrumentation.node.ts` which can use normal `import`s:
OpenTelemetry APIs are not compatible with edge runtime, so you need to make sure that you are importing them only when `process.env.NEXT_RUNTIME === 'nodejs'`. We recommend creating a new file `instrumentation.node.ts` which you conditionally import only when using node:

```ts
// instrumentation.ts
export function register() {
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
require('./instrumentation.node.ts')
await import('./instrumentation.node.ts')
}
}
```
Expand All @@ -90,7 +90,7 @@ sdk.start()
```

Doing this is equivalent to using `@vercel/otel`, but it's possible to modify and extend.
For example, you could use `@opentelemetry/exporter-trace-otlp-grpc` instead of `@opentelemetry/exporter-trace-otlp-http`.
For example, you could use `@opentelemetry/exporter-trace-otlp-grpc` instead of `@opentelemetry/exporter-trace-otlp-http` or you can specify more resource attributes.

## Testing your instrumentation

Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/server/dev/next-dev-server.ts
Expand Up @@ -21,7 +21,10 @@ import findUp from 'next/dist/compiled/find-up'
import { join as pathJoin, relative, resolve as pathResolve, sep } from 'path'
import Watchpack from 'next/dist/compiled/watchpack'
import { ampValidation } from '../../build/output'
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../../lib/constants'
import {
INSTRUMENTATION_HOOK_FILENAME,
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
} from '../../lib/constants'
import { fileExists } from '../../lib/file-exists'
import { findPagesDir } from '../../lib/find-pages-dir'
import loadCustomRoutes from '../../lib/load-custom-routes'
Expand Down Expand Up @@ -1496,9 +1499,9 @@ export default class DevServer extends Server {
const instrumentationHook = await require(pathJoin(
this.distDir,
'server',
'instrumentation'
INSTRUMENTATION_HOOK_FILENAME
))
instrumentationHook.register()
await instrumentationHook.register()
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/server/next-server.ts
Expand Up @@ -340,8 +340,7 @@ export default class NextNodeServer extends BaseServer {
'server',
INSTRUMENTATION_HOOK_FILENAME
))

instrumentationHook.register?.()
await instrumentationHook.register?.()
} catch (err: any) {
if (err.code !== 'MODULE_NOT_FOUND') {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
Expand Down
39 changes: 27 additions & 12 deletions packages/next/src/server/web/adapter.ts
Expand Up @@ -59,9 +59,34 @@ export type AdapterOptions = {
IncrementalCache?: typeof import('../lib/incremental-cache').IncrementalCache
}

async function registerInstrumentation() {
if (
'_ENTRIES' in globalThis &&
_ENTRIES.middleware_instrumentation &&
_ENTRIES.middleware_instrumentation.register
) {
try {
await _ENTRIES.middleware_instrumentation.register()
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
}
}
}

let registerInstrumentationPromise: Promise<void> | null = null
function ensureInstrumentationRegistered() {
if (!registerInstrumentationPromise) {
registerInstrumentationPromise = registerInstrumentation()
}
return registerInstrumentationPromise
}

export async function adapter(
params: AdapterOptions
): Promise<FetchEventResult> {
await ensureInstrumentationRegistered()

// TODO-APP: use explicit marker for this
const isEdgeRendering = typeof self.__BUILD_MANIFEST !== 'undefined'

Expand Down Expand Up @@ -334,16 +359,6 @@ export function enhanceGlobals() {
configurable: false,
})

if (
'_ENTRIES' in globalThis &&
_ENTRIES.middleware_instrumentation &&
_ENTRIES.middleware_instrumentation.register
) {
try {
_ENTRIES.middleware_instrumentation.register()
} catch (err: any) {
err.message = `An error occurred while loading instrumentation hook: ${err.message}`
throw err
}
}
// Eagerly fire instrumentation hook to make the startup faster.
void ensureInstrumentationRegistered()
}
14 changes: 14 additions & 0 deletions test/e2e/instrumentation-hook/instrumentation-hook.test.ts
Expand Up @@ -82,6 +82,20 @@ describe('Instrumentation Hook', () => {
})
})

describeCase('with-async-node-page', ({ next }) => {
it('with-async-node-page should run the instrumentation hook', async () => {
const page = await next.render('/')
expect(page).toContain('Node - finished: true')
})
})

describeCase('with-async-edge-page', ({ next }) => {
it('with-async-edge-page should run the instrumentation hook', async () => {
const page = await next.render('/')
expect(page).toContain('Edge - finished: true')
})
})

describeCase('general', ({ next, isNextDev }) => {
it('should not overlap with a instrumentation page', async () => {
const page = await next.render('/instrumentation')
Expand Down
@@ -0,0 +1,8 @@
/*global globalThis*/

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

export async function register() {
await sleep(1000)
globalThis.instrumentationFinished = true
}
@@ -1,6 +1,8 @@
export function register() {
export async function register() {
if (process.env.NEXT_RUNTIME === 'edge') {
console.log('instrumentation hook on the edge')
const { register } = await import('./instrumentation.edge')
await register()
} else if (process.env.NEXT_RUNTIME === 'nodejs') {
console.log('instrumentation hook on nodejs')
} else {
Expand Down
15 changes: 15 additions & 0 deletions test/e2e/instrumentation-hook/with-async-edge-page/pages/index.tsx
@@ -0,0 +1,15 @@
export default function Page({ finished }) {
return <p>{`Edge - finished: ${finished}`}</p>
}

export function getServerSideProps() {
return {
props: {
finished: Boolean(globalThis.instrumentationFinished),
},
}
}

export const config = {
runtime: 'experimental-edge',
}
@@ -0,0 +1,11 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'edge') {
console.log('instrumentation hook on the edge')
} else if (process.env.NEXT_RUNTIME === 'nodejs') {
console.log('instrumentation hook on nodejs')
const { register } = await import('./instrumentation.node')
await register()
} else {
await require('this should fail')
}
}
@@ -0,0 +1,8 @@
/*global globalThis*/

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

export async function register() {
await sleep(1000)
globalThis.instrumentationFinished = true
}
11 changes: 11 additions & 0 deletions test/e2e/instrumentation-hook/with-async-node-page/pages/index.tsx
@@ -0,0 +1,11 @@
export default function Page({ finished }) {
return <p>{`Node - finished: ${finished}`}</p>
}

export async function getServerSideProps() {
return {
props: {
finished: Boolean(globalThis.instrumentationFinished),
},
}
}

0 comments on commit db764c3

Please sign in to comment.