Skip to content

Commit

Permalink
feat: add OTEL instrumentation for next-server + OTEL example (#46198)
Browse files Browse the repository at this point in the history
fixes NEXT-479

## content

This PR adds a `getTracer` API to Next.js that uses the `otel/api` under
the hood to provide Next.js level instrumentation through Open
Telemetry.

This also adds an example `with-opentelemetry` to demonstrate how it can
be used, assuming you have a collector.

This allows most notably to have `getServerSideProps` and `fetch` calls
inside Server Components traced.

## details

- we hide most internals spans, if you want to see all of them, use the
NEXT_OTEL_VERBOSE=1 env var
- if you want to use this, you'll need to rely on the
`config.experimental.instrumentationHook` config option to initialise
OTEL, like in the example

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ]
[e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have a helpful link attached, see
[`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md)

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm build && pnpm lint`
- [ ] The "examples guidelines" are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
  • Loading branch information
feedthejim authored Feb 22, 2023
1 parent 65ecd9c commit 4072955
Show file tree
Hide file tree
Showing 32 changed files with 1,415 additions and 303 deletions.
36 changes: 36 additions & 0 deletions examples/with-opentelemetry/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
4 changes: 4 additions & 0 deletions examples/with-opentelemetry/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "../../node_modules/.pnpm/typescript@4.7.4/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
30 changes: 30 additions & 0 deletions examples/with-opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Data fetch example

Next.js was conceived to make it easy to create universal apps. That's why fetching data
on the server and the client when necessary is so easy with Next.js.

By using `getStaticProps` Next.js will fetch data at build time from a page, and pre-render the page to static assets.

## Deploy your own

Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/data-fetch)

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/data-fetch&project-name=data-fetch&repository-name=data-fetch)

## How to use

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:

```bash
npx create-next-app --example data-fetch data-fetch-app
```

```bash
yarn create next-app --example data-fetch data-fetch-app
```

```bash
pnpm create next-app --example data-fetch data-fetch-app
```

Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
12 changes: 12 additions & 0 deletions examples/with-opentelemetry/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function Layout({ children }) {
return (
<html>
<head>
<title>Next.js with OpenTelemetry</title>
</head>
<body>
<main>{children}</main>
</body>
</html>
)
}
12 changes: 12 additions & 0 deletions examples/with-opentelemetry/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link'
import { fetchGithubStars } from '../shared/fetch-github-stars'

export default async function Page() {
const stars = await fetchGithubStars()
return (
<>
<p>Next.js has {stars} ⭐️</p>
<Link href="/preact-stars">How about preact?</Link>
</>
)
}
23 changes: 23 additions & 0 deletions examples/with-opentelemetry/instrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const opentelemetry = require('@opentelemetry/sdk-node')
const {
OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http')
const { Resource } = require('@opentelemetry/resources')
const {
SemanticResourceAttributes,
} = require('@opentelemetry/semantic-conventions')

const sdk = new opentelemetry.NodeSDK({
traceExporter: new OTLPTraceExporter({}),
instrumentations: [],
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-next-app',
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
}),
})

sdk.start()
}
}
6 changes: 6 additions & 0 deletions examples/with-opentelemetry/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
experimental: {
instrumentationHook: true,
appDir: true,
},
}
28 changes: 28 additions & 0 deletions examples/with-opentelemetry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"private": true,
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
"start:otel-verbose": "NEXT_OTEL_VERBOSE=1 next start"
},
"dependencies": {
"@opentelemetry/api": "1.4.0",
"@opentelemetry/auto-instrumentations-node": "0.36.3",
"@opentelemetry/exporter-trace-otlp-grpc": "0.35.1",
"@opentelemetry/exporter-trace-otlp-http": "0.35.1",
"@opentelemetry/instrumentation-fetch": "0.35.1",
"@opentelemetry/resources": "1.9.1",
"@opentelemetry/sdk-node": "0.35.1",
"@opentelemetry/sdk-trace-base": "1.9.1",
"@opentelemetry/semantic-conventions": "1.9.1",
"next": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "18.7.11",
"@types/react": "18.0.17",
"typescript": "4.7.4"
}
}
11 changes: 11 additions & 0 deletions examples/with-opentelemetry/pages/api/github-stars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// hello world api route
import { NextApiRequest, NextApiResponse } from 'next'
import { fetchGithubStars } from '../../shared/fetch-github-stars'

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const stars = await fetchGithubStars()
res.status(200).json({ stars })
}
20 changes: 20 additions & 0 deletions examples/with-opentelemetry/pages/legacy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link'
import { fetchGithubStars } from '../shared/fetch-github-stars'

export async function getServerSideProps() {
const stars = await fetchGithubStars()
return {
props: {
stars,
},
}
}

export default function IndexPage({ stars }) {
return (
<>
<p>Next.js has {stars} ⭐️</p>
<Link href="/preact-stars">How about preact?</Link>
</>
)
}
15 changes: 15 additions & 0 deletions examples/with-opentelemetry/shared/fetch-github-stars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { trace } from '@opentelemetry/api'

export async function fetchGithubStars() {
const span = trace.getTracer('nextjs-example').startSpan('fetchGithubStars')
return fetch('https://api.github.com/repos/vercel/next.js', {
next: {
revalidate: 0,
},
})
.then((res) => res.json())
.then((data) => data.stargazers_count)
.finally(() => {
span.end()
})
}
26 changes: 26 additions & 0 deletions examples/with-opentelemetry/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
8 changes: 7 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"node-sass": "^6.0.0 || ^7.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
"sass": "^1.3.0",
"@opentelemetry/api": "^1.4.0"
},
"peerDependenciesMeta": {
"node-sass": {
Expand All @@ -102,6 +103,9 @@
},
"fibers": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
}
},
"devDependencies": {
Expand Down Expand Up @@ -131,6 +135,7 @@
"@hapi/accept": "5.0.2",
"@napi-rs/cli": "2.14.7",
"@napi-rs/triples": "1.1.0",
"@opentelemetry/api": "1.4.0",
"@next/polyfill-module": "13.1.7-canary.25",
"@next/polyfill-nomodule": "13.1.7-canary.25",
"@next/react-dev-overlay": "13.1.7-canary.25",
Expand All @@ -153,6 +158,7 @@
"@types/cookie": "0.3.3",
"@types/cross-spawn": "6.0.0",
"@types/debug": "4.1.5",
"@types/express-serve-static-core": "4.17.33",
"@types/fresh": "0.5.0",
"@types/glob": "7.1.1",
"@types/jsonwebtoken": "9.0.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,18 @@ export default async function build(
appDir ? path.join(SERVER_DIRECTORY, APP_PATHS_MANIFEST) : null,
path.join(SERVER_DIRECTORY, FONT_LOADER_MANIFEST + '.js'),
path.join(SERVER_DIRECTORY, FONT_LOADER_MANIFEST + '.json'),
...(hasInstrumentationHook
? [
path.join(
SERVER_DIRECTORY,
`${INSTRUMENTATION_HOOK_FILENAME}.js`
),
path.join(
SERVER_DIRECTORY,
`edge-${INSTRUMENTATION_HOOK_FILENAME}.js`
),
]
: []),
]
.filter(nonNullable)
.map((file) => path.join(config.distDir, file)),
Expand Down
9 changes: 8 additions & 1 deletion packages/next/src/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,11 @@ export default async function getBaseWebpackConfig(

const reactDir = path.dirname(require.resolve('react/package.json'))
const reactDomDir = path.dirname(require.resolve('react-dom/package.json'))
let hasOptionalOTELAPIPackage = false
try {
require('@opentelemetry/api')
hasOptionalOTELAPIPackage = true
} catch {}

const resolveConfig: webpack.Configuration['resolve'] = {
// Disable .mjs for node_modules bundling
Expand Down Expand Up @@ -955,6 +960,9 @@ export default async function getBaseWebpackConfig(
'next/dist/client/components/navigation',
[require.resolve('next/dist/client/components/headers')]:
'next/dist/client/components/headers',
'@opentelemetry/api': hasOptionalOTELAPIPackage
? '@opentelemetry/api'
: 'next/dist/compiled/@opentelemetry/api',
}
: undefined),

Expand Down Expand Up @@ -1101,7 +1109,6 @@ export default async function getBaseWebpackConfig(
// Returning from the function in case the directory has already been added and traversed
if (topLevelFrameworkPaths.includes(directory)) return
topLevelFrameworkPaths.push(directory)

const dependencies = require(packageJsonPath).dependencies || {}
for (const name of Object.keys(dependencies)) {
addPackagePath(name, directory)
Expand Down
Loading

0 comments on commit 4072955

Please sign in to comment.