365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
// 배치 실행 로그 서비스
|
|
// 작성일: 2024-12-24
|
|
|
|
import { query, queryOne } from "../database/db";
|
|
import {
|
|
BatchExecutionLog,
|
|
CreateBatchExecutionLogRequest,
|
|
UpdateBatchExecutionLogRequest,
|
|
BatchExecutionLogFilter,
|
|
BatchExecutionLogWithConfig
|
|
} from "../types/batchExecutionLogTypes";
|
|
import { ApiResponse } from "../types/batchTypes";
|
|
|
|
export class BatchExecutionLogService {
|
|
/**
|
|
* 배치 실행 로그 목록 조회
|
|
*/
|
|
static async getExecutionLogs(
|
|
filter: BatchExecutionLogFilter = {}
|
|
): Promise<ApiResponse<BatchExecutionLogWithConfig[]>> {
|
|
try {
|
|
const {
|
|
batch_config_id,
|
|
execution_status,
|
|
start_date,
|
|
end_date,
|
|
page = 1,
|
|
limit = 50
|
|
} = filter;
|
|
|
|
const skip = (page - 1) * limit;
|
|
const take = limit;
|
|
|
|
// WHERE 조건 구성
|
|
const whereConditions: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (batch_config_id) {
|
|
whereConditions.push(`bel.batch_config_id = $${paramIndex++}`);
|
|
params.push(batch_config_id);
|
|
}
|
|
|
|
if (execution_status) {
|
|
whereConditions.push(`bel.execution_status = $${paramIndex++}`);
|
|
params.push(execution_status);
|
|
}
|
|
|
|
if (start_date) {
|
|
whereConditions.push(`bel.start_time >= $${paramIndex++}`);
|
|
params.push(start_date);
|
|
}
|
|
|
|
if (end_date) {
|
|
whereConditions.push(`bel.start_time <= $${paramIndex++}`);
|
|
params.push(end_date);
|
|
}
|
|
|
|
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
|
|
|
// 로그 조회 (batch_config 정보 포함)
|
|
const sql = `
|
|
SELECT
|
|
bel.*,
|
|
json_build_object(
|
|
'id', bc.id,
|
|
'batch_name', bc.batch_name,
|
|
'description', bc.description,
|
|
'cron_schedule', bc.cron_schedule,
|
|
'is_active', bc.is_active
|
|
) as batch_config
|
|
FROM batch_execution_logs bel
|
|
LEFT JOIN batch_configs bc ON bel.batch_config_id = bc.id
|
|
${whereClause}
|
|
ORDER BY bel.start_time DESC
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
`;
|
|
|
|
const countSql = `
|
|
SELECT COUNT(*) as count
|
|
FROM batch_execution_logs bel
|
|
${whereClause}
|
|
`;
|
|
|
|
params.push(take, skip);
|
|
|
|
const [logs, countResult] = await Promise.all([
|
|
query<any>(sql, params),
|
|
query<{ count: number }>(countSql, params.slice(0, -2))
|
|
]);
|
|
|
|
const total = parseInt(countResult[0]?.count?.toString() || '0', 10);
|
|
|
|
return {
|
|
success: true,
|
|
data: logs as BatchExecutionLogWithConfig[],
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
total,
|
|
totalPages: Math.ceil(total / limit)
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error("배치 실행 로그 조회 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "배치 실행 로그 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 실행 로그 생성
|
|
*/
|
|
static async createExecutionLog(
|
|
data: CreateBatchExecutionLogRequest
|
|
): Promise<ApiResponse<BatchExecutionLog>> {
|
|
try {
|
|
const log = await queryOne<BatchExecutionLog>(
|
|
`INSERT INTO batch_execution_logs (
|
|
batch_config_id, execution_status, start_time, end_time,
|
|
duration_ms, total_records, success_records, failed_records,
|
|
error_message, error_details, server_name, process_id
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
RETURNING *`,
|
|
[
|
|
data.batch_config_id,
|
|
data.execution_status,
|
|
data.start_time || new Date(),
|
|
data.end_time,
|
|
data.duration_ms,
|
|
data.total_records || 0,
|
|
data.success_records || 0,
|
|
data.failed_records || 0,
|
|
data.error_message,
|
|
data.error_details,
|
|
data.server_name || process.env.HOSTNAME || 'unknown',
|
|
data.process_id || process.pid?.toString()
|
|
]
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: log as BatchExecutionLog,
|
|
message: "배치 실행 로그가 생성되었습니다."
|
|
};
|
|
} catch (error) {
|
|
console.error("배치 실행 로그 생성 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "배치 실행 로그 생성 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 실행 로그 업데이트
|
|
*/
|
|
static async updateExecutionLog(
|
|
id: number,
|
|
data: UpdateBatchExecutionLogRequest
|
|
): Promise<ApiResponse<BatchExecutionLog>> {
|
|
try {
|
|
// 동적 UPDATE 쿼리 생성
|
|
const updates: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (data.execution_status !== undefined) {
|
|
updates.push(`execution_status = $${paramIndex++}`);
|
|
params.push(data.execution_status);
|
|
}
|
|
if (data.end_time !== undefined) {
|
|
updates.push(`end_time = $${paramIndex++}`);
|
|
params.push(data.end_time);
|
|
}
|
|
if (data.duration_ms !== undefined) {
|
|
updates.push(`duration_ms = $${paramIndex++}`);
|
|
params.push(data.duration_ms);
|
|
}
|
|
if (data.total_records !== undefined) {
|
|
updates.push(`total_records = $${paramIndex++}`);
|
|
params.push(data.total_records);
|
|
}
|
|
if (data.success_records !== undefined) {
|
|
updates.push(`success_records = $${paramIndex++}`);
|
|
params.push(data.success_records);
|
|
}
|
|
if (data.failed_records !== undefined) {
|
|
updates.push(`failed_records = $${paramIndex++}`);
|
|
params.push(data.failed_records);
|
|
}
|
|
if (data.error_message !== undefined) {
|
|
updates.push(`error_message = $${paramIndex++}`);
|
|
params.push(data.error_message);
|
|
}
|
|
if (data.error_details !== undefined) {
|
|
updates.push(`error_details = $${paramIndex++}`);
|
|
params.push(data.error_details);
|
|
}
|
|
|
|
params.push(id);
|
|
|
|
const log = await queryOne<BatchExecutionLog>(
|
|
`UPDATE batch_execution_logs
|
|
SET ${updates.join(', ')}
|
|
WHERE id = $${paramIndex}
|
|
RETURNING *`,
|
|
params
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: log as BatchExecutionLog,
|
|
message: "배치 실행 로그가 업데이트되었습니다."
|
|
};
|
|
} catch (error) {
|
|
console.error("배치 실행 로그 업데이트 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "배치 실행 로그 업데이트 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 실행 로그 삭제
|
|
*/
|
|
static async deleteExecutionLog(id: number): Promise<ApiResponse<void>> {
|
|
try {
|
|
await query(`DELETE FROM batch_execution_logs WHERE id = $1`, [id]);
|
|
|
|
return {
|
|
success: true,
|
|
message: "배치 실행 로그가 삭제되었습니다."
|
|
};
|
|
} catch (error) {
|
|
console.error("배치 실행 로그 삭제 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "배치 실행 로그 삭제 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 배치의 최신 실행 로그 조회
|
|
*/
|
|
static async getLatestExecutionLog(
|
|
batchConfigId: number
|
|
): Promise<ApiResponse<BatchExecutionLog | null>> {
|
|
try {
|
|
const log = await queryOne<BatchExecutionLog>(
|
|
`SELECT * FROM batch_execution_logs
|
|
WHERE batch_config_id = $1
|
|
ORDER BY start_time DESC
|
|
LIMIT 1`,
|
|
[batchConfigId]
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
data: log || null
|
|
};
|
|
} catch (error) {
|
|
console.error("최신 배치 실행 로그 조회 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "최신 배치 실행 로그 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배치 실행 통계 조회
|
|
*/
|
|
static async getExecutionStats(
|
|
batchConfigId?: number,
|
|
startDate?: Date,
|
|
endDate?: Date
|
|
): Promise<ApiResponse<{
|
|
total_executions: number;
|
|
success_count: number;
|
|
failed_count: number;
|
|
success_rate: number;
|
|
average_duration_ms: number;
|
|
total_records_processed: number;
|
|
}>> {
|
|
try {
|
|
const whereConditions: string[] = [];
|
|
const params: any[] = [];
|
|
let paramIndex = 1;
|
|
|
|
if (batchConfigId) {
|
|
whereConditions.push(`batch_config_id = $${paramIndex++}`);
|
|
params.push(batchConfigId);
|
|
}
|
|
|
|
if (startDate) {
|
|
whereConditions.push(`start_time >= $${paramIndex++}`);
|
|
params.push(startDate);
|
|
}
|
|
|
|
if (endDate) {
|
|
whereConditions.push(`start_time <= $${paramIndex++}`);
|
|
params.push(endDate);
|
|
}
|
|
|
|
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
|
|
|
const logs = await query<{
|
|
execution_status: string;
|
|
duration_ms: number;
|
|
total_records: number;
|
|
}>(
|
|
`SELECT execution_status, duration_ms, total_records
|
|
FROM batch_execution_logs
|
|
${whereClause}`,
|
|
params
|
|
);
|
|
|
|
const total_executions = logs.length;
|
|
const success_count = logs.filter((log: any) => log.execution_status === 'SUCCESS').length;
|
|
const failed_count = logs.filter((log: any) => log.execution_status === 'FAILED').length;
|
|
const success_rate = total_executions > 0 ? (success_count / total_executions) * 100 : 0;
|
|
|
|
const validDurations = logs
|
|
.filter((log: any) => log.duration_ms !== null)
|
|
.map((log: any) => log.duration_ms!);
|
|
const average_duration_ms = validDurations.length > 0
|
|
? validDurations.reduce((sum: number, duration: number) => sum + duration, 0) / validDurations.length
|
|
: 0;
|
|
|
|
const total_records_processed = logs
|
|
.filter((log: any) => log.total_records !== null)
|
|
.reduce((sum: number, log: any) => sum + (log.total_records || 0), 0);
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
total_executions,
|
|
success_count,
|
|
failed_count,
|
|
success_rate,
|
|
average_duration_ms,
|
|
total_records_processed
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error("배치 실행 통계 조회 실패:", error);
|
|
return {
|
|
success: false,
|
|
message: "배치 실행 통계 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "알 수 없는 오류"
|
|
};
|
|
}
|
|
}
|
|
}
|