Skip to content

Commit

Permalink
adding nextjs example (#30)
Browse files Browse the repository at this point in the history
* adding nextjs example

* adding nextjs example

* replaced the traces png

* updating the instrumentation code
  • Loading branch information
karthikeyangs9 authored Dec 9, 2024
1 parent 44408dc commit 4dcbd20
Show file tree
Hide file tree
Showing 28 changed files with 755 additions and 0 deletions.
55 changes: 55 additions & 0 deletions javascript/nextjs/README.md
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)
1 change: 1 addition & 0 deletions javascript/nextjs/data/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
5 changes: 5 additions & 0 deletions javascript/nextjs/next-env.d.ts
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.
15 changes: 15 additions & 0 deletions javascript/nextjs/next.config.ts
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
36 changes: 36 additions & 0 deletions javascript/nextjs/package.json
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"
}
}
8 changes: 8 additions & 0 deletions javascript/nextjs/postcss.config.mjs
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;
1 change: 1 addition & 0 deletions javascript/nextjs/public/file.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions javascript/nextjs/public/globe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions javascript/nextjs/public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions javascript/nextjs/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions javascript/nextjs/public/window.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions javascript/nextjs/src/app/api/users/route.js
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()
}
})
}
59 changes: 59 additions & 0 deletions javascript/nextjs/src/app/components/UserForm.js
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>
)
}
Loading

0 comments on commit 4dcbd20

Please sign in to comment.