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
141 changes: 140 additions & 1 deletion stellar-payment-platform/package-lock.json

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

1 change: 1 addition & 0 deletions stellar-payment-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"generic-pool": "^3.9.0",
"node-cron": "4.5.0",
"pdfkit": "^0.15.2",
"pino-http": "^11.0.0",
"prom-client": "^15.1.3",
"rate-limit-redis": "^4.2.0",
"redis": "^4.7.0",
Expand Down
10 changes: 5 additions & 5 deletions stellar-payment-platform/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const redisClient = process.env.REDIS_URL ? createClient({
url: process.env.REDIS_URL
}) : null;
if (redisClient) {
redisClient.connect().catch((err) => logger.error('Redis connection error:', err));
redisClient.connect().catch((err) => logger.error(err, 'Redis connection failed'));
}

const limiter = rateLimit({
Expand All @@ -94,6 +94,7 @@ const limiter = rateLimit({
app.use(cors(corsOptions));
app.use(limiter);
app.use(express.json({ limit: '10kb' }));
app.use(pinoHttp());
app.use((err, _req, res, next) => {
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
return res.status(400).json({ error: 'Malformed JSON payload' });
Expand Down Expand Up @@ -620,7 +621,7 @@ app.post('/register', idempotencyMiddleware(redisClient), async (req, res, next)
}

// Handle other errors
logger.error('Registration error:', error.message);
logger.error(error, 'Registration error');
const registrationError = new Error(`Registration verification failed: ${error.message}`);
registrationError.statusCode = 500;
return next(registrationError);
Expand Down Expand Up @@ -664,7 +665,7 @@ app.get('/lookup', async (req, res, next) => {
return res.json({ username: row.username, address });
} catch (err) { // <-- 1. Add (err) here
// 2. Add this console.log to print the exact reason Prisma is failing
logger.error("🚨 ACTUAL PRISMA ERROR:", err);
logger.error(err, 'Actual Prisma error');

const dbError = new Error('Database lookup failed');
dbError.statusCode = 500;
Expand Down Expand Up @@ -858,8 +859,7 @@ const gracefulShutdown = (server, prismaClient, signal) => {
try {
await prismaClient.$disconnect();
} catch (err) {
console.error('Error disconnecting Prisma during shutdown:', err);
logger.error('Error draining DB pool during shutdown:', err);
logger.error(err, 'Error draining DB pool during shutdown');
}
process.exit(0);
});
Expand Down
3 changes: 2 additions & 1 deletion stellar-payment-platform/src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const genericPool = require('generic-pool');
const { scheduleCleanupJob } = require('./cleanup-cron');
const { logger } = require('./logger');
const dotenv = require('dotenv');
const { logger } = require('./logger');

dotenv.config();

Expand Down Expand Up @@ -129,7 +130,7 @@ const poolAll = (sql, params) =>
);
logger.info(`Database pool initialised — max ${dbConfig.connectionLimit} connections, ${dbConfig.poolTimeout}s timeout`);
} catch (err) {
logger.error('Failed to initialise database schema:', err);
logger.error(err, 'Failed to initialise database schema');
process.exit(1);
}
})();
Expand Down
90 changes: 3 additions & 87 deletions stellar-payment-platform/src/logger.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,7 @@
const path = require('path');
const winston = require('winston');
require('winston-daily-rotate-file');
const pino = require('pino');

// #294 — Centralised Winston logger with rotating file transports.
// Writing to a single ever-growing file eventually exhausts disk space, so every
// file transport rotates daily *and* whenever the active file passes MAX_SIZE,
// keeping only MAX_FILES worth of history (older archives are gzipped/deleted).

// Logs live in <stellar-payment-platform>/logs by default. LOG_DIR can point the
// transports somewhere else (e.g. a mounted volume in Docker).
const LOG_DIR = process.env.LOG_DIR || path.join(__dirname, '..', 'logs');
const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug');
const MAX_SIZE = process.env.LOG_MAX_SIZE || '20m';
const MAX_FILES = process.env.LOG_MAX_FILES || '14d';

// Test runs should not litter the working tree with log files or console noise.
const IS_TEST = process.env.NODE_ENV === 'test';

// The transport creates LOG_DIR on demand, so no bootstrap mkdir is needed.
const rotateOptions = (filename, level) => ({
filename: path.join(LOG_DIR, filename),
datePattern: 'YYYY-MM-DD',
maxSize: MAX_SIZE,
maxFiles: MAX_FILES,
zippedArchive: true,
...(level ? { level } : {}),
});

const redactKeys = winston.format((info) => {
const S_KEY_REGEX = /S[A-Z2-7]{55}/g;
if (typeof info.message === 'string') {
info.message = info.message.replace(S_KEY_REGEX, '[REDACTED_SECRET_KEY]');
}
if (info.stack && typeof info.stack === 'string') {
info.stack = info.stack.replace(S_KEY_REGEX, '[REDACTED_SECRET_KEY]');
}
return info;
});

const fileFormat = winston.format.combine(
redactKeys(),
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);

const consoleFormat = winston.format.combine(
redactKeys(),
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, correlationId, stack }) => {
const trace = correlationId ? ` [Correlation ID: ${correlationId}]` : '';
return `${timestamp} ${level}:${trace} ${stack || message}`;
})
);

const fileTransports = IS_TEST
? []
: [
// Everything at LOG_LEVEL and above.
new winston.transports.DailyRotateFile(rotateOptions('application-%DATE%.log')),
// Errors again on their own, so incidents are easy to find.
new winston.transports.DailyRotateFile(rotateOptions('error-%DATE%.log', 'error')),
];

const transports = [
new winston.transports.Console({
format: consoleFormat,
silent: IS_TEST,
}),
...fileTransports,
];

const logger = winston.createLogger({
level: LOG_LEVEL,
format: fileFormat,
defaultMeta: { service: 'stellar-payment-platform' },
transports,
exitOnError: false,
});

// Surface rotation problems (permissions, full disk) instead of failing silently.
fileTransports.forEach((transport) => {
transport.on('error', (error) => {
console.error('[logger] rotating file transport error:', error.message);
});
});
const logger = pino({ level: LOG_LEVEL });

module.exports = { logger, fileTransports, LOG_DIR, MAX_SIZE, MAX_FILES };
module.exports = { logger };
Loading