Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ if (require.main === module) {
app.listen(3000, () => log.info('Equipchain API running'));
}

module.exports = app;
module.exports = app;
86 changes: 86 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const crypto = require('crypto');
const express = require('express');
const cors = require('cors');
const { trace } = require('@opentelemetry/api');
const { childLogger } = require('./config/logger');
const config = require('./config');
const routes = require('./routes');

const app = express();
const log = childLogger('http');

// Security middleware
app.use(cors());

// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Correlation ID and request logging middleware
app.use((req, res, next) => {
const correlationId = req.headers['x-correlation-id'] || crypto.randomUUID();
req.correlationId = correlationId;
res.setHeader('x-correlation-id', correlationId);

const activeSpan = trace.getActiveSpan();
if (activeSpan) {
activeSpan.setAttribute('correlation.id', correlationId);
}

const start = process.hrtime.bigint();

res.on('finish', () => {
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
log.info(
{
correlationId,
method: req.method,
url: req.originalUrl,
status: res.statusCode,
durationMs,
},
'request completed'
);
});

next();
});

// Mount routes
app.use('/', routes);

// Root route with project info
app.get('/', (req, res) => {
res.json({
project: 'Equipchain',
status: 'Monitoring Meters',
contract: config.contractId,
});
});

// 404 handler
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.originalUrl}`,
});
});

// Error handling middleware
app.use((err, req, res, next) => {
log.error(
{
correlationId: req.correlationId,
error: err.message,
stack: err.stack,
},
'request error'
);

res.status(err.status || 500).json({
error: err.name || 'Internal Server Error',
message: config.isProduction ? 'An error occurred' : err.message,
});
});

module.exports = app;
33 changes: 33 additions & 0 deletions src/config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require('dotenv').config();

const {
NODE_ENV = 'development',
PORT = '3000',
CONTRACT_ID = 'CB7PSJZALNWNX7NLOAM6LOEL4OJZMFPQZJMIYO522ZSACYWXTZIDEDSS',
LOG_LEVEL = 'info',
OTEL_SERVICE_NAME = 'equipchain-api',
} = process.env;

// Validate required environment variables for production
const isProduction = NODE_ENV === 'production';
const isTest = NODE_ENV === 'test';

if (isProduction) {
// Add any production-specific required variables here
// For example: JWT_SECRET, DATABASE_URL, etc.
// Currently no additional required variables for this project
}

const config = Object.freeze({
env: NODE_ENV,
port: parseInt(PORT, 10),
contractId: CONTRACT_ID,
logLevel: LOG_LEVEL,
otel: {
serviceName: OTEL_SERVICE_NAME,
},
isProduction,
isTest,
});

module.exports = config;
19 changes: 19 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();

// Import route modules here as they are created
// const authRoutes = require('./auth');
// const adminRoutes = require('./admin');
// const analyticsRoutes = require('./analytics');

// Mount routes under their respective prefixes
// router.use('/api/auth', authRoutes);
// router.use('/api/admin', adminRoutes);
// router.use('/api/analytics', analyticsRoutes);

// Health check route
router.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

module.exports = router;
115 changes: 115 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require('./config/tracing');
const http = require('http');
const { childLogger } = require('./config/logger');
const config = require('./config');
const app = require('./app');
const { initServices, shutdownServices } = require('./services');

const log = childLogger('server');

let server;
let isShuttingDown = false;

/**
* Start the HTTP server
*/
async function startServer() {
try {
// Initialize services
await initServices(app);

// Create HTTP server
server = http.createServer(app);

// Start listening
server.listen(config.port, () => {
log.info(
{
port: config.port,
env: config.env,
contractId: config.contractId,
},
'Equipchain API server started'
);
});

// Handle server errors
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
log.error({ port: config.port }, 'Port already in use');
} else {
log.error({ error }, 'Server error');
}
process.exit(1);
});
} catch (error) {
log.error({ error }, 'Failed to start server');
process.exit(1);
}
}

/**
* Gracefully shutdown the server
*/
async function gracefulShutdown(signal) {
if (isShuttingDown) {
log.warn('Shutdown already in progress, ignoring signal');
return;
}

isShuttingDown = true;
log.info({ signal }, 'Received shutdown signal, starting graceful shutdown');

// Stop accepting new connections
if (server) {
server.close(async (err) => {
if (err) {
log.error({ error: err }, 'Error closing server');
process.exit(1);
}

log.info('HTTP server closed');

try {
// Shutdown services
await shutdownServices();
log.info('Graceful shutdown complete');
process.exit(0);
} catch (error) {
log.error({ error }, 'Error during service shutdown');
process.exit(1);
}
});

// Force shutdown after timeout
setTimeout(() => {
log.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
} else {
process.exit(0);
}
}

// Register signal handlers
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
log.error({ error }, 'Uncaught exception');
gracefulShutdown('uncaughtException');
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
log.error({ reason, promise }, 'Unhandled promise rejection');
gracefulShutdown('unhandledRejection');
});

// Start server if this file is run directly
if (require.main === module) {
startServer();
}

module.exports = { startServer, gracefulShutdown };
82 changes: 82 additions & 0 deletions src/services/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const { childLogger } = require('../config/logger');
const log = childLogger('services');

// Service registry to track initialized services
const services = {
cache: null,
queue: null,
eventListener: null,
websocket: null,
};

/**
* Initialize all services in the correct dependency order
* @param {Object} app - Express application instance
*/
async function initServices(app) {
try {
log.info('Initializing services...');

// Initialize cache service (if implemented)
// services.cache = await initCache();
// log.info('Cache service initialized');

// Initialize queue service (if implemented)
// services.queue = await initQueue();
// log.info('Queue service initialized');

// Initialize event listener (if implemented)
// services.eventListener = await initEventListener();
// log.info('Event listener initialized');

// Initialize WebSocket (if implemented)
// services.websocket = await initWebSocket(app);
// log.info('WebSocket initialized');

log.info('All services initialized successfully');
} catch (error) {
log.error({ error }, 'Failed to initialize services');
throw error;
}
}

/**
* Gracefully shutdown all services
*/
async function shutdownServices() {
try {
log.info('Shutting down services...');

// Shutdown services in reverse dependency order
if (services.websocket) {
await services.websocket.close();
log.info('WebSocket shutdown complete');
}

if (services.eventListener) {
await services.eventListener.stop();
log.info('Event listener shutdown complete');
}

if (services.queue) {
await services.queue.close();
log.info('Queue shutdown complete');
}

if (services.cache) {
await services.cache.quit();
log.info('Cache shutdown complete');
}

log.info('All services shutdown complete');
} catch (error) {
log.error({ error }, 'Error during service shutdown');
throw error;
}
}

module.exports = {
initServices,
shutdownServices,
services,
};
Loading
Loading