From da3fc85f2ff3797fbab3da568a47c276f173e322 Mon Sep 17 00:00:00 2001 From: Ameer-5-5 Date: Sat, 25 Jul 2026 19:18:40 +0000 Subject: [PATCH] feat: standardize server logs as JSON with pino - Replace Winston with pino in src/logger.js - Add pino-http middleware to Express app - Replace console.log/error calls with pino logger in server.js and src/db.js - Install pino-http dependency --- stellar-payment-platform/package-lock.json | 142 ++++++++++++++++++++- stellar-payment-platform/package.json | 1 + stellar-payment-platform/server.js | 15 ++- stellar-payment-platform/src/db.js | 5 +- stellar-payment-platform/src/logger.js | 77 +---------- 5 files changed, 157 insertions(+), 83 deletions(-) diff --git a/stellar-payment-platform/package-lock.json b/stellar-payment-platform/package-lock.json index b2d4279..f20b3e6 100644 --- a/stellar-payment-platform/package-lock.json +++ b/stellar-payment-platform/package-lock.json @@ -22,6 +22,7 @@ "generic-pool": "^3.9.0", "node-cron": "4.5.0", "pdfkit": "^0.15.2", + "pino-http": "^11.0.0", "rate-limit-redis": "^4.2.0", "redis": "^4.7.0", "sqlite3": "^5.1.7", @@ -1520,6 +1521,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@prisma/client": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", @@ -2102,6 +2109,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4233,7 +4249,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6554,6 +6569,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6804,6 +6828,55 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-http": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-11.0.0.tgz", + "integrity": "sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==", + "license": "MIT", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^10.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -6937,6 +7010,22 @@ } } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -7037,6 +7126,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7143,6 +7238,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", @@ -7672,6 +7776,15 @@ "license": "MIT", "optional": true }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7693,6 +7806,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8063,6 +8185,24 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", diff --git a/stellar-payment-platform/package.json b/stellar-payment-platform/package.json index 08c1cd7..ef05532 100644 --- a/stellar-payment-platform/package.json +++ b/stellar-payment-platform/package.json @@ -33,6 +33,7 @@ "generic-pool": "^3.9.0", "node-cron": "4.5.0", "pdfkit": "^0.15.2", + "pino-http": "^11.0.0", "rate-limit-redis": "^4.2.0", "redis": "^4.7.0", "sqlite3": "^5.1.7", diff --git a/stellar-payment-platform/server.js b/stellar-payment-platform/server.js index e6d2c05..c0220ed 100644 --- a/stellar-payment-platform/server.js +++ b/stellar-payment-platform/server.js @@ -17,6 +17,8 @@ const {verifyMultiSignerThreshold,} = require('./src/multisigner-verifier'); const { poolGet, poolRun, poolAll } = require('./src/db'); const xss = require('xss'); const { Keypair, StrKey } = require('@stellar/stellar-sdk'); +const pinoHttp = require('pino-http'); +const { logger } = require('./src/logger'); dotenv.config(); @@ -61,7 +63,7 @@ const redisClient = process.env.REDIS_URL ? createClient({ url: process.env.REDIS_URL }) : null; if (redisClient) { - redisClient.connect().catch(console.error); + redisClient.connect().catch((err) => logger.error(err, 'Redis connection failed')); } const limiter = rateLimit({ @@ -78,6 +80,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' }); @@ -566,7 +569,7 @@ app.post('/register', idempotencyMiddleware(redisClient), async (req, res, next) } // Handle other errors - console.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); @@ -610,7 +613,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 - console.error("🚨 ACTUAL PRISMA ERROR:", err); + logger.error(err, 'Actual Prisma error'); const dbError = new Error('Database lookup failed'); dbError.statusCode = 500; @@ -781,7 +784,7 @@ const gracefulShutdown = (server, pool, signal) => { await pool.drain(); await pool.clear(); } catch (err) { - console.error('Error draining DB pool during shutdown:', err); + logger.error(err, 'Error draining DB pool during shutdown'); } process.exit(0); }); @@ -790,12 +793,12 @@ const gracefulShutdown = (server, pool, signal) => { if (require.main === module) { const server = app.listen(PORT, '0.0.0.0', () => { - console.log(`Server successfully initialized on port ${PORT}`); + logger.info(`Server successfully initialized on port ${PORT}`); }); server.on('error', (e) => { if (e.code === 'EADDRINUSE') { - console.error(`Port ${PORT} is in use, forcing shutdown so Railway can restart cleanly.`); + logger.error(`Port ${PORT} is in use, forcing shutdown so Railway can restart cleanly.`); process.exit(1); } }); diff --git a/stellar-payment-platform/src/db.js b/stellar-payment-platform/src/db.js index 48c98c5..f666e5c 100644 --- a/stellar-payment-platform/src/db.js +++ b/stellar-payment-platform/src/db.js @@ -6,6 +6,7 @@ const sqlite3 = require('sqlite3').verbose(); const genericPool = require('generic-pool'); const { scheduleCleanupJob } = require('./cleanup-cron'); const dotenv = require('dotenv'); +const { logger } = require('./logger'); dotenv.config(); @@ -126,9 +127,9 @@ const poolAll = (sql, params) => )`, [], ); - console.log(`Database pool initialised — max ${dbConfig.connectionLimit} connections, ${dbConfig.poolTimeout}s timeout`); + logger.info(`Database pool initialised — max ${dbConfig.connectionLimit} connections, ${dbConfig.poolTimeout}s timeout`); } catch (err) { - console.error('Failed to initialise database schema:', err); + logger.error(err, 'Failed to initialise database schema'); process.exit(1); } })(); diff --git a/stellar-payment-platform/src/logger.js b/stellar-payment-platform/src/logger.js index ea32bd2..b6061d5 100644 --- a/stellar-payment-platform/src/logger.js +++ b/stellar-payment-platform/src/logger.js @@ -1,78 +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 /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 fileFormat = winston.format.combine( - winston.format.timestamp(), - winston.format.errors({ stack: true }), - winston.format.json() -); - -const consoleFormat = winston.format.combine( - 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 };