Tutorial: Integrating with Node.js/Express

This tutorial shows you how to integrate AIA with a Node.js/Express application.

#Prerequisites

  • Node.js 18+ or Bun
  • Express application
  • AIA installed and running (Install Guide)
  • GitHub repository for your app

#Step 1: Install OpenTelemetry

Terminal
bun add @opentelemetry/api @opentelemetry/sdk-node \ @opentelemetry/auto-instrumentations-node \ @opentelemetry/exporter-trace-otlp-http

#Step 2: Create Instrumentation File

Create instrumentation.ts:

Terminal
import { NodeSDK } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces', }), instrumentations: [getNodeAutoInstrumentations()], serviceName: process.env.OTEL_SERVICE_NAME || 'my-express-app', }); sdk.start(); // Graceful shutdown process.on('SIGTERM', () => { sdk.shutdown() .then(() => console.log('Tracing terminated')) .catch((error) => console.log('Error terminating tracing', error)) .finally(() => process.exit(0)); });

#Step 3: Update Your Server

Update your main server file (server.ts or index.ts):

Terminal
import express from 'express'; const app = express(); const port = process.env.PORT || 3000; app.use(express.json()); // Your routes app.get('/api/users', async (req, res) => { try { const users = await db.query('SELECT * FROM users'); res.json(users); } catch (error) { // Error automatically captured by OTEL console.error('Failed to fetch users:', error); res.status(500).json({ error: 'Failed to fetch users' }); } }); // Test error endpoint app.get('/api/test-error', (req, res) => { throw new Error('Test error for AIA demo'); }); // Error handler (important for OTEL to capture errors) app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Unhandled error:', err); res.status(500).json({ error: err.message }); }); app.listen(port, () => { console.log(`Server running on port ${port}`); });

#Step 4: Configure Environment

Create .env:

Terminal
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 OTEL_SERVICE_NAME=my-express-app PORT=3000

#Step 5: Update package.json

Add start script that loads instrumentation:

Terminal
{ "scripts": { "dev": "bun run -r ./instrumentation.ts server.ts", "start": "node -r ./instrumentation.js server.js" } }

#Step 6: Test Integration

Terminal
# Start your app bun run dev # In another terminal, trigger error curl http://localhost:3000/api/test-error

#Step 7: Verify in Dashboard

  1. Open AIA Dashboard: http://localhost:3000 (AIA dashboard, not your app)
  2. See the incident appear
  3. Wait for autopsy analysis
  4. Check GitHub for PR

#Common Patterns

Database Errors

Terminal
app.get('/api/users/:id', async (req, res) => { try { const user = await db.query( 'SELECT * FROM users WHERE id = ?', [req.params.id] ); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); } catch (error) { // Captured by OTEL console.error('Database error:', error); res.status(500).json({ error: 'Database error' }); } });

Async Errors

Terminal
app.post('/api/process', async (req, res) => { try { const result = await processData(req.body); res.json(result); } catch (error) { // Captured by OTEL console.error('Processing error:', error); res.status(500).json({ error: 'Processing failed' }); } });

Middleware Errors

Terminal
const authMiddleware = async (req, res, next) => { try { const token = req.headers.authorization; const user = await verifyToken(token); req.user = user; next(); } catch (error) { // Captured by OTEL res.status(401).json({ error: 'Unauthorized' }); } }; app.get('/api/protected', authMiddleware, (req, res) => { res.json({ user: req.user }); });

#Production Deployment

Docker

Create Dockerfile:

Terminal
FROM oven/bun:latest WORKDIR /app COPY package.json bun.lockb ./ RUN bun install --production COPY . . ENV OTEL_EXPORTER_OTLP_ENDPOINT=https://your-aia-agent.example.com ENV OTEL_SERVICE_NAME=my-express-app CMD ["bun", "run", "-r", "./instrumentation.ts", "server.ts"]

PM2

Create ecosystem.config.js:

Terminal
module.exports = { apps: [{ name: 'my-express-app', script: 'server.ts', interpreter: 'bun', node_args: '-r ./instrumentation.ts', env: { OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:4318', OTEL_SERVICE_NAME: 'my-express-app', PORT: 3000 }, env_production: { OTEL_EXPORTER_OTLP_ENDPOINT: 'https://your-aia-agent.example.com', OTEL_SERVICE_NAME: 'my-express-app', PORT: 3000 } }] };

Start with PM2:

Terminal
pm2 start ecosystem.config.js --env production

#Troubleshooting

No traces received

Check:

  1. Instrumentation is loaded before Express: bun run -r ./instrumentation.ts server.ts
  2. OTEL endpoint is correct: http://localhost:4318
  3. AIA Agent is running
  4. Environment variables are set

Traces sent but no incidents

Check:

  1. Error status code is 500+
  2. Error is thrown (not just logged)
  3. Error handler is in place
  4. Check AIA Agent logs

Performance impact

OTEL has minimal overhead (~1-2% CPU):

  • Traces are sent asynchronously
  • Sampling can be configured
  • Only errors trigger AIA analysis

#Advanced Configuration

Custom Spans

Terminal
import { trace } from '@opentelemetry/api'; const tracer = trace.getTracer('my-app'); app.get('/api/complex', async (req, res) => { const span = tracer.startSpan('complex-operation'); try { const result = await performComplexOperation(); span.setStatus({ code: 0 }); // OK res.json(result); } catch (error) { span.setStatus({ code: 2 }); // ERROR span.recordException(error); throw error; } finally { span.end(); } });

Sampling

Terminal
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base'; const sdk = new NodeSDK({ // ... other config sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10% of traces });

#Next Steps