-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adding nextjs example * adding nextjs example * replaced the traces png * updating the instrumentation code
- Loading branch information
1 parent
44408dc
commit 4dcbd20
Showing
28 changed files
with
755 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Auto instrumenting NextJS application with OpenTelemetry | ||
|
||
This example demonstrates how to auto-instrument an NextJS application with | ||
OpenTelemetry. Make sure you have **Node.js v18** or higher installed on your | ||
machine. | ||
|
||
1. To clone this example run the following command: | ||
|
||
```bash | ||
npx degit last9/opentelemetry-examples/javascript/nextjs/user-management nextjs | ||
``` | ||
|
||
2. In the `express/env` directory create `.env` file and add the contents of | ||
`.env.example` file. | ||
|
||
```bash | ||
cd env | ||
cp .env.example .env | ||
``` | ||
|
||
3. Obtain the OTLP endpoint and the Auth Header from the Last9 dashboard and | ||
modify the values of the `OTLP_ENDPOINT` and `OTLP_AUTH_HEADER` variables | ||
accordingly in the `.env` file. | ||
|
||
4. Next, install the dependencies by running the following command in the | ||
`nextjs` directory: | ||
|
||
```bash | ||
npm install | ||
``` | ||
|
||
5. To build the project, run the following command in the `nextjs` directory: | ||
|
||
```bash | ||
npm run build | ||
``` | ||
|
||
6. Start the server by running the following command: | ||
|
||
```bash | ||
npm run start | ||
``` | ||
|
||
Once the server is running, you can access the application at | ||
`http://localhost:8081` by default. Where you can make CRUD operations. The API | ||
endpoints are: | ||
|
||
- GET `/api/users/` - Get all users | ||
- CREATE a new user in the `user-management` page | ||
- DELETE a user in the `user-management` page using delete button | ||
|
||
7. Sign in to [Last9 Dashboard](https://app.last9.io) and visit the APM | ||
dashboard to see the traces in action. | ||
|
||
![Traces](./traces.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = { | ||
experimental: { | ||
instrumentationHook: true | ||
}, | ||
// Keep your existing config options | ||
typescript: { | ||
ignoreBuildErrors: true | ||
}, | ||
eslint: { | ||
ignoreDuringBuilds: true | ||
} | ||
} | ||
|
||
module.exports = nextConfig |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
{ | ||
"name": "user-management", | ||
"version": "0.1.0", | ||
"private": true, | ||
"scripts": { | ||
"dev": "next dev -p 3001", | ||
"build": "next build", | ||
"start": "next start -p 3001", | ||
"lint": "next lint" | ||
}, | ||
"dependencies": { | ||
"@opentelemetry/api": "^1.9.0", | ||
"@opentelemetry/auto-instrumentations-node": "^0.54.0", | ||
"@opentelemetry/exporter-trace-otlp-http": "^0.56.0", | ||
"@opentelemetry/instrumentation": "^0.56.0", | ||
"@opentelemetry/resources": "^1.29.0", | ||
"@opentelemetry/sdk-node": "^0.56.0", | ||
"@opentelemetry/sdk-trace-base": "^1.29.0", | ||
"@opentelemetry/sdk-trace-node": "^1.29.0", | ||
"@opentelemetry/semantic-conventions": "^1.28.0", | ||
"@vercel/otel": "^1.10.0", | ||
"next": "15.0.4", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20", | ||
"@types/react": "^19", | ||
"@types/react-dom": "^19", | ||
"eslint": "^8", | ||
"eslint-config-next": "15.0.4", | ||
"postcss": "^8", | ||
"tailwindcss": "^3.4.1", | ||
"typescript": "^5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/** @type {import('postcss-load-config').Config} */ | ||
const config = { | ||
plugins: { | ||
tailwindcss: {}, | ||
}, | ||
}; | ||
|
||
export default config; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { NextResponse } from 'next/server' | ||
import { trace } from '@opentelemetry/api' | ||
import fs from 'fs' | ||
import path from 'path' | ||
|
||
const tracer = trace.getTracer('user-management-api') | ||
const dataFile = path.join(process.cwd(), 'data', 'users.json') | ||
|
||
// Helper function to read users | ||
const getUsers = () => { | ||
return tracer.startActiveSpan('users.read', async (span) => { | ||
try { | ||
if (!fs.existsSync(path.dirname(dataFile))) { | ||
fs.mkdirSync(path.dirname(dataFile), { recursive: true }) | ||
span.setAttribute('file.created_directory', true) | ||
} | ||
|
||
if (!fs.existsSync(dataFile)) { | ||
fs.writeFileSync(dataFile, JSON.stringify([]), 'utf8') | ||
console.log('Created new users.json file') | ||
span.setAttribute('file.created', true) | ||
return [] | ||
} | ||
|
||
const data = fs.readFileSync(dataFile, 'utf8') | ||
const users = JSON.parse(data) | ||
span.setAttribute('users.count', users.length) | ||
span.setStatus({ code: 0 }) // Success | ||
return users | ||
} catch (err) { | ||
console.error('Error reading users file:', err) | ||
span.setAttribute('error', true) | ||
span.setAttribute('error.message', err.message) | ||
span.setStatus({ code: 1, message: err.message }) | ||
return [] | ||
} finally { | ||
span.end() | ||
} | ||
}) | ||
} | ||
|
||
// Helper function to save users | ||
const saveUsers = (users) => { | ||
return tracer.startActiveSpan('users.save', async (span) => { | ||
try { | ||
fs.writeFileSync(dataFile, JSON.stringify(users, null, 2)) | ||
console.log('Successfully saved users') | ||
span.setAttribute('users.count', users.length) | ||
span.setStatus({ code: 0 }) | ||
return true | ||
} catch (err) { | ||
console.error('Error saving users:', err) | ||
span.setAttribute('error', true) | ||
span.setAttribute('error.message', err.message) | ||
span.setStatus({ code: 1, message: err.message }) | ||
return false | ||
} finally { | ||
span.end() | ||
} | ||
}) | ||
} | ||
|
||
// GET handler | ||
export async function GET() { | ||
return tracer.startActiveSpan('users.list', async (span) => { | ||
try { | ||
const users = await getUsers() | ||
span.setAttribute('users.count', users.length) | ||
span.setStatus({ code: 0 }) | ||
console.log('Retrieved users:', users) | ||
return NextResponse.json(users) | ||
} catch (err) { | ||
console.error('Error in GET handler:', err) | ||
span.setAttribute('error', true) | ||
span.setAttribute('error.message', err.message) | ||
span.setStatus({ code: 1, message: err.message }) | ||
return NextResponse.json( | ||
{ error: 'Failed to fetch users' }, | ||
{ status: 500 } | ||
) | ||
} finally { | ||
span.end() | ||
} | ||
}) | ||
} | ||
|
||
// POST handler | ||
export async function POST(request) { | ||
return tracer.startActiveSpan('users.create', async (span) => { | ||
try { | ||
const users = await getUsers() | ||
const data = await request.json() | ||
const newUser = { | ||
id: Date.now().toString(), | ||
...data | ||
} | ||
users.push(newUser) | ||
|
||
span.setAttribute('user.id', newUser.id) | ||
span.setAttribute('user.email', newUser.email) | ||
|
||
if (await saveUsers(users)) { | ||
span.setStatus({ code: 0 }) | ||
return NextResponse.json(newUser, { status: 201 }) | ||
} else { | ||
throw new Error('Failed to save user') | ||
} | ||
} catch (err) { | ||
console.error('Error in POST handler:', err) | ||
span.setAttribute('error', true) | ||
span.setAttribute('error.message', err.message) | ||
span.setStatus({ code: 1, message: err.message }) | ||
return NextResponse.json( | ||
{ error: 'Failed to add user' }, | ||
{ status: 500 } | ||
) | ||
} finally { | ||
span.end() | ||
} | ||
}) | ||
} | ||
|
||
// DELETE handler | ||
export async function DELETE(request) { | ||
return tracer.startActiveSpan('users.delete', async (span) => { | ||
try { | ||
const { searchParams } = new URL(request.url) | ||
const id = searchParams.get('id') | ||
span.setAttribute('user.id', id) | ||
|
||
let users = await getUsers() | ||
const initialCount = users.length | ||
users = users.filter(user => user.id !== id) | ||
|
||
span.setAttribute('users.deleted_count', initialCount - users.length) | ||
|
||
if (await saveUsers(users)) { | ||
span.setStatus({ code: 0 }) | ||
return NextResponse.json({ message: 'User deleted' }) | ||
} else { | ||
throw new Error('Failed to save after deletion') | ||
} | ||
} catch (err) { | ||
console.error('Error in DELETE handler:', err) | ||
span.setAttribute('error', true) | ||
span.setAttribute('error.message', err.message) | ||
span.setStatus({ code: 1, message: err.message }) | ||
return NextResponse.json( | ||
{ error: 'Failed to delete user' }, | ||
{ status: 500 } | ||
) | ||
} finally { | ||
span.end() | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
'use client' | ||
|
||
import { useState } from 'react' | ||
|
||
export default function UserForm({ onUserAdded }) { | ||
const [newUser, setNewUser] = useState({ name: '', email: '' }) | ||
const [loading, setLoading] = useState(false) | ||
|
||
const addUser = async (e) => { | ||
e.preventDefault() | ||
setLoading(true) | ||
try { | ||
const response = await fetch('http://localhost:3001/api/users', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(newUser), | ||
}) | ||
if (!response.ok) throw new Error('Failed to add user') | ||
setNewUser({ name: '', email: '' }) | ||
onUserAdded() | ||
} catch (err) { | ||
console.error('Failed to add user:', err) | ||
} finally { | ||
setLoading(false) | ||
} | ||
} | ||
|
||
return ( | ||
<form onSubmit={addUser} className="mb-8"> | ||
<div className="flex gap-4 mb-4"> | ||
<input | ||
type="text" | ||
placeholder="Name" | ||
value={newUser.name} | ||
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })} | ||
className="flex-1 p-2 border rounded text-red-500" | ||
required | ||
/> | ||
<input | ||
type="email" | ||
placeholder="Email" | ||
value={newUser.email} | ||
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} | ||
className="flex-1 p-2 border rounded text-red-500" | ||
required | ||
/> | ||
<button | ||
type="submit" | ||
disabled={loading} | ||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50" | ||
> | ||
{loading ? 'Adding...' : 'Add User'} | ||
</button> | ||
</div> | ||
</form> | ||
) | ||
} |
Oops, something went wrong.