Skip to content
Merged
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
17 changes: 10 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"node-cron": "^3.0.3",
"stellar-sdk": "^11.3.0",
"winston": "^3.14.0",
"zod": "^4.4.3"
"zod": "^4.4.3",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const logger = require('./logger');
const cache = require('./services/cache');
const priceRefreshJob = require('./jobs/priceRefresh');
const buildCorsMiddleware = require('./middleware/cors');
const { requestIdMiddleware } = require('./middleware/requestId');
const { requireApiKey } = require('./middleware/auth');
const pricesRouter = require('./routes/prices');
const alertsRouter = require('./routes/alerts');
Expand All @@ -14,6 +15,7 @@ const airdropsRouter = require('./routes/airdrops');

const app = express();

app.use(requestIdMiddleware);
app.use(helmet());
app.use(buildCorsMiddleware(config.corsAllowedOrigins));
app.use(express.json());
Expand Down Expand Up @@ -41,8 +43,10 @@ app.use((err, req, res, _next) => {
res.status(status).json({ error: err.message || 'Internal server error' });
});

let server;

if (require.main === module) {
const server = app.listen(config.port, () => {
server = app.listen(config.port, () => {
logger.info(`SmartDrop backend running on port ${config.port}`);
priceRefreshJob.start();
});
Expand Down
8 changes: 8 additions & 0 deletions src/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const { name: serviceName, version } = require('../package.json');
const { requestContext } = require('./middleware/requestId');

// ==================== LOG LEVEL ====================
const getLogLevel = () => {
Expand Down Expand Up @@ -49,10 +50,17 @@ const env = process.env.NODE_ENV || 'development';
const logFormat = process.env.LOG_FORMAT || (env === 'production' ? 'json' : 'pretty');
const useJsonFormat = logFormat === 'json';

// ==================== REQUEST CONTEXT ====================
const requestIdFormat = winston.format((info) => {
info.requestId = requestContext.getStore()?.requestId ?? 'system';
return info;
});

// ==================== BASE FORMATS ====================
const baseFormats = [
winston.format.timestamp({ format: () => new Date().toISOString() }),
winston.format.errors({ stack: true }),
requestIdFormat(),
redactFormat(),
];

Expand Down
17 changes: 17 additions & 0 deletions src/middleware/requestId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

const crypto = require('node:crypto');
const { AsyncLocalStorage } = require('node:async_hooks');

const requestContext = new AsyncLocalStorage();

function requestIdMiddleware(req, res, next) {
req.id = crypto.randomUUID();
res.setHeader('X-Request-ID', req.id);
requestContext.run({ requestId: req.id }, next);
}

module.exports = {
requestIdMiddleware,
requestContext,
};
118 changes: 118 additions & 0 deletions test/requestId.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use strict';

const express = require('express');
const request = require('supertest');
const { requestIdMiddleware, requestContext } = require('../src/middleware/requestId');

function buildTestApp(onRequest) {
const app = express();
app.use(requestIdMiddleware);
app.get('/test', (req, res) => {
onRequest(req);
res.json({ ok: true });
});
return app;
}

describe('requestId middleware', () => {
test('sets X-Request-ID response header on every response', async () => {
const app = buildTestApp(() => {});

const res = await request(app).get('/test');

expect(res.status).toBe(200);
expect(res.headers['x-request-id']).toBeDefined();
expect(res.headers['x-request-id']).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
);
});

test('attaches the same ID to req.id and the response header', async () => {
let capturedReqId;
const app = buildTestApp((req) => {
capturedReqId = req.id;
});

const res = await request(app).get('/test');

expect(capturedReqId).toBe(res.headers['x-request-id']);
});

test('runs downstream handlers inside AsyncLocalStorage context', async () => {
let storeRequestId;
const app = buildTestApp((req) => {
storeRequestId = requestContext.getStore()?.requestId;
expect(storeRequestId).toBe(req.id);
});

const res = await request(app).get('/test');

expect(storeRequestId).toBe(res.headers['x-request-id']);
});
});

describe('logger requestId correlation', () => {
let writeSpy;

beforeEach(() => {
jest.resetModules();
process.env.LOG_FORMAT = 'json';
process.env.LOG_LEVEL = 'info';
writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk, _encoding, cb) => {
if (typeof cb === 'function') cb();
return true;
});
});

afterEach(() => {
writeSpy.mockRestore();
});

function findLogLine(message) {
return writeSpy.mock.calls
.map(([chunk]) => chunk.toString())
.find((line) => line.includes(message));
}

test('log output includes matching requestId for a given request', async () => {
const { requestIdMiddleware: middleware } = require('../src/middleware/requestId');
const logger = require('../src/logger');

const app = express();
app.use(middleware);
app.get('/test', (req, res) => {
logger.info('Handling correlated request');
res.json({ ok: true });
});

const res = await request(app).get('/test');
const logLine = findLogLine('Handling correlated request');

expect(logLine).toBeDefined();
const parsed = JSON.parse(logLine);
expect(parsed.requestId).toBe(res.headers['x-request-id']);
});

test('background tasks log with requestId system', () => {
const logger = require('../src/logger');

logger.info('Background task running');

const logLine = findLogLine('Background task running');
expect(logLine).toBeDefined();
const parsed = JSON.parse(logLine);
expect(parsed.requestId).toBe('system');
});
});

describe('requestId on app routes', () => {
test('health endpoint returns X-Request-ID header', async () => {
jest.resetModules();
const { app } = require('../src/index');

const res = await request(app).get('/health');

expect(res.status).toBe(200);
expect(res.headers['x-request-id']).toBeDefined();
});
});
Loading