Spaces:
Sleeping
Sleeping
| 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<string, unknown>; | |
| } | |
| /** | |
| * ログディレクトリが存在することを確認し、必要に応じて作成 | |
| */ | |
| async function ensureLogDirectory(): Promise<void> { | |
| 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<string, unknown>, | |
| userEmail?: string, | |
| ): Promise<void> { | |
| // ログエントリの作成 | |
| 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, | |
| }; | |