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)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
c_payment_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(255)
date DateTime? @db.Timestamptz(6)
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)
invoicepartner invoicepartner? @relation(fields: [invoicepartner_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
costumerno String @db.VarChar(50)
c_bpartner_id Decimal? @db.Decimal(10, 0)
documentno String? @db.VarChar(30)
db_id String? @db.Uuid
c_invoice_id Decimal? @db.Decimal(10, 0)
is_active Boolean @default(true)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
is_pay Boolean @default(false)
grandtotal Decimal? @db.Decimal(20, 2)
amount Decimal? @db.Decimal(20, 2)
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")
transactionsLines transactions_lines[]
invoiceLines invoice_lines[]
}
model invoice {
@ -390,7 +390,6 @@ model invoice_lines {
description String? @db.VarChar(255)
amount Decimal? @db.Decimal(20, 2)
c_invoice_id Decimal? @db.Decimal(10, 0)
invoicepartner_id String? @db.Uuid
invoice_id String? @db.Uuid
db_id String? @db.Uuid
created_at DateTime? @default(now()) @db.Timestamptz(6)
@ -400,9 +399,10 @@ model invoice_lines {
line_no Int @default(1)
billcode String? @db.VarChar(10)
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)
invoice invoice? @relation(fields: [invoice_id], references: [id])
invoicepartner invoicepartner? @relation(fields: [invoicepartner_id], references: [id])
}
model sequences {
@ -424,3 +424,21 @@ model whitelistcors {
is_active String @default("Y") @db.Char(1)
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 prisma from "../prisma";
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 = {
table: string;
@ -11,12 +18,22 @@ type DbRequest = {
export class DbController {
async handle(req: Request, res: Response) {
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" });
}
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}` });
}
// Only allow safe actions
const allowed = [
@ -29,8 +46,14 @@ export class DbController {
"upsert",
"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" });
}
// Tables that require access checks
const protectedTables = new Set([
@ -91,6 +114,11 @@ export class DbController {
if (body.table === "users" && body.action === "create") {
const payload = body.data?.data || body.data || {};
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({
error: "username and password are required for users.create",
});
@ -100,8 +128,13 @@ export class DbController {
const existing = await prisma.users.findFirst({
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" });
}
const hashed = await hash(payload.password, 10);
const createData = {
@ -112,6 +145,11 @@ export class DbController {
// Ensure payload is nested under data if using Prisma create syntax
const createArg = { data: createData };
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 });
}
@ -120,10 +158,16 @@ export class DbController {
// Expecting Prisma update signature: { where: {...}, data: {...} }
const where = body.data?.where;
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
.status(400)
.json({ error: "where and data are required for users.update" });
}
// If username is being changed, ensure it's not taken by another user
if (data.username) {
@ -136,6 +180,10 @@ export class DbController {
existing.user_id !== where.id &&
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" });
}
}
@ -147,13 +195,28 @@ export class DbController {
}
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 });
}
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 });
} 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({
error: "internal error",
detail: err instanceof Error ? err.message : String(err),

View File

@ -184,11 +184,11 @@ export default class LogController {
files: allFiles,
};
logAccessLogger.info("Log list accessed", {
path: safePath,
userId: (req as any).user?.user_id,
fileCount: result.files.length,
});
// logAccessLogger.info("Log list accessed", {
// path: safePath,
// userId: (req as any).user?.user_id,
// fileCount: result.files.length,
// });
return res.json(result);
} 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,
minLevel: config.minLevel ?? LogLevel.DEBUG,
maxFileSizeMB: config.maxFileSizeMB ?? 10,
filePrefix: config.filePrefix ?? "app",
filePrefix: config.filePrefix ?? "",
};
// Ensure log directory exists
@ -48,10 +48,10 @@ export class Logger {
if (this.currentDate !== dateStr) {
this.currentDate = dateStr;
this.currentLogFile = path.join(
this.config.logDir,
`${this.config.filePrefix}-${dateStr}.log`
);
const fileName = this.config.filePrefix
? `${this.config.filePrefix}-${dateStr}.log`
: `${dateStr}.log`;
this.currentLogFile = path.join(this.config.logDir, fileName);
}
return this.currentLogFile!;

View File

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