165 lines
6.6 KiB
TypeScript
165 lines
6.6 KiB
TypeScript
import { z } from 'zod';
|
|
import { db } from '../db/client.js';
|
|
|
|
export const ERROR_LOG_TABLE = 'error_logs';
|
|
export const ERROR_LOG_VIEW_TOKEN = 'usr_7f3a9c2d8e1b4a6f';
|
|
|
|
export const createErrorLogSchema = z.object({
|
|
source: z.enum(['server', 'client', 'automation']).default('server'),
|
|
sourceLabel: z.string().trim().max(80).optional().nullable(),
|
|
errorType: z.string().trim().min(1).max(120),
|
|
errorName: z.string().trim().max(255).optional().nullable(),
|
|
errorMessage: z.string().trim().min(1).max(10000),
|
|
detail: z.string().trim().max(50000).optional().nullable(),
|
|
stackTrace: z.string().trim().max(50000).optional().nullable(),
|
|
statusCode: z.number().int().min(100).max(599).optional().nullable(),
|
|
requestMethod: z.string().trim().max(10).optional().nullable(),
|
|
requestPath: z.string().trim().max(1000).optional().nullable(),
|
|
relatedPlanId: z.number().int().positive().optional().nullable(),
|
|
relatedWorkId: z.string().trim().max(120).optional().nullable(),
|
|
context: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type ErrorLogPayload = z.infer<typeof createErrorLogSchema>;
|
|
|
|
async function ensureErrorLogTable() {
|
|
const hasTable = await db.schema.hasTable(ERROR_LOG_TABLE);
|
|
|
|
if (!hasTable) {
|
|
await db.schema.createTable(ERROR_LOG_TABLE, (table) => {
|
|
table.increments('id').primary();
|
|
table.string('source', 20).notNullable().defaultTo('server');
|
|
table.string('source_label', 80).nullable();
|
|
table.string('error_type', 120).notNullable();
|
|
table.string('error_name', 255).nullable();
|
|
table.text('error_message').notNullable();
|
|
table.text('detail').nullable();
|
|
table.text('stack_trace').nullable();
|
|
table.integer('status_code').nullable();
|
|
table.string('request_method', 10).nullable();
|
|
table.string('request_path', 1000).nullable();
|
|
table.integer('related_plan_id').nullable();
|
|
table.string('related_work_id', 120).nullable();
|
|
table.jsonb('context_json').nullable();
|
|
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
const requiredColumns: Array<[string, (table: any) => void]> = [
|
|
['source', (table) => table.string('source', 20).notNullable().defaultTo('server')],
|
|
['source_label', (table) => table.string('source_label', 80).nullable()],
|
|
['error_type', (table) => table.string('error_type', 120).notNullable().defaultTo('unknown')],
|
|
['error_name', (table) => table.string('error_name', 255).nullable()],
|
|
['error_message', (table) => table.text('error_message').notNullable().defaultTo('')],
|
|
['detail', (table) => table.text('detail').nullable()],
|
|
['stack_trace', (table) => table.text('stack_trace').nullable()],
|
|
['status_code', (table) => table.integer('status_code').nullable()],
|
|
['request_method', (table) => table.string('request_method', 10).nullable()],
|
|
['request_path', (table) => table.string('request_path', 1000).nullable()],
|
|
['related_plan_id', (table) => table.integer('related_plan_id').nullable()],
|
|
['related_work_id', (table) => table.string('related_work_id', 120).nullable()],
|
|
['context_json', (table) => table.jsonb('context_json').nullable()],
|
|
['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
|
|
];
|
|
|
|
for (const [columnName, createColumn] of requiredColumns) {
|
|
const hasColumn = await db.schema.hasColumn(ERROR_LOG_TABLE, columnName);
|
|
|
|
if (!hasColumn) {
|
|
await db.schema.alterTable(ERROR_LOG_TABLE, (table) => {
|
|
createColumn(table);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizePayload(payload: ErrorLogPayload) {
|
|
const parsedPayload = createErrorLogSchema.parse(payload);
|
|
const sourceLabel =
|
|
parsedPayload.sourceLabel ??
|
|
(parsedPayload.source === 'client'
|
|
? '프론트엔드'
|
|
: parsedPayload.source === 'automation'
|
|
? 'Plan 자동화'
|
|
: '워크서버 API');
|
|
|
|
return {
|
|
source: parsedPayload.source,
|
|
source_label: sourceLabel,
|
|
error_type: parsedPayload.errorType,
|
|
error_name: parsedPayload.errorName ?? null,
|
|
error_message: parsedPayload.errorMessage,
|
|
detail: parsedPayload.detail ?? null,
|
|
stack_trace: parsedPayload.stackTrace ?? null,
|
|
status_code: parsedPayload.statusCode ?? null,
|
|
request_method: parsedPayload.requestMethod ?? null,
|
|
request_path: parsedPayload.requestPath ?? null,
|
|
related_plan_id: parsedPayload.relatedPlanId ?? null,
|
|
related_work_id: parsedPayload.relatedWorkId ?? null,
|
|
context_json: parsedPayload.context ?? null,
|
|
};
|
|
}
|
|
|
|
function mapErrorLogRow(row: Record<string, unknown>) {
|
|
return {
|
|
id: Number(row.id),
|
|
source: String(row.source ?? 'server'),
|
|
sourceLabel: row.source_label ? String(row.source_label) : null,
|
|
errorType: String(row.error_type ?? ''),
|
|
errorName: row.error_name ? String(row.error_name) : null,
|
|
errorMessage: String(row.error_message ?? ''),
|
|
detail: row.detail ? String(row.detail) : null,
|
|
stackTrace: row.stack_trace ? String(row.stack_trace) : null,
|
|
statusCode: typeof row.status_code === 'number' ? row.status_code : row.status_code ? Number(row.status_code) : null,
|
|
requestMethod: row.request_method ? String(row.request_method) : null,
|
|
requestPath: row.request_path ? String(row.request_path) : null,
|
|
relatedPlanId:
|
|
typeof row.related_plan_id === 'number'
|
|
? row.related_plan_id
|
|
: row.related_plan_id
|
|
? Number(row.related_plan_id)
|
|
: null,
|
|
relatedWorkId: row.related_work_id ? String(row.related_work_id) : null,
|
|
context: row.context_json && typeof row.context_json === 'object' ? (row.context_json as Record<string, unknown>) : null,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|
|
|
|
export async function setupErrorLogTable() {
|
|
await ensureErrorLogTable();
|
|
|
|
return {
|
|
ok: true,
|
|
table: ERROR_LOG_TABLE,
|
|
};
|
|
}
|
|
|
|
export async function createErrorLog(payload: ErrorLogPayload) {
|
|
await ensureErrorLogTable();
|
|
|
|
const [row] = await db(ERROR_LOG_TABLE)
|
|
.insert({
|
|
...normalizePayload(payload),
|
|
created_at: db.fn.now(),
|
|
})
|
|
.returning('*');
|
|
|
|
return mapErrorLogRow(row);
|
|
}
|
|
|
|
export async function listErrorLogs(limit = 50) {
|
|
await ensureErrorLogTable();
|
|
|
|
const safeLimit = Number.isFinite(limit) ? Math.min(Math.max(Math.trunc(limit), 1), 200) : 50;
|
|
const rows = await db(ERROR_LOG_TABLE).select('*').orderBy('created_at', 'desc').limit(safeLimit);
|
|
|
|
return rows.map((row) => mapErrorLogRow(row));
|
|
}
|
|
|
|
export function hasErrorLogViewAccessToken(token: string | string[] | undefined) {
|
|
const normalizedToken = Array.isArray(token) ? token[0] : token;
|
|
return String(normalizedToken ?? '').trim() === ERROR_LOG_VIEW_TOKEN;
|
|
}
|