FE_Test / server /utils /persistent-error-logger.ts
GitHub Actions
Deploy from GitHub Actions [test] - 2025-10-31 10:18:25
5f2aab6
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,
};