feat: add server lifecycle management and D1 logging
- Add start/stop/reboot endpoints for server power management - Add D1-based logging system (logs table + db-logger utility) - Add idempotency_key validation for order deduplication - Extend VPS provider interface with lifecycle methods Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
105
src/utils/db-logger.ts
Normal file
105
src/utils/db-logger.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* D1 기반 실시간 로깅 유틸리티
|
||||
* console.log 대신 D1에 저장하여 나중에 조회 가능
|
||||
*/
|
||||
|
||||
type LogLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
export interface DbLogger {
|
||||
info: (message: string, context?: Record<string, unknown>) => void;
|
||||
warn: (message: string, context?: Record<string, unknown>) => void;
|
||||
error: (message: string, context?: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* D1에 로그 저장 (fire-and-forget, non-blocking)
|
||||
*/
|
||||
async function writeLog(
|
||||
db: D1Database,
|
||||
level: LogLevel,
|
||||
service: string,
|
||||
message: string,
|
||||
context?: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db.prepare(
|
||||
'INSERT INTO logs (level, service, message, context) VALUES (?, ?, ?, ?)'
|
||||
).bind(
|
||||
level,
|
||||
service,
|
||||
message,
|
||||
context ? JSON.stringify(context) : null
|
||||
).run();
|
||||
} catch (e) {
|
||||
// DB 저장 실패 시 console로 fallback
|
||||
console.error('[db-logger] Failed to write log:', e, { level, service, message, context });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스별 로거 생성
|
||||
*/
|
||||
export function createDbLogger(db: D1Database, service: string): DbLogger {
|
||||
return {
|
||||
info: (message: string, context?: Record<string, unknown>) => {
|
||||
console.log(`[${service}] ${message}`, context || '');
|
||||
writeLog(db, 'info', service, message, context);
|
||||
},
|
||||
warn: (message: string, context?: Record<string, unknown>) => {
|
||||
console.warn(`[${service}] ${message}`, context || '');
|
||||
writeLog(db, 'warn', service, message, context);
|
||||
},
|
||||
error: (message: string, context?: Record<string, unknown>) => {
|
||||
console.error(`[${service}] ${message}`, context || '');
|
||||
writeLog(db, 'error', service, message, context);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 최근 로그 조회
|
||||
*/
|
||||
export async function getRecentLogs(
|
||||
db: D1Database,
|
||||
options?: {
|
||||
level?: LogLevel;
|
||||
service?: string;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<Array<{
|
||||
id: number;
|
||||
level: string;
|
||||
service: string;
|
||||
message: string;
|
||||
context: string | null;
|
||||
created_at: string;
|
||||
}>> {
|
||||
const { level, service, limit = 100 } = options || {};
|
||||
|
||||
let query = 'SELECT * FROM logs WHERE 1=1';
|
||||
const params: string[] = [];
|
||||
|
||||
if (level) {
|
||||
query += ' AND level = ?';
|
||||
params.push(level);
|
||||
}
|
||||
if (service) {
|
||||
query += ' AND service = ?';
|
||||
params.push(service);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC LIMIT ?';
|
||||
params.push(String(limit));
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const result = await stmt.bind(...params).all();
|
||||
|
||||
return result.results as Array<{
|
||||
id: number;
|
||||
level: string;
|
||||
service: string;
|
||||
message: string;
|
||||
context: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
}
|
||||
@@ -52,6 +52,13 @@ export {
|
||||
EXCHANGE_RATE_FALLBACK
|
||||
} from './exchange-rate';
|
||||
|
||||
// D1 logging utilities
|
||||
export {
|
||||
createDbLogger,
|
||||
getRecentLogs
|
||||
} from './db-logger';
|
||||
export type { DbLogger } from './db-logger';
|
||||
|
||||
// Re-export region utilities from region-utils.ts for backward compatibility
|
||||
export {
|
||||
DEFAULT_ANVIL_REGION_FILTER_SQL,
|
||||
|
||||
Reference in New Issue
Block a user