import fs from 'fs/promises'; /** * 永続ストレージにエラーログを記録するためのロガー * HuggingFace Spacesの /data ディレクトリに保存される * ログファイルは月ごとにローテーションされる(例: image-generation-error-2025-10.csv) * CSV形式で出力される */ const LOG_DIR = '/data/logs/html-preview'; /** * 現在の年月に基づいてログファイルパスを生成 */ function getLogFilePath(): string { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); return `${LOG_DIR}/image-generation-error-${year}-${month}.csv`; } interface ErrorLogEntry { timestamp: string; errorType: 'FV' | 'Content'; service: string; errorMessage: string; userEmail?: string; referenceUrl?: string; tabName?: string; prompt?: string; status?: number | string; details: Record; } /** * ログディレクトリが存在することを確認し、必要に応じて作成 */ async function ensureLogDirectory(): Promise { try { await fs.mkdir(LOG_DIR, { recursive: true }); } catch (error) { console.error('[PersistentErrorLogger] Failed to create log directory:', error); } } /** * CSV値をエスケープ(常にダブルクォートで囲み、内部のダブルクォートを二重化) */ function escapeCSV(value: string): string { // 空文字の場合はそのまま返す if (!value) return ''; // すべての値をダブルクォートで囲む(日本語の文字化け防止) return `"${value.replace(/"/g, '""')}"`; } /** * CSVヘッダー行(UTF-8 BOM付き) */ const CSV_HEADER = '\uFEFF' + 'timestamp,errorType,service,errorMessage,userEmail,referenceUrl,tabName,prompt,status,details\n'; /** * ErrorLogEntryをCSV行に変換 */ function toCSVLine(entry: ErrorLogEntry): string { const fields = [ escapeCSV(entry.timestamp), escapeCSV(entry.errorType), escapeCSV(entry.service), escapeCSV(entry.errorMessage), escapeCSV(entry.userEmail || ''), escapeCSV(entry.referenceUrl || ''), escapeCSV(entry.tabName || ''), escapeCSV(entry.prompt || ''), escapeCSV(entry.status?.toString() || ''), escapeCSV(JSON.stringify(entry.details)), ]; return fields.join(',') + '\n'; } /** * 永続ストレージにエラーログを追記 * @param errorType エラータイプ('FV' or 'Content') * @param service サービス名(例: 'OpenAI gpt-image-1', 'Gemini 2.5 Flash Image') * @param errorMessage エラーメッセージ * @param commonFields 共通フィールド(referenceUrl, tabName, prompt, status) * @param details サービス固有の詳細情報 * @param userEmail ユーザーメールアドレス(あれば) */ async function errorToPersistStorage( errorType: 'FV' | 'Content', service: string, errorMessage: string, commonFields: { referenceUrl?: string; tabName?: string; prompt?: string; status?: number | string; }, details: Record, userEmail?: string, ): Promise { // ログエントリの作成 const logEntry: ErrorLogEntry = { timestamp: new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo', hour12: false, }), errorType, service, errorMessage, ...(userEmail && { userEmail }), ...commonFields, details, }; try { // /dataディレクトリが利用可能かチェック await fs.access('/data', fs.constants.W_OK); // /dataが利用可能な場合は永続ストレージに書き込み await ensureLogDirectory(); // 現在の月に基づいたログファイルパスを取得 const logFilePath = getLogFilePath(); // ファイルが存在しない場合はヘッダー行を追加 let fileExists = false; try { await fs.access(logFilePath); fileExists = true; } catch { // ファイルが存在しない場合 fileExists = false; } if (!fileExists) { await fs.writeFile(logFilePath, CSV_HEADER, 'utf-8'); } // CSV形式で追記 const csvLine = toCSVLine(logEntry); await fs.appendFile(logFilePath, csvLine, 'utf-8'); } catch { } } /** * ロガーオブジェクト */ export const logger = { errorToPersistStorage, };