feat: enhance logging in PrismaClient for better query, warning, and error visibility
This commit is contained in:
@@ -10,6 +10,97 @@ import teamRouter from './routes/team.js';
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
// ── Logger ─────────────────────────────────────────────────────────────────────
|
||||||
|
const R = '\x1b[0m';
|
||||||
|
const BOLD = '\x1b[1m';
|
||||||
|
const DIM = '\x1b[2m';
|
||||||
|
const RED = '\x1b[31m';
|
||||||
|
const GREEN = '\x1b[32m';
|
||||||
|
const YELLOW = '\x1b[33m';
|
||||||
|
const BLUE = '\x1b[34m';
|
||||||
|
const MAGENTA = '\x1b[35m';
|
||||||
|
const CYAN = '\x1b[36m';
|
||||||
|
const WHITE = '\x1b[37m';
|
||||||
|
const BG_RED = '\x1b[41m';
|
||||||
|
const BG_GREEN = '\x1b[42m';
|
||||||
|
const BG_YELLOW = '\x1b[43m';
|
||||||
|
const BG_BLUE = '\x1b[44m';
|
||||||
|
const BG_MAGENTA = '\x1b[45m';
|
||||||
|
const BG_CYAN = '\x1b[46m';
|
||||||
|
|
||||||
|
const METHOD_STYLE: Record<string, string> = {
|
||||||
|
GET: `${BG_BLUE}${WHITE}${BOLD}`,
|
||||||
|
POST: `${BG_GREEN}${WHITE}${BOLD}`,
|
||||||
|
PUT: `${BG_YELLOW}${WHITE}${BOLD}`,
|
||||||
|
PATCH: `${BG_MAGENTA}${WHITE}${BOLD}`,
|
||||||
|
DELETE: `${BG_RED}${WHITE}${BOLD}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
function methodBadge(method: string): string {
|
||||||
|
const style = METHOD_STYLE[method] ?? `${BG_CYAN}${WHITE}${BOLD}`;
|
||||||
|
return `${style} ${method.padEnd(6)} ${R}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(code: number): string {
|
||||||
|
if (code < 300) return `${BG_GREEN}${WHITE}${BOLD} ${code} ${R}`;
|
||||||
|
if (code < 400) return `${BG_YELLOW}${WHITE}${BOLD} ${code} ${R}`;
|
||||||
|
return `${BG_RED}${WHITE}${BOLD} ${code} ${R}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyJson(value: unknown): string {
|
||||||
|
return JSON.stringify(value, (k, v) => k === 'password' ? '***' : v, 2)
|
||||||
|
.split('\n')
|
||||||
|
.map((line, i) => i === 0 ? line : ` ${DIM} ${line}${R}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEP = `${DIM}${'─'.repeat(60)}${R}`;
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const ts = new Date().toISOString().replace('T', ' ').slice(0, 23);
|
||||||
|
|
||||||
|
// Skip health check noise
|
||||||
|
if (req.originalUrl === '/api/health') { next(); return; }
|
||||||
|
|
||||||
|
const originalJson = res.json.bind(res);
|
||||||
|
let resBody: unknown;
|
||||||
|
res.json = (body) => { resBody = body; return originalJson(body); };
|
||||||
|
|
||||||
|
res.on('finish', () => {
|
||||||
|
const ms = Date.now() - start;
|
||||||
|
const userId = req.user?.userId ? `${CYAN}${req.user.userId.slice(0, 8)}…${R}` : `${DIM}anon${R}`;
|
||||||
|
const role = req.user?.role ? `${MAGENTA}${req.user.role}${R}` : `${DIM}-${R}`;
|
||||||
|
|
||||||
|
const hasBody = ['POST', 'PUT', 'PATCH'].includes(req.method)
|
||||||
|
&& req.body && Object.keys(req.body).length > 0;
|
||||||
|
|
||||||
|
const lines: string[] = [
|
||||||
|
SEP,
|
||||||
|
`${DIM}${ts}${R} ${methodBadge(req.method)} ${BOLD}${req.originalUrl}${R}`,
|
||||||
|
` ${DIM}┌ user ${R} ${userId} ${DIM}role:${R} ${role}`,
|
||||||
|
` ${DIM}└ status ${R} ${statusBadge(res.statusCode)} ${DIM}${ms}ms${R}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (hasBody) {
|
||||||
|
lines.push(` ${GREEN}↑ REQUEST BODY${R}`);
|
||||||
|
lines.push(` ${DIM} ${prettyJson(req.body)}${R}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode >= 400 && resBody) {
|
||||||
|
lines.push(` ${RED}↓ ERROR RESPONSE${R}`);
|
||||||
|
lines.push(` ${DIM} ${prettyJson(resBody)}${R}`);
|
||||||
|
} else if (res.statusCode < 300 && resBody && req.method !== 'GET') {
|
||||||
|
lines.push(` ${GREEN}↓ RESPONSE BODY${R}`);
|
||||||
|
lines.push(` ${DIM} ${prettyJson(resBody)}${R}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(lines.join('\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: [
|
origin: [
|
||||||
'http://localhost:5173', // nest-front dev
|
'http://localhost:5173', // nest-front dev
|
||||||
@@ -37,7 +128,7 @@ app.use((_req, res) => res.status(404).json({ error: 'Not found' }));
|
|||||||
|
|
||||||
// Global error handler
|
// Global error handler
|
||||||
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||||
console.error(err);
|
console.error(`${BG_RED}${WHITE}${BOLD} UNHANDLED ERROR ${R}`, err);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient({
|
||||||
|
log: [
|
||||||
|
{ emit: 'stdout', level: 'query' },
|
||||||
|
{ emit: 'stdout', level: 'warn' },
|
||||||
|
{ emit: 'stdout', level: 'error' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
export default prisma;
|
export default prisma;
|
||||||
|
|||||||
Reference in New Issue
Block a user