aiwf
Version:
AI Workflow Framework for Claude Code with multi-language support (Korean/English)
484 lines (421 loc) • 13.6 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { TokenStorage } from './token-storage.js';
/**
* 토큰 사용량 모니터링 및 임계값 알림을 관리하는 클래스
*/
export class TokenMonitor {
constructor() {
this.storage = new TokenStorage();
this.configFile = '.aiwf/token-data/monitor-config.json';
this.alertsFile = '.aiwf/token-data/alerts.json';
this.isEnabled = false;
this.thresholds = this.loadConfig();
this.alerts = this.loadAlerts();
this.ensureConfigExists();
}
/**
* 모니터링 설정을 로드합니다
* @returns {Object} 모니터링 설정
*/
loadConfig() {
try {
if (fs.existsSync(this.configFile)) {
const config = JSON.parse(fs.readFileSync(this.configFile, 'utf8'));
this.isEnabled = config.enabled || false;
return config.thresholds || this.getDefaultThresholds();
}
} catch (error) {
console.error('모니터링 설정 로드 중 오류 발생:', error);
}
return this.getDefaultThresholds();
}
/**
* 기본 임계값 설정을 반환합니다
* @returns {Object} 기본 임계값
*/
getDefaultThresholds() {
return {
dailyTokenLimit: 10000,
sessionTokenLimit: 2000,
warningThreshold: 80, // 80%에서 경고
criticalThreshold: 95, // 95%에서 위험 경고
sessionsPerDayLimit: 20,
tokensPerCommandLimit: 500
};
}
/**
* 알림 히스토리를 로드합니다
* @returns {Array<Object>} 알림 목록
*/
loadAlerts() {
try {
if (fs.existsSync(this.alertsFile)) {
return JSON.parse(fs.readFileSync(this.alertsFile, 'utf8'));
}
} catch (error) {
console.error('알림 히스토리 로드 중 오류 발생:', error);
}
return [];
}
/**
* 설정 파일이 존재하는지 확인하고 생성합니다
*/
ensureConfigExists() {
try {
const configDir = path.dirname(this.configFile);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
if (!fs.existsSync(this.configFile)) {
this.saveConfig();
}
} catch (error) {
console.error('설정 파일 생성 중 오류 발생:', error);
}
}
/**
* 설정을 저장합니다
*/
saveConfig() {
try {
const config = {
enabled: this.isEnabled,
thresholds: this.thresholds,
lastUpdated: Date.now()
};
fs.writeFileSync(this.configFile, JSON.stringify(config, null, 2));
} catch (error) {
console.error('설정 저장 중 오류 발생:', error);
}
}
/**
* 알림을 저장합니다
*/
saveAlerts() {
try {
fs.writeFileSync(this.alertsFile, JSON.stringify(this.alerts, null, 2));
} catch (error) {
console.error('알림 저장 중 오류 발생:', error);
}
}
/**
* 모니터링을 활성화/비활성화합니다
* @param {boolean} enabled - 활성화 여부
*/
setEnabled(enabled) {
this.isEnabled = enabled;
this.saveConfig();
console.log(`토큰 모니터링이 ${enabled ? '활성화' : '비활성화'}되었습니다.`);
}
/**
* 임계값을 설정합니다
* @param {Object} newThresholds - 새 임계값 설정
*/
setThresholds(newThresholds) {
this.thresholds = { ...this.thresholds, ...newThresholds };
this.saveConfig();
console.log('임계값이 업데이트되었습니다.');
}
/**
* 현재 토큰 사용량을 확인하고 임계값을 검사합니다
* @returns {Array<Object>} 임계값 위반 목록
*/
checkThresholds() {
if (!this.isEnabled) {
return [];
}
try {
const violations = this.storage.checkThresholds(this.thresholds);
// 새로운 위반사항에 대해 알림 생성
for (const violation of violations) {
if (!this.hasRecentAlert(violation.type)) {
this.createAlert(violation);
}
}
return violations;
} catch (error) {
console.error('임계값 확인 중 오류 발생:', error);
return [];
}
}
/**
* 최근 알림이 있는지 확인합니다
* @param {string} type - 알림 타입
* @returns {boolean} 최근 알림 존재 여부
*/
hasRecentAlert(type) {
const oneHour = 60 * 60 * 1000;
const cutoff = Date.now() - oneHour;
return this.alerts.some(alert =>
alert.type === type && alert.timestamp > cutoff
);
}
/**
* 새 알림을 생성합니다
* @param {Object} violation - 위반 정보
*/
createAlert(violation) {
try {
const alert = {
id: this.generateAlertId(),
type: violation.type,
level: this.getAlertLevel(violation),
message: violation.message,
threshold: violation.threshold,
current: violation.current,
timestamp: Date.now(),
acknowledged: false
};
this.alerts.push(alert);
this.saveAlerts();
// 알림 출력
this.displayAlert(alert);
// 중요한 알림의 경우 추가 처리
if (alert.level === 'critical') {
this.handleCriticalAlert(alert);
}
} catch (error) {
console.error('알림 생성 중 오류 발생:', error);
}
}
/**
* 알림 ID를 생성합니다
* @returns {string} 알림 ID
*/
generateAlertId() {
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 알림 레벨을 결정합니다
* @param {Object} violation - 위반 정보
* @returns {string} 알림 레벨
*/
getAlertLevel(violation) {
const currentPercentage = (violation.current / violation.threshold) * 100;
if (currentPercentage >= this.thresholds.criticalThreshold) {
return 'critical';
} else if (currentPercentage >= this.thresholds.warningThreshold) {
return 'warning';
} else {
return 'info';
}
}
/**
* 알림을 화면에 표시합니다
* @param {Object} alert - 알림 정보
*/
displayAlert(alert) {
const levelEmoji = {
info: 'ℹ️',
warning: '⚠️',
critical: '🚨'
};
const emoji = levelEmoji[alert.level] || 'ℹ️';
const timestamp = new Date(alert.timestamp).toLocaleString();
console.log(`\n${emoji} [${alert.level.toUpperCase()}] ${timestamp}`);
console.log(`${alert.message}`);
console.log(`현재: ${alert.current.toLocaleString()}, 임계값: ${alert.threshold.toLocaleString()}`);
if (alert.level === 'critical') {
console.log('🛑 즉시 조치가 필요합니다!');
}
console.log('─'.repeat(50));
}
/**
* 위험 알림을 처리합니다
* @param {Object} alert - 알림 정보
*/
handleCriticalAlert(alert) {
try {
// 위험 알림 로그 파일 생성
const criticalLogFile = '.aiwf/token-data/critical-alerts.log';
const logEntry = `[${new Date(alert.timestamp).toISOString()}] ${alert.message}\n`;
fs.appendFileSync(criticalLogFile, logEntry);
// 자동 압축 제안
if (alert.type === 'daily_token_limit') {
console.log('\n🔧 자동 압축을 실행하여 토큰 사용량을 줄이시겠습니까?');
console.log('명령어: aiwf compress --mode aggressive');
}
// 세션 정리 제안
if (alert.type === 'daily_session_limit') {
console.log('\n🧹 오래된 세션을 정리하시겠습니까?');
console.log('명령어: aiwf cleanup-sessions --older-than 7d');
}
} catch (error) {
console.error('위험 알림 처리 중 오류 발생:', error);
}
}
/**
* 실시간 모니터링을 시작합니다
* @param {number} interval - 모니터링 간격 (밀리초)
*/
startMonitoring(interval = 60000) { // 기본 1분
if (!this.isEnabled) {
console.log('모니터링이 비활성화되어 있습니다.');
return;
}
console.log(`토큰 모니터링을 시작합니다 (${interval/1000}초 간격)`);
this.monitoringInterval = setInterval(() => {
this.checkThresholds();
}, interval);
}
/**
* 실시간 모니터링을 중지합니다
*/
stopMonitoring() {
if (this.monitoringInterval) {
clearInterval(this.monitoringInterval);
this.monitoringInterval = null;
console.log('토큰 모니터링을 중지했습니다.');
}
}
/**
* 알림을 확인 처리합니다
* @param {string} alertId - 알림 ID
*/
acknowledgeAlert(alertId) {
try {
const alert = this.alerts.find(a => a.id === alertId);
if (alert) {
alert.acknowledged = true;
alert.acknowledgedAt = Date.now();
this.saveAlerts();
console.log(`알림 ${alertId}를 확인 처리했습니다.`);
} else {
console.log(`알림 ${alertId}를 찾을 수 없습니다.`);
}
} catch (error) {
console.error('알림 확인 처리 중 오류 발생:', error);
}
}
/**
* 모든 알림을 확인 처리합니다
*/
acknowledgeAllAlerts() {
try {
let count = 0;
for (const alert of this.alerts) {
if (!alert.acknowledged) {
alert.acknowledged = true;
alert.acknowledgedAt = Date.now();
count++;
}
}
this.saveAlerts();
console.log(`${count}개의 알림을 확인 처리했습니다.`);
} catch (error) {
console.error('모든 알림 확인 처리 중 오류 발생:', error);
}
}
/**
* 미확인 알림을 조회합니다
* @returns {Array<Object>} 미확인 알림 목록
*/
getUnacknowledgedAlerts() {
return this.alerts.filter(alert => !alert.acknowledged);
}
/**
* 알림 히스토리를 조회합니다
* @param {number} limit - 조회할 알림 수
* @returns {Array<Object>} 알림 목록
*/
getAlertHistory(limit = 50) {
return this.alerts
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit);
}
/**
* 알림 통계를 조회합니다
* @returns {Object} 알림 통계
*/
getAlertStats() {
try {
const stats = {
total: this.alerts.length,
byLevel: { info: 0, warning: 0, critical: 0 },
byType: {},
acknowledged: 0,
recent: 0 // 최근 24시간
};
const dayAgo = Date.now() - (24 * 60 * 60 * 1000);
for (const alert of this.alerts) {
stats.byLevel[alert.level]++;
stats.byType[alert.type] = (stats.byType[alert.type] || 0) + 1;
if (alert.acknowledged) {
stats.acknowledged++;
}
if (alert.timestamp > dayAgo) {
stats.recent++;
}
}
return stats;
} catch (error) {
console.error('알림 통계 조회 중 오류 발생:', error);
return null;
}
}
/**
* 오래된 알림을 정리합니다
* @param {number} daysToKeep - 보관할 일수
*/
cleanupOldAlerts(daysToKeep = 30) {
try {
const cutoff = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000);
const originalCount = this.alerts.length;
this.alerts = this.alerts.filter(alert => alert.timestamp > cutoff);
const removedCount = originalCount - this.alerts.length;
if (removedCount > 0) {
this.saveAlerts();
console.log(`${removedCount}개의 오래된 알림을 정리했습니다.`);
}
} catch (error) {
console.error('알림 정리 중 오류 발생:', error);
}
}
/**
* 모니터링 상태를 조회합니다
* @returns {Object} 모니터링 상태
*/
getStatus() {
return {
enabled: this.isEnabled,
thresholds: this.thresholds,
unacknowledgedAlerts: this.getUnacknowledgedAlerts().length,
totalAlerts: this.alerts.length,
isRunning: !!this.monitoringInterval,
lastCheck: this.lastCheck || null
};
}
/**
* 모니터링 대시보드를 출력합니다
*/
displayDashboard() {
console.log('\n=== 토큰 모니터링 대시보드 ===');
const status = this.getStatus();
const stats = this.getAlertStats();
console.log(`상태: ${status.enabled ? '✅ 활성화' : '❌ 비활성화'}`);
console.log(`실행 중: ${status.isRunning ? '✅ 예' : '❌ 아니오'}`);
console.log('\n--- 임계값 설정 ---');
console.log(`일일 토큰 한도: ${status.thresholds.dailyTokenLimit.toLocaleString()}`);
console.log(`세션 토큰 한도: ${status.thresholds.sessionTokenLimit.toLocaleString()}`);
console.log(`경고 임계값: ${status.thresholds.warningThreshold}%`);
console.log(`위험 임계값: ${status.thresholds.criticalThreshold}%`);
console.log('\n--- 알림 통계 ---');
console.log(`총 알림: ${stats.total}`);
console.log(`미확인 알림: ${status.unacknowledgedAlerts}`);
console.log(`최근 24시간: ${stats.recent}`);
console.log(`위험 알림: ${stats.byLevel.critical}`);
console.log(`경고 알림: ${stats.byLevel.warning}`);
const unacknowledged = this.getUnacknowledgedAlerts();
if (unacknowledged.length > 0) {
console.log('\n--- 미확인 알림 ---');
unacknowledged.slice(0, 5).forEach(alert => {
const emoji = alert.level === 'critical' ? '🚨' :
alert.level === 'warning' ? '⚠️' : 'ℹ️';
console.log(`${emoji} ${alert.message}`);
});
}
console.log('\n========================\n');
}
}
export default TokenMonitor;