feat(logging): enhance logging functionality with detailed request and error logging
Deploy Application / deploy (push) Successful in 46s
Details
Deploy Application / deploy (push) Successful in 46s
Details
This commit is contained in:
parent
7b08aafa72
commit
d3c269505a
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
);
|
||||||
|
|
@ -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!;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue