ERP-node/backend-node/src/services/ddlAuditLogger.ts

369 lines
9.1 KiB
TypeScript

/**
* DDL 실행 감사 로깅 서비스
* 모든 DDL 실행을 추적하고 기록
*/
import { PrismaClient } from "@prisma/client";
import { logger } from "../utils/logger";
const prisma = new PrismaClient();
export class DDLAuditLogger {
/**
* DDL 실행 로그 기록
*/
static async logDDLExecution(
userId: string,
companyCode: string,
ddlType: "CREATE_TABLE" | "ADD_COLUMN" | "DROP_TABLE" | "DROP_COLUMN",
tableName: string,
ddlQuery: string,
success: boolean,
error?: string,
additionalInfo?: Record<string, any>
): Promise<void> {
try {
// DDL 실행 로그 데이터베이스에 저장
const logEntry = await prisma.$executeRaw`
INSERT INTO ddl_execution_log (
user_id,
company_code,
ddl_type,
table_name,
ddl_query,
success,
error_message,
executed_at
) VALUES (
${userId},
${companyCode},
${ddlType},
${tableName},
${ddlQuery},
${success},
${error || null},
NOW()
)
`;
// 추가 로깅 (파일 로그)
const logData = {
userId,
companyCode,
ddlType,
tableName,
success,
queryLength: ddlQuery.length,
error: error || null,
additionalInfo: additionalInfo || null,
timestamp: new Date().toISOString(),
};
if (success) {
logger.info("DDL 실행 성공", logData);
} else {
logger.error("DDL 실행 실패", { ...logData, ddlQuery });
}
// 중요한 DDL 실행은 별도 알림 (필요시)
if (ddlType === "CREATE_TABLE" || ddlType === "DROP_TABLE") {
logger.warn("중요 DDL 실행", {
...logData,
severity: "HIGH",
action: "TABLE_STRUCTURE_CHANGE",
});
}
} catch (logError) {
// 로그 기록 실패는 시스템에 영향을 주지 않도록 처리
logger.error("DDL 실행 로그 기록 실패:", {
originalUserId: userId,
originalDdlType: ddlType,
originalTableName: tableName,
originalSuccess: success,
logError: logError,
});
// 로그 기록 실패를 파일 로그로라도 남김
console.error("CRITICAL: DDL 로그 기록 실패", {
userId,
ddlType,
tableName,
success,
logError,
timestamp: new Date().toISOString(),
});
}
}
/**
* DDL 실행 시작 로그
*/
static async logDDLStart(
userId: string,
companyCode: string,
ddlType: "CREATE_TABLE" | "ADD_COLUMN" | "DROP_TABLE" | "DROP_COLUMN",
tableName: string,
requestData: any
): Promise<void> {
logger.info("DDL 실행 시작", {
userId,
companyCode,
ddlType,
tableName,
requestData,
timestamp: new Date().toISOString(),
});
}
/**
* 최근 DDL 실행 로그 조회
*/
static async getRecentDDLLogs(
limit: number = 50,
userId?: string,
ddlType?: string
): Promise<any[]> {
try {
let whereClause = "WHERE 1=1";
const params: any[] = [];
if (userId) {
whereClause += " AND user_id = $" + (params.length + 1);
params.push(userId);
}
if (ddlType) {
whereClause += " AND ddl_type = $" + (params.length + 1);
params.push(ddlType);
}
const query = `
SELECT
id,
user_id,
company_code,
ddl_type,
table_name,
success,
error_message,
executed_at,
CASE
WHEN LENGTH(ddl_query) > 100 THEN SUBSTRING(ddl_query, 1, 100) || '...'
ELSE ddl_query
END as ddl_query_preview
FROM ddl_execution_log
${whereClause}
ORDER BY executed_at DESC
LIMIT $${params.length + 1}
`;
params.push(limit);
const logs = await prisma.$queryRawUnsafe(query, ...params);
return logs as any[];
} catch (error) {
logger.error("DDL 로그 조회 실패:", error);
return [];
}
}
/**
* DDL 실행 통계 조회
*/
static async getDDLStatistics(
fromDate?: Date,
toDate?: Date
): Promise<{
totalExecutions: number;
successfulExecutions: number;
failedExecutions: number;
byDDLType: Record<string, number>;
byUser: Record<string, number>;
recentFailures: any[];
}> {
try {
let dateFilter = "";
const params: any[] = [];
if (fromDate) {
dateFilter += " AND executed_at >= $" + (params.length + 1);
params.push(fromDate);
}
if (toDate) {
dateFilter += " AND executed_at <= $" + (params.length + 1);
params.push(toDate);
}
// 전체 통계
const totalStats = (await prisma.$queryRawUnsafe(
`
SELECT
COUNT(*) as total_executions,
SUM(CASE WHEN success = true THEN 1 ELSE 0 END) as successful_executions,
SUM(CASE WHEN success = false THEN 1 ELSE 0 END) as failed_executions
FROM ddl_execution_log
WHERE 1=1 ${dateFilter}
`,
...params
)) as any[];
// DDL 타입별 통계
const ddlTypeStats = (await prisma.$queryRawUnsafe(
`
SELECT ddl_type, COUNT(*) as count
FROM ddl_execution_log
WHERE 1=1 ${dateFilter}
GROUP BY ddl_type
ORDER BY count DESC
`,
...params
)) as any[];
// 사용자별 통계
const userStats = (await prisma.$queryRawUnsafe(
`
SELECT user_id, COUNT(*) as count
FROM ddl_execution_log
WHERE 1=1 ${dateFilter}
GROUP BY user_id
ORDER BY count DESC
LIMIT 10
`,
...params
)) as any[];
// 최근 실패 로그
const recentFailures = (await prisma.$queryRawUnsafe(
`
SELECT
user_id,
ddl_type,
table_name,
error_message,
executed_at
FROM ddl_execution_log
WHERE success = false ${dateFilter}
ORDER BY executed_at DESC
LIMIT 10
`,
...params
)) as any[];
const stats = totalStats[0];
return {
totalExecutions: parseInt(stats.total_executions) || 0,
successfulExecutions: parseInt(stats.successful_executions) || 0,
failedExecutions: parseInt(stats.failed_executions) || 0,
byDDLType: ddlTypeStats.reduce((acc, row) => {
acc[row.ddl_type] = parseInt(row.count);
return acc;
}, {}),
byUser: userStats.reduce((acc, row) => {
acc[row.user_id] = parseInt(row.count);
return acc;
}, {}),
recentFailures,
};
} catch (error) {
logger.error("DDL 통계 조회 실패:", error);
return {
totalExecutions: 0,
successfulExecutions: 0,
failedExecutions: 0,
byDDLType: {},
byUser: {},
recentFailures: [],
};
}
}
/**
* 특정 테이블의 DDL 히스토리 조회
*/
static async getTableDDLHistory(tableName: string): Promise<any[]> {
try {
const history = await prisma.$queryRawUnsafe(
`
SELECT
id,
user_id,
ddl_type,
ddl_query,
success,
error_message,
executed_at
FROM ddl_execution_log
WHERE table_name = $1
ORDER BY executed_at DESC
LIMIT 20
`,
tableName
);
return history as any[];
} catch (error) {
logger.error(`테이블 '${tableName}' DDL 히스토리 조회 실패:`, error);
return [];
}
}
/**
* DDL 로그 정리 (오래된 로그 삭제)
*/
static async cleanupOldLogs(retentionDays: number = 90): Promise<number> {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const result = await prisma.$executeRaw`
DELETE FROM ddl_execution_log
WHERE executed_at < ${cutoffDate}
`;
logger.info(`DDL 로그 정리 완료: ${result}개 레코드 삭제`, {
retentionDays,
cutoffDate: cutoffDate.toISOString(),
});
return result as number;
} catch (error) {
logger.error("DDL 로그 정리 실패:", error);
return 0;
}
}
/**
* 긴급 상황 로그 (시스템 테이블 접근 시도 등)
*/
static async logSecurityAlert(
userId: string,
companyCode: string,
alertType:
| "SYSTEM_TABLE_ACCESS"
| "INVALID_PERMISSION"
| "SUSPICIOUS_ACTIVITY",
details: string,
requestData?: any
): Promise<void> {
try {
// 보안 알림은 별도의 고급 로깅
logger.error("DDL 보안 알림", {
alertType,
userId,
companyCode,
details,
requestData,
severity: "CRITICAL",
timestamp: new Date().toISOString(),
});
// 필요시 외부 알림 시스템 연동 (이메일, 슬랙 등)
// await sendSecurityAlert(alertType, userId, details);
} catch (error) {
logger.error("보안 알림 기록 실패:", error);
}
}
}