Next.js
Learn how to integrate OpenTelemetry with Next.js applications and send telemetry data to Last9
This guide shows you how to instrument your Next.js application with OpenTelemetry and send traces, metrics, and logs to Last9. This covers both App Router (13+) and Pages Router approaches.
Prerequisites
- Node.js 16 or later
- Next.js 12+ application
- Last9 account with OTLP endpoint configured
Installation
-
Install OpenTelemetry packages
npm install --save @opentelemetry/api \@opentelemetry/sdk-node \@opentelemetry/auto-instrumentations-node \@opentelemetry/exporter-trace-otlp-http \@opentelemetry/exporter-metrics-otlp-http \@opentelemetry/exporter-logs-otlp-http \@opentelemetry/resources \@opentelemetry/semantic-conventions -
Install Next.js specific packages (optional for enhanced features)
npm install --save @vercel/otel \@opentelemetry/instrumentation-http \@opentelemetry/instrumentation-fetch \@opentelemetry/instrumentation-fs
Configuration
Set up your environment variables:
export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_SERVICE_NAME="nextjs-app"export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"export NODE_ENV="production"Server-Side Instrumentation
Enable Next.js Instrumentation Hook
/** @type {import('next').NextConfig} */const nextConfig = { experimental: { instrumentationHook: true, }, // Optional: Enable OpenTelemetry support experimental: { serverComponentsExternalPackages: ["@opentelemetry/api"], },};
module.exports = nextConfig;import type { NextConfig } from "next";
const nextConfig: NextConfig = { experimental: { instrumentationHook: true, serverComponentsExternalPackages: ["@opentelemetry/api"], },};
export default nextConfig;Core Instrumentation Setup
export async function register() { // Only register instrumentation in Node.js environment (server-side) if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation.node"); }}import { NodeSDK } from "@opentelemetry/sdk-node";import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";import { Resource } from "@opentelemetry/resources";import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_DEPLOYMENT_ENVIRONMENT,} from "@opentelemetry/semantic-conventions";import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
// Create resource with service informationconst resource = new Resource({ [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "nextjs-app", [ATTR_SERVICE_VERSION]: process.env.npm_package_version || "1.0.0", [ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || "development",});
// Configure exportersconst traceExporter = new OTLPTraceExporter({ url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`, headers: { Authorization: process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") || "", },});
const metricExporter = new OTLPMetricExporter({ url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics`, headers: { Authorization: process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") || "", },});
const logExporter = new OTLPLogExporter({ url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`, headers: { Authorization: process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") || "", },});
// Initialize the SDKconst sdk = new NodeSDK({ resource, traceExporter, metricReader: new PeriodicExportingMetricReader({ exporter: metricExporter, exportIntervalMillis: 30000, // Export metrics every 30 seconds }), logRecordProcessors: [new BatchLogRecordProcessor(logExporter)], instrumentations: [ getNodeAutoInstrumentations({ // Disable file system instrumentation as it's too noisy for Next.js "@opentelemetry/instrumentation-fs": { enabled: false, }, // Disable DNS instrumentation to reduce noise "@opentelemetry/instrumentation-dns": { enabled: false, }, // Configure HTTP instrumentation "@opentelemetry/instrumentation-http": { enabled: true, ignoreIncomingRequestHook: (req) => { // Ignore health checks and static assets const url = req.url || ""; return ( url.includes("/_next/") || url.includes("/favicon.ico") || url.includes("/health") || url.includes("/api/health") ); }, }, // Configure fetch instrumentation for server-side API calls "@opentelemetry/instrumentation-fetch": { enabled: true, ignoreUrls: [/\/_next\//, /\/favicon\.ico/, /\/health/], }, }), ],});
// Start the SDKtry { sdk.start(); console.log( "🚀 Next.js OpenTelemetry instrumentation initialized successfully", );} catch (error) { console.error("❌ Error initializing OpenTelemetry:", error);}
// Graceful shutdownprocess.on("SIGTERM", () => { sdk.shutdown().finally(() => process.exit(0));});
process.on("SIGINT", () => { sdk.shutdown().finally(() => process.exit(0));});Enhanced Instrumentation with Custom Telemetry
import { trace, metrics, context } from "@opentelemetry/api";import type { Span, Tracer, Meter } from "@opentelemetry/api";
// Get tracer and meter instancesexport const tracer: Tracer = trace.getTracer("nextjs-app", "1.0.0");export const meter: Meter = metrics.getMeter("nextjs-app", "1.0.0");
// Create custom metricsexport const nextjsMetrics = { pageViews: meter.createCounter("nextjs_page_views_total", { description: "Total number of page views", }),
apiCalls: meter.createCounter("nextjs_api_calls_total", { description: "Total number of API calls", }),
renderDuration: meter.createHistogram("nextjs_render_duration_ms", { description: "Duration of page renders in milliseconds", unit: "ms", }),
hydrationDuration: meter.createHistogram("nextjs_hydration_duration_ms", { description: "Duration of client-side hydration in milliseconds", unit: "ms", }),
errorCount: meter.createCounter("nextjs_errors_total", { description: "Total number of errors", }),
cacheHits: meter.createCounter("nextjs_cache_hits_total", { description: "Total number of cache hits", }),
cacheMisses: meter.createCounter("nextjs_cache_misses_total", { description: "Total number of cache misses", }),};
// Utility functions for common tracing patternsexport const withTracing = async <T>( name: string, fn: (span: Span) => Promise<T>, attributes: Record<string, string | number | boolean> = {},): Promise<T> => { return tracer.startActiveSpan(name, async (span) => { try { // Set attributes Object.entries(attributes).forEach(([key, value]) => { span.setAttributes({ [key]: value }); });
const result = await fn(span);
span.setStatus({ code: 1 }); // OK return result; } catch (error) { span.recordException(error as Error); span.setStatus({ code: 2, message: (error as Error).message }); // ERROR throw error; } finally { span.end(); } });};
export const getCurrentSpan = (): Span | undefined => { return trace.getActiveSpan();};
export const addSpanAttributes = ( attributes: Record<string, string | number | boolean>,): void => { const span = getCurrentSpan(); if (span) { span.setAttributes(attributes); }};import { NextRequest, NextResponse } from "next/server";import { tracer, nextjsMetrics, addSpanAttributes } from "./telemetry";
export function withTelemetryMiddleware( handler: (req: NextRequest) => Promise<NextResponse> | NextResponse,) { return async (req: NextRequest): Promise<NextResponse> => { const startTime = Date.now();
return tracer.startActiveSpan("nextjs.middleware", async (span) => { try { // Set basic span attributes span.setAttributes({ "http.method": req.method, "http.url": req.url, "http.route": req.nextUrl.pathname, "user_agent.original": req.headers.get("user-agent") || "", });
// Execute the handler const response = await handler(req);
// Record metrics const duration = Date.now() - startTime; const statusCode = response.status;
nextjsMetrics.apiCalls.add(1, { method: req.method, route: req.nextUrl.pathname, status_code: statusCode.toString(), });
// Set response attributes span.setAttributes({ "http.status_code": statusCode, "http.response.size": response.headers.get("content-length") || 0, });
return response; } catch (error) { span.recordException(error as Error); span.setStatus({ code: 2, message: (error as Error).message });
nextjsMetrics.errorCount.add(1, { error_type: "middleware_error", route: req.nextUrl.pathname, });
throw error; } }); };}App Router (Next.js 13+) Integration
Server Components
import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'import { Suspense } from 'react'
async function fetchUsers() { return withTracing('users.fetch_all', async (span) => { const startTime = Date.now()
try { // Simulate API call const response = await fetch('https://jsonplaceholder.typicode.com/users')
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) }
const users = await response.json()
span.setAttributes({ 'users.count': users.length, 'http.status_code': response.status, 'cache.hit': response.headers.get('x-cache') === 'HIT', })
// Record cache metrics if (response.headers.get('x-cache') === 'HIT') { nextjsMetrics.cacheHits.add(1, { type: 'users' }) } else { nextjsMetrics.cacheMisses.add(1, { type: 'users' }) }
return users } catch (error) { nextjsMetrics.errorCount.add(1, { error_type: 'fetch_error', operation: 'fetch_users', }) throw error } finally { const duration = Date.now() - startTime nextjsMetrics.renderDuration.record(duration, { component: 'users_page', operation: 'fetch_users', }) } })}
async function UsersList() { const users = await fetchUsers()
return ( <div className="grid gap-4"> {users.map((user: any) => ( <div key={user.id} className="p-4 border rounded"> <h3 className="font-bold">{user.name}</h3> <p className="text-gray-600">{user.email}</p> <p className="text-sm">{user.company.name}</p> </div> ))} </div> )}
export default async function UsersPage() { // Record page view nextjsMetrics.pageViews.add(1, { page: '/users', type: 'server_component', })
return ( <div className="container mx-auto px-4 py-8"> <h1 className="text-3xl font-bold mb-8">Users</h1> <Suspense fallback={<div>Loading users...</div>}> <UsersList /> </Suspense> </div> )}
// Generate metadata with tracingexport async function generateMetadata() { return withTracing('users.generate_metadata', async (span) => { span.setAttributes({ 'metadata.page': '/users', 'metadata.type': 'dynamic', })
return { title: 'Users - Next.js App', description: 'List of all users', } })}import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'import { notFound } from 'next/navigation'
interface User { id: number name: string email: string phone: string website: string company: { name: string catchPhrase: string }}
async function fetchUser(id: string): Promise<User | null> { return withTracing('user.fetch_by_id', async (span) => { span.setAttributes({ 'user.id': id, 'operation': 'fetch_user', })
try { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
if (response.status === 404) { span.setAttributes({ 'user.found': false }) return null }
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) }
const user = await response.json()
span.setAttributes({ 'user.found': true, 'user.name': user.name, 'http.status_code': response.status, })
return user } catch (error) { nextjsMetrics.errorCount.add(1, { error_type: 'fetch_error', operation: 'fetch_user', user_id: id, }) throw error } })}
export default async function UserPage({ params }: { params: { id: string } }) { // Record page view nextjsMetrics.pageViews.add(1, { page: `/users/${params.id}`, type: 'server_component', })
const user = await fetchUser(params.id)
if (!user) { notFound() }
return ( <div className="container mx-auto px-4 py-8"> <div className="max-w-2xl"> <h1 className="text-3xl font-bold mb-8">{user.name}</h1>
<div className="space-y-4"> <div> <h3 className="font-semibold">Email</h3> <p>{user.email}</p> </div>
<div> <h3 className="font-semibold">Phone</h3> <p>{user.phone}</p> </div>
<div> <h3 className="font-semibold">Website</h3> <p>{user.website}</p> </div>
<div> <h3 className="font-semibold">Company</h3> <p>{user.company.name}</p> <p className="text-sm text-gray-600">{user.company.catchPhrase}</p> </div> </div> </div> </div> )}
// Static generation with tracingexport async function generateStaticParams() { return withTracing('users.generate_static_params', async (span) => { // Generate first 10 user IDs for static generation const params = Array.from({ length: 10 }, (_, i) => ({ id: (i + 1).toString() }))
span.setAttributes({ 'static_params.count': params.length, 'static_params.type': 'user_ids', })
return params })}API Routes
import { NextRequest, NextResponse } from "next/server";import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";
export async function GET(request: NextRequest) { return withTracing("api.users.get", async (span) => { const startTime = Date.now();
try { // Parse query parameters const { searchParams } = new URL(request.url); const limit = searchParams.get("limit") || "10"; const page = searchParams.get("page") || "1";
span.setAttributes({ "api.operation": "list_users", "api.limit": parseInt(limit), "api.page": parseInt(page), });
// Simulate database query await new Promise((resolve) => setTimeout(resolve, 100));
const users = Array.from({ length: parseInt(limit) }, (_, i) => ({ id: i + 1 + (parseInt(page) - 1) * parseInt(limit), name: `User ${i + 1}`, email: `user${i + 1}@example.com`, }));
span.setAttributes({ "users.count": users.length, "response.size": JSON.stringify(users).length, });
// Record metrics const duration = Date.now() - startTime; nextjsMetrics.apiCalls.add(1, { method: "GET", route: "/api/users", status_code: "200", });
nextjsMetrics.renderDuration.record(duration, { component: "api_users", operation: "list", });
return NextResponse.json({ users, pagination: { page: parseInt(page), limit: parseInt(limit), total: users.length, }, }); } catch (error) { span.recordException(error as Error);
nextjsMetrics.errorCount.add(1, { error_type: "api_error", operation: "list_users", });
return NextResponse.json( { error: "Failed to fetch users" }, { status: 500 }, ); } });}
export async function POST(request: NextRequest) { return withTracing("api.users.create", async (span) => { const startTime = Date.now();
try { const body = await request.json();
span.setAttributes({ "api.operation": "create_user", "user.email": body.email, "request.body_size": JSON.stringify(body).length, });
// Validate required fields if (!body.name || !body.email) { span.setAttributes({ "validation.failed": true, "validation.missing_fields": [ !body.name && "name", !body.email && "email", ] .filter(Boolean) .join(","), });
return NextResponse.json( { error: "Name and email are required" }, { status: 400 }, ); }
// Simulate user creation await new Promise((resolve) => setTimeout(resolve, 200));
const newUser = { id: Date.now(), name: body.name, email: body.email, createdAt: new Date().toISOString(), };
span.setAttributes({ "user.created": true, "user.id": newUser.id, });
// Record metrics const duration = Date.now() - startTime; nextjsMetrics.apiCalls.add(1, { method: "POST", route: "/api/users", status_code: "201", });
nextjsMetrics.renderDuration.record(duration, { component: "api_users", operation: "create", });
return NextResponse.json(newUser, { status: 201 }); } catch (error) { span.recordException(error as Error);
nextjsMetrics.errorCount.add(1, { error_type: "api_error", operation: "create_user", });
return NextResponse.json( { error: "Failed to create user" }, { status: 500 }, ); } });}import { NextRequest, NextResponse } from "next/server";import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";
export async function GET( request: NextRequest, { params }: { params: { id: string } },) { return withTracing("api.users.get_by_id", async (span) => { const startTime = Date.now();
try { const userId = params.id;
span.setAttributes({ "api.operation": "get_user", "user.id": userId, });
// Validate user ID if (!userId || isNaN(parseInt(userId))) { span.setAttributes({ "validation.failed": true, "validation.error": "invalid_user_id", });
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 }); }
// Simulate database query await new Promise((resolve) => setTimeout(resolve, 50));
// Simulate user not found for IDs > 100 if (parseInt(userId) > 100) { span.setAttributes({ "user.found": false, });
return NextResponse.json({ error: "User not found" }, { status: 404 }); }
const user = { id: parseInt(userId), name: `User ${userId}`, email: `user${userId}@example.com`, createdAt: new Date().toISOString(), };
span.setAttributes({ "user.found": true, "user.name": user.name, });
// Record metrics const duration = Date.now() - startTime; nextjsMetrics.apiCalls.add(1, { method: "GET", route: "/api/users/[id]", status_code: "200", });
nextjsMetrics.renderDuration.record(duration, { component: "api_user_detail", operation: "get", });
return NextResponse.json(user); } catch (error) { span.recordException(error as Error);
nextjsMetrics.errorCount.add(1, { error_type: "api_error", operation: "get_user", user_id: params.id, });
return NextResponse.json( { error: "Failed to fetch user" }, { status: 500 }, ); } });}Client-Side Instrumentation
Web Vitals and User Experience
import { getCLS, getFID, getFCP, getLCP, getTTFB, Metric } from "web-vitals";
// Custom function to send metrics to your analytics endpointfunction sendToAnalytics(metric: Metric, labels: Record<string, string> = {}) { // Send to your API endpoint or directly to Last9 fetch("/api/analytics/web-vitals", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, delta: metric.delta, rating: metric.rating, ...labels, }), }).catch(console.error);}
export function reportWebVitals() { const labels = { page: window.location.pathname, referrer: document.referrer || "direct", user_agent: navigator.userAgent, };
getCLS((metric) => sendToAnalytics(metric, labels)); getFID((metric) => sendToAnalytics(metric, labels)); getFCP((metric) => sendToAnalytics(metric, labels)); getLCP((metric) => sendToAnalytics(metric, labels)); getTTFB((metric) => sendToAnalytics(metric, labels));}import { NextRequest, NextResponse } from "next/server";import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";
export async function POST(request: NextRequest) { return withTracing("api.analytics.web_vitals", async (span) => { try { const metrics = await request.json();
span.setAttributes({ "web_vitals.metric": metrics.name, "web_vitals.value": metrics.value, "web_vitals.rating": metrics.rating, "web_vitals.page": metrics.page, });
// Record web vitals as custom metrics const metricName = `nextjs_web_vitals_${metrics.name.toLowerCase()}`;
// Create or get the metric counter/histogram const webVitalMetric = nextjsMetrics.renderDuration; // Reuse existing histogram
webVitalMetric.record(metrics.value, { metric_name: metrics.name, page: metrics.page, rating: metrics.rating, });
return NextResponse.json({ success: true }); } catch (error) { span.recordException(error as Error);
nextjsMetrics.errorCount.add(1, { error_type: "web_vitals_error", operation: "record_metrics", });
return NextResponse.json( { error: "Failed to record web vitals" }, { status: 500 }, ); } });}'use client'
import { useEffect } from 'react'import { reportWebVitals } from '@/lib/web-vitals'
export default function RootLayout({ children,}: { children: React.ReactNode}) { useEffect(() => { // Initialize web vitals reporting reportWebVitals() }, [])
return ( <html lang="en"> <body>{children}</body> </html> )}Pages Router Integration
import type { NextApiRequest, NextApiResponse } from "next";import { tracer, nextjsMetrics, withTracing } from "@/lib/telemetry";
export default async function handler( req: NextApiRequest, res: NextApiResponse,) { return withTracing("pages_api.legacy_users", async (span) => { const startTime = Date.now();
try { span.setAttributes({ "api.method": req.method, "api.route": "/api/legacy-users", "api.type": "pages_router", });
if (req.method === "GET") { // Simulate database query await new Promise((resolve) => setTimeout(resolve, 100));
const users = Array.from({ length: 5 }, (_, i) => ({ id: i + 1, name: `Legacy User ${i + 1}`, email: `legacy${i + 1}@example.com`, }));
span.setAttributes({ "users.count": users.length, operation: "list_users", });
// Record metrics const duration = Date.now() - startTime; nextjsMetrics.apiCalls.add(1, { method: "GET", route: "/api/legacy-users", status_code: "200", router_type: "pages", });
nextjsMetrics.renderDuration.record(duration, { component: "pages_api_users", operation: "list", });
res.status(200).json({ users }); } else { span.setAttributes({ "error.type": "method_not_allowed", });
res.setHeader("Allow", ["GET"]); res.status(405).end(`Method ${req.method} Not Allowed`); } } catch (error) { span.recordException(error as Error);
nextjsMetrics.errorCount.add(1, { error_type: "pages_api_error", operation: "legacy_users", });
res.status(500).json({ error: "Internal Server Error" }); } });}import { GetServerSideProps } from 'next'import { tracer, nextjsMetrics, withTracing } from '@/lib/telemetry'
interface User { id: number name: string email: string}
interface Props { users: User[]}
export default function LegacyUsersPage({ users }: Props) { return ( <div className="container mx-auto px-4 py-8"> <h1 className="text-3xl font-bold mb-8">Legacy Users</h1> <div className="grid gap-4"> {users.map((user) => ( <div key={user.id} className="p-4 border rounded"> <h3 className="font-bold">{user.name}</h3> <p className="text-gray-600">{user.email}</p> </div> ))} </div> </div> )}
export const getServerSideProps: GetServerSideProps = async (context) => { return withTracing('pages.legacy_users.get_server_side_props', async (span) => { const startTime = Date.now()
try { span.setAttributes({ 'pages.route': '/legacy-users', 'pages.type': 'server_side_props', })
// Simulate API call const users = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, name: `Legacy User ${i + 1}`, email: `legacy${i + 1}@example.com`, }))
span.setAttributes({ 'users.count': users.length, 'data_fetched': true, })
// Record metrics const duration = Date.now() - startTime nextjsMetrics.pageViews.add(1, { page: '/legacy-users', type: 'server_side_props', })
nextjsMetrics.renderDuration.record(duration, { component: 'legacy_users_page', operation: 'get_server_side_props', })
return { props: { users, }, }
} catch (error) { span.recordException(error as Error)
nextjsMetrics.errorCount.add(1, { error_type: 'ssr_error', page: '/legacy-users', })
// Return error props or redirect return { props: { users: [], }, } } })}Production Deployment
Environment Configuration
# OpenTelemetry ConfigurationOTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpointOTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_headerOTEL_SERVICE_NAME=nextjs-appOTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
# Next.js ConfigurationNODE_ENV=productionNEXT_TELEMETRY_DISABLED=1
# Optional: Custom configurationsDATABASE_URL=your-database-urlREDIS_URL=your-redis-urlFROM node:18-alpine AS depsRUN apk add --no-cache libc6-compatWORKDIR /app
COPY package.json package-lock.json ./RUN npm ci --only=production
FROM node:18-alpine AS builderWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
FROM node:18-alpine AS runnerWORKDIR /app
ENV NODE_ENV productionENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejsRUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000ENV HOSTNAME 0.0.0.0
# Health checkHEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]version: "3.8"
services: nextjs-app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header - OTEL_SERVICE_NAME=nextjs-app - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0 - DATABASE_URL=postgresql://user:pass@postgres:5432/nextjs_db - REDIS_URL=redis://redis:6379 depends_on: - postgres - redis healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 3
postgres: image: postgres:15-alpine environment: POSTGRES_DB: nextjs_db POSTGRES_USER: user POSTGRES_PASSWORD: pass volumes: - postgres_data:/var/lib/postgresql/data
redis: image: redis:7-alpine volumes: - redis_data:/data
volumes: postgres_data: redis_data:Testing the Integration
-
Start your application
npm run dev# or for productionnpm run build && npm start -
Test different routes
# App Router pagescurl http://localhost:3000/userscurl http://localhost:3000/users/1# API routescurl http://localhost:3000/api/userscurl http://localhost:3000/api/users/1# Pages Router (if implemented)curl http://localhost:3000/legacy-userscurl http://localhost:3000/api/legacy-users -
View telemetry in Last9
Check your Last9 dashboard for:
- Server-side rendering traces with Next.js specific attributes
- API route spans with proper context propagation
- Client-side web vitals metrics
- Database and external API call traces
- Custom business logic spans and metrics