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:
kappa
2026-01-30 08:27:34 +09:00
parent f62712af37
commit 6385b5cab6
13 changed files with 767 additions and 7 deletions

105
src/utils/db-logger.ts Normal file
View 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;
}>;
}

View File

@@ -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,