feat(logging): enhance logging functionality with detailed request and error logging
Deploy Application / deploy (push) Successful in 46s Details

This commit is contained in:
faisolavolut 2025-12-10 14:01:36 +07:00
parent 7b08aafa72
commit d3c269505a
6 changed files with 179 additions and 30 deletions

View File

@ -320,11 +320,11 @@ model transactions_lines {
line_no Int @default(1) line_no Int @default(1)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
c_payment_id Decimal? @db.Decimal(10, 0) c_payment_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(255)
date DateTime? @db.Timestamptz(6) date DateTime? @db.Timestamptz(6)
invoicepartner_id String? @db.Uuid invoicepartner_id String? @db.Uuid
documentno String? @db.VarChar(255)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction) database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
transactions transactions @relation(fields: [transaction_id], references: [id], onDelete: NoAction, onUpdate: NoAction) transactions transactions @relation(fields: [transaction_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
} }
@ -342,19 +342,19 @@ model invoicepartner {
id String @id(map: "invoicePartner_pkey") @default(dbgenerated("gen_random_uuid()")) @db.Uuid id String @id(map: "invoicePartner_pkey") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
costumerno String @db.VarChar(50) costumerno String @db.VarChar(50)
c_bpartner_id Decimal? @db.Decimal(10, 0) c_bpartner_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(30)
db_id String? @db.Uuid db_id String? @db.Uuid
c_invoice_id Decimal? @db.Decimal(10, 0) c_invoice_id Decimal? @db.Decimal(10, 0)
is_active Boolean @default(true) is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6)
is_pay Boolean @default(false) is_pay Boolean @default(false)
grandtotal Decimal? @db.Decimal(20, 2)
amount Decimal? @db.Decimal(20, 2) amount Decimal? @db.Decimal(20, 2)
rv_openitem_id String? @db.Uuid rv_openitem_id String? @db.Uuid
documentno String? @db.VarChar(30)
grandtotal Decimal? @db.Decimal(20, 2)
invoiceLines invoice_lines[]
rv_openitem rv_openitem? @relation(fields: [rv_openitem_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "invoicePartner_rv_openitem_id_fkey") rv_openitem rv_openitem? @relation(fields: [rv_openitem_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "invoicePartner_rv_openitem_id_fkey")
transactionsLines transactions_lines[] transactionsLines transactions_lines[]
invoiceLines invoice_lines[]
} }
model invoice { model invoice {
@ -390,7 +390,6 @@ model invoice_lines {
description String? @db.VarChar(255) description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2) amount Decimal? @db.Decimal(20, 2)
c_invoice_id Decimal? @db.Decimal(10, 0) c_invoice_id Decimal? @db.Decimal(10, 0)
invoicepartner_id String? @db.Uuid
invoice_id String? @db.Uuid invoice_id String? @db.Uuid
db_id String? @db.Uuid db_id String? @db.Uuid
created_at DateTime? @default(now()) @db.Timestamptz(6) created_at DateTime? @default(now()) @db.Timestamptz(6)
@ -400,9 +399,10 @@ model invoice_lines {
line_no Int @default(1) line_no Int @default(1)
billcode String? @db.VarChar(10) billcode String? @db.VarChar(10)
billname String? @db.VarChar(50) billname String? @db.VarChar(50)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id]) invoicepartner_id String? @db.Uuid
database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction) database database? @relation(fields: [db_id], references: [db_id], onDelete: NoAction, onUpdate: NoAction)
invoice invoice? @relation(fields: [invoice_id], references: [id]) invoice invoice? @relation(fields: [invoice_id], references: [id])
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id])
} }
model sequences { model sequences {
@ -424,3 +424,21 @@ model whitelistcors {
is_active String @default("Y") @db.Char(1) is_active String @default("Y") @db.Char(1)
tenants tenants @relation(fields: [tenant_id], references: [tenant_id], onDelete: NoAction, onUpdate: NoAction) tenants tenants @relation(fields: [tenant_id], references: [tenant_id], onDelete: NoAction, onUpdate: NoAction)
} }
model roles {
role_id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String @unique @db.VarChar(100)
description String? @db.VarChar(255)
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
}
model user_roles {
user_role_id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @db.Uuid
role_id String @db.Uuid
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
}

View File

@ -1,6 +1,13 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import prisma from "../prisma"; import prisma from "../prisma";
import { hash } from "bcryptjs"; import { hash } from "bcryptjs";
import { createLogger, LogLevel } from "../lib/logger";
// Logger untuk database controller
const dbLogger = createLogger({
logDir: "./logs/log",
minLevel: LogLevel.INFO,
});
type DbRequest = { type DbRequest = {
table: string; table: string;
@ -11,12 +18,22 @@ type DbRequest = {
export class DbController { export class DbController {
async handle(req: Request, res: Response) { async handle(req: Request, res: Response) {
const body: DbRequest = req.body; const body: DbRequest = req.body;
if (!body || !body.table || !body.action) if (!body || !body.table || !body.action) {
dbLogger.warning("Invalid DB request - missing table or action", {
body,
// ip: req.ip,
});
return res.status(400).json({ error: "table and action required" }); return res.status(400).json({ error: "table and action required" });
}
const model = (prisma as any)[body.table]; const model = (prisma as any)[body.table];
if (!model) if (!model) {
dbLogger.warning("Unknown table access attempt", {
table: body.table,
ip: req.ip,
});
return res.status(400).json({ error: `unknown table ${body.table}` }); return res.status(400).json({ error: `unknown table ${body.table}` });
}
// Only allow safe actions // Only allow safe actions
const allowed = [ const allowed = [
@ -29,8 +46,14 @@ export class DbController {
"upsert", "upsert",
"count", "count",
]; ];
if (!allowed.includes(body.action)) if (!allowed.includes(body.action)) {
dbLogger.warning("Disallowed action attempt", {
table: body.table,
action: body.action,
ip: req.ip,
});
return res.status(400).json({ error: "action not allowed" }); return res.status(400).json({ error: "action not allowed" });
}
// Tables that require access checks // Tables that require access checks
const protectedTables = new Set([ const protectedTables = new Set([
@ -91,6 +114,11 @@ export class DbController {
if (body.table === "users" && body.action === "create") { if (body.table === "users" && body.action === "create") {
const payload = body.data?.data || body.data || {}; const payload = body.data?.data || body.data || {};
if (!payload.username || !payload.password) { if (!payload.username || !payload.password) {
dbLogger.warning("User creation missing credentials", {
hasUsername: !!payload.username,
hasPassword: !!payload.password,
ip: req.ip,
});
return res.status(400).json({ return res.status(400).json({
error: "username and password are required for users.create", error: "username and password are required for users.create",
}); });
@ -100,8 +128,13 @@ export class DbController {
const existing = await prisma.users.findFirst({ const existing = await prisma.users.findFirst({
where: { username: payload.username }, where: { username: payload.username },
}); });
if (existing) if (existing) {
dbLogger.warning("Duplicate username creation attempt", {
username: payload.username,
ip: req.ip,
});
return res.status(409).json({ error: "username already exists" }); return res.status(409).json({ error: "username already exists" });
}
const hashed = await hash(payload.password, 10); const hashed = await hash(payload.password, 10);
const createData = { const createData = {
@ -112,6 +145,11 @@ export class DbController {
// Ensure payload is nested under data if using Prisma create syntax // Ensure payload is nested under data if using Prisma create syntax
const createArg = { data: createData }; const createArg = { data: createData };
const result = await model.create(createArg); const result = await model.create(createArg);
dbLogger.info("User created successfully", {
username: payload.username,
userId: result.user_id,
ip: req.ip,
});
return res.status(201).json({ result }); return res.status(201).json({ result });
} }
@ -120,10 +158,16 @@ export class DbController {
// Expecting Prisma update signature: { where: {...}, data: {...} } // Expecting Prisma update signature: { where: {...}, data: {...} }
const where = body.data?.where; const where = body.data?.where;
const data = body.data?.data || body.data; const data = body.data?.data || body.data;
if (!where || !data) if (!where || !data) {
dbLogger.warning("User update missing where or data", {
hasWhere: !!where,
hasData: !!data,
ip: req.ip,
});
return res return res
.status(400) .status(400)
.json({ error: "where and data are required for users.update" }); .json({ error: "where and data are required for users.update" });
}
// If username is being changed, ensure it's not taken by another user // If username is being changed, ensure it's not taken by another user
if (data.username) { if (data.username) {
@ -136,6 +180,10 @@ export class DbController {
existing.user_id !== where.id && existing.user_id !== where.id &&
existing.user_id !== where.userId existing.user_id !== where.userId
) { ) {
dbLogger.warning("Duplicate username update attempt", {
username: data.username,
ip: req.ip,
});
return res.status(409).json({ error: "username already exists" }); return res.status(409).json({ error: "username already exists" });
} }
} }
@ -147,13 +195,28 @@ export class DbController {
} }
const result = await model.update({ where, data }); const result = await model.update({ where, data });
dbLogger.info("User updated successfully", {
userId: result.user_id,
updatedFields: Object.keys(data),
ip: req.ip,
});
return res.json({ result }); return res.json({ result });
} }
const result = await model[body.action](body.data || {}); const result = await model[body.action](body.data || {});
dbLogger.info("Database operation completed", {
table: body.table,
action: body.action,
ip: req.ip,
});
return res.json({ result }); return res.json({ result });
} catch (err) { } catch (err) {
console.error("DB dispatch error", err); dbLogger.error("Database operation error", {
table: body.table,
action: body.action,
error: err instanceof Error ? err.message : String(err),
ip: req.ip,
});
return res.status(500).json({ return res.status(500).json({
error: "internal error", error: "internal error",
detail: err instanceof Error ? err.message : String(err), detail: err instanceof Error ? err.message : String(err),

View File

@ -184,11 +184,11 @@ export default class LogController {
files: allFiles, files: allFiles,
}; };
logAccessLogger.info("Log list accessed", { // logAccessLogger.info("Log list accessed", {
path: safePath, // path: safePath,
userId: (req as any).user?.user_id, // userId: (req as any).user?.user_id,
fileCount: result.files.length, // fileCount: result.files.length,
}); // });
return res.json(result); return res.json(result);
} catch (err) { } catch (err) {

68
src/lib/logger-example.ts Normal file
View File

@ -0,0 +1,68 @@
import { LogLevel, createLogger, getDefaultLogger } from "./logger";
// Example 1: Using default logger (logs to ./logs folder)
const defaultLog = getDefaultLogger();
console.log("=== Default Logger Example ===");
defaultLog.debug("a debug message");
defaultLog.info("a info message");
defaultLog.notice("a notice message");
defaultLog.warning("a warning message");
defaultLog.error("a error message");
defaultLog.critical("a critical message");
defaultLog.alert("a alert message");
defaultLog.emergency("a emergency message");
// Example 2: Custom logger with specific folder
const customLog = createLogger({
logDir: "./custom-logs",
minLevel: LogLevel.WARNING, // Only log WARNING and above
filePrefix: "payment-api",
});
console.log("\n=== Custom Logger Example ===");
customLog.debug("this will not be logged (below min level)");
customLog.info("this will not be logged (below min level)");
customLog.warning("a warning message that will be logged");
customLog.error("a error message that will be logged");
customLog.critical("a critical message that will be logged");
// Example 3: Logger with additional arguments
const apiLog = createLogger({
logDir: "./api-logs",
filePrefix: "mandiri-api",
maxFileSizeMB: 5,
});
console.log("\n=== API Logger Example with Objects ===");
apiLog.info("Payment received", {
amount: 100000,
virtualAccount: "1234567890",
timestamp: new Date(),
});
apiLog.error("Payment failed", {
error: "Insufficient balance",
virtualAccount: "1234567890",
attemptedAmount: 500000,
});
// Example 4: Different log levels with context
const bankLog = createLogger({
logDir: "./bank-integration",
filePrefix: "bank-sync",
});
console.log("\n=== Bank Integration Logger Example ===");
bankLog.debug("Starting database sync");
bankLog.info("Connected to bank API");
bankLog.notice("Processing 1000 transactions");
bankLog.warning("API rate limit approaching");
bankLog.error("Connection timeout, retrying...");
bankLog.critical("Multiple failures detected");
bankLog.alert("System performance degraded");
bankLog.emergency("All bank connections failed");
console.log(
"\nLogger examples completed. Check the log files in respective directories."
);

View File

@ -29,7 +29,7 @@ export class Logger {
logDir: config.logDir, logDir: config.logDir,
minLevel: config.minLevel ?? LogLevel.DEBUG, minLevel: config.minLevel ?? LogLevel.DEBUG,
maxFileSizeMB: config.maxFileSizeMB ?? 10, maxFileSizeMB: config.maxFileSizeMB ?? 10,
filePrefix: config.filePrefix ?? "app", filePrefix: config.filePrefix ?? "",
}; };
// Ensure log directory exists // Ensure log directory exists
@ -48,10 +48,10 @@ export class Logger {
if (this.currentDate !== dateStr) { if (this.currentDate !== dateStr) {
this.currentDate = dateStr; this.currentDate = dateStr;
this.currentLogFile = path.join( const fileName = this.config.filePrefix
this.config.logDir, ? `${this.config.filePrefix}-${dateStr}.log`
`${this.config.filePrefix}-${dateStr}.log` : `${dateStr}.log`;
); this.currentLogFile = path.join(this.config.logDir, fileName);
} }
return this.currentLogFile!; return this.currentLogFile!;

View File

@ -89,12 +89,12 @@ export async function authenticateLogAccess(
session_token: token, session_token: token,
}; };
authLogger.info("Log access authenticated", { // authLogger.info("Log access authenticated", {
userId: req.user.user_id, // userId: req.user.user_id,
username: req.user.username, // username: req.user.username,
ip: req.ip, // ip: req.ip,
path: req.path, // path: req.path,
}); // });
next(); next();
} catch (err) { } catch (err) {
@ -201,7 +201,7 @@ export function auditLogAccess() {
}, },
}); });
authLogger.info("Log access audit", auditData); // authLogger.info("Log access audit", auditData);
} catch (err) { } catch (err) {
authLogger.error("Failed to create audit log", { authLogger.error("Failed to create audit log", {
error: err instanceof Error ? err.message : String(err), error: err instanceof Error ? err.message : String(err),