プロジェクト

全般

プロフィール

バグ #756

未完了

【開発計画】インフラヘルパー Phase 3 - コア機能追加(ログ・バックアップ・通知)

Redmine Admin さんが5日前に追加.

ステータス:
新規
優先度:
高め
担当者:
-
開始日:
2025-06-26
期日:
進捗率:

0%

予定工数:

説明

🎯 Phase 3: コア機能追加(機能拡張)

概要

インフラヘルパーサービスに不足している主要機能(ログ管理、バックアップ、通知)を実装する。

対応期限

2025年8月15日(6週間)

実装タスク

📊 3-1. ログ管理機能

APIエンドポイント設計

// ログ取得
GET /api/v1/logs/containers/:containerName
  Query params:
    - lines: 取得行数デフォルト: 100
    - since: 開始時刻ISO 8601
    - until: 終了時刻ISO 8601
    - follow: リアルタイム追跡boolean

// ログ検索
POST /api/v1/logs/search
  Body:
    - query: 検索クエリ
    - containers: 対象コンテナ配列
    - dateRange: { start, end }
    - severity: ['error', 'warn', 'info']

// ログダウンロード
GET /api/v1/logs/download/:containerName
  Query params:
    - format: 'txt' | 'json' | 'csv'
    - compress: boolean

実装詳細

// backend/services/logService.js
class LogService {
  async getContainerLogs(containerName, options) {
    const container = docker.getContainer(containerName);
    const stream = await container.logs({
      stdout: true,
      stderr: true,
      since: options.since || 0,
      timestamps: true,
      tail: options.lines || 100
    });
    
    return this.parseLogStream(stream);
  }
  
  async searchLogs(searchParams) {
    // Elasticsearchまたは内部検索実装
    const results = [];
    for (const containerName of searchParams.containers) {
      const logs = await this.getContainerLogs(containerName, {
        since: searchParams.dateRange.start
      });
      const filtered = logs.filter(log => 
        log.message.includes(searchParams.query) &&
        (!searchParams.severity || searchParams.severity.includes(log.level))
      );
      results.push(...filtered);
    }
    return results;
  }
  
  streamLogs(containerName, ws) {
    const container = docker.getContainer(containerName);
    container.logs({
      stdout: true,
      stderr: true,
      follow: true,
      timestamps: true
    }, (err, stream) => {
      stream.on('data', chunk => {
        ws.send(JSON.stringify({
          type: 'log',
          container: containerName,
          data: chunk.toString()
        }));
      });
    });
  }
}

フロントエンド実装

// LogViewer Component
function LogViewer() {
  const [logs, setLogs] = useState([]);
  const [filter, setFilter] = useState('');
  const [following, setFollowing] = useState(false);
  
  useEffect(() => {
    if (following) {
      const ws = new WebSocket(`${WS_URL}/logs/${containerName}`);
      ws.onmessage = (event) => {
        const log = JSON.parse(event.data);
        setLogs(prev => [...prev.slice(-999), log]);
      };
      return () => ws.close();
    }
  }, [following, containerName]);
  
  return (
    <div className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono">
      <div className="flex justify-between mb-4">
        <input
          type="text"
          placeholder="Filter logs..."
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          className="bg-gray-800 px-3 py-1 rounded"
        />
        <button
          onClick={() => setFollowing(!following)}
          className={`px-4 py-1 rounded ${following ? 'bg-red-600' : 'bg-green-600'}`}
        >
          {following ? 'Stop Following' : 'Follow Logs'}
        </button>
      </div>
      <div className="h-96 overflow-y-auto">
        {logs
          .filter(log => log.message.includes(filter))
          .map((log, i) => (
            <div key={i} className={`py-1 ${log.level === 'error' ? 'text-red-400' : ''}`}>
              <span className="text-gray-500">{log.timestamp}</span> {log.message}
            </div>
          ))}
      </div>
    </div>
  );
}

💾 3-2. バックアップ・復旧機能

APIエンドポイント設計

// バックアップ作成
POST /api/v1/backup/create
  Body:
    - type: 'full' | 'incremental' | 'selective'
    - targets: ['containers', 'volumes', 'configs', 'databases']
    - compression: boolean
    - encryption: boolean

// バックアップ一覧
GET /api/v1/backup/list
  Query params:
    - page, limit, sort

// バックアップ復旧
POST /api/v1/backup/restore/:backupId
  Body:
    - targets: 復旧対象の選択
    - options: { stopContainers: boolean, overwrite: boolean }

// バックアップスケジュール
POST /api/v1/backup/schedule
  Body:
    - cron: "0 2 * * *"
    - type: 'full' | 'incremental'
    - retention: { days: 30 }

実装詳細

// backend/services/backupService.js
class BackupService {
  async createBackup(options) {
    const backupId = uuidv4();
    const backupPath = `/backups/${backupId}`;
    
    // メタデータ作成
    const metadata = {
      id: backupId,
      timestamp: new Date(),
      type: options.type,
      targets: options.targets,
      status: 'in_progress'
    };
    
    // バックアップ実行
    const tasks = [];
    
    if (options.targets.includes('containers')) {
      tasks.push(this.backupContainers(backupPath));
    }
    
    if (options.targets.includes('volumes')) {
      tasks.push(this.backupVolumes(backupPath));
    }
    
    if (options.targets.includes('databases')) {
      tasks.push(this.backupDatabases(backupPath));
    }
    
    await Promise.all(tasks);
    
    // 圧縮・暗号化
    if (options.compression) {
      await this.compressBackup(backupPath);
    }
    
    if (options.encryption) {
      await this.encryptBackup(backupPath);
    }
    
    metadata.status = 'completed';
    metadata.size = await this.getBackupSize(backupPath);
    
    return metadata;
  }
  
  async backupVolumes(backupPath) {
    const volumes = await docker.listVolumes();
    for (const volume of volumes.Volumes) {
      await execAsync(`docker run --rm -v ${volume.Name}:/data -v ${backupPath}:/backup alpine tar -czf /backup/${volume.Name}.tar.gz /data`);
    }
  }
  
  async scheduleBackup(schedule) {
    // cron-jobとして登録
    cron.schedule(schedule.cron, async () => {
      await this.createBackup({
        type: schedule.type,
        targets: schedule.targets || ['containers', 'volumes', 'configs']
      });
      
      // 古いバックアップの削除
      await this.cleanupOldBackups(schedule.retention);
    });
  }
}

🔔 3-3. 通知・アラート機能

APIエンドポイント設計

// 通知チャンネル設定
POST /api/v1/notifications/channels
  Body:
    - type: 'email' | 'slack' | 'webhook' | 'line'
    - config: { /* チャンネル固有の設定 */ }

// アラートルール設定
POST /api/v1/alerts/rules
  Body:
    - name: "高CPU使用率"
    - condition: {
        metric: "cpu_usage",
        operator: ">",
        threshold: 80,
        duration: "5m"
      }
    - actions: ['notify', 'restart', 'scale']
    - channels: ['channelId1', 'channelId2']

// アラート履歴
GET /api/v1/alerts/history
  Query params:
    - status: 'active' | 'resolved' | 'acknowledged'
    - severity: 'critical' | 'warning' | 'info'
    - from, to: 日付範囲

実装詳細

// backend/services/alertService.js
class AlertService {
  constructor() {
    this.rules = new Map();
    this.channels = new Map();
    this.activeAlerts = new Map();
  }
  
  async evaluateRules() {
    for (const [ruleId, rule] of this.rules) {
      const value = await this.getMetricValue(rule.condition.metric);
      const triggered = this.evaluateCondition(value, rule.condition);
      
      if (triggered && !this.activeAlerts.has(ruleId)) {
        await this.triggerAlert(rule);
      } else if (!triggered && this.activeAlerts.has(ruleId)) {
        await this.resolveAlert(ruleId);
      }
    }
  }
  
  async triggerAlert(rule) {
    const alert = {
      id: uuidv4(),
      ruleId: rule.id,
      name: rule.name,
      severity: rule.severity,
      triggeredAt: new Date(),
      status: 'active'
    };
    
    this.activeAlerts.set(rule.id, alert);
    
    // 通知送信
    for (const channelId of rule.channels) {
      const channel = this.channels.get(channelId);
      await this.sendNotification(channel, alert);
    }
    
    // 自動アクション実行
    if (rule.actions.includes('restart')) {
      await this.executeAction('restart', rule.target);
    }
  }
  
  async sendNotification(channel, alert) {
    switch (channel.type) {
      case 'slack':
        await this.sendSlackNotification(channel.config, alert);
        break;
      case 'email':
        await this.sendEmailNotification(channel.config, alert);
        break;
      case 'line':
        await this.sendLineNotification(channel.config, alert);
        break;
      case 'webhook':
        await this.sendWebhookNotification(channel.config, alert);
        break;
    }
  }
}

// 5分ごとにルール評価
setInterval(() => {
  alertService.evaluateRules();
}, 5 * 60 * 1000);

フロントエンド実装

// Alert Rules Manager
function AlertRulesManager() {
  const [rules, setRules] = useState([]);
  const [showCreateModal, setShowCreateModal] = useState(false);
  
  return (
    <div className="space-y-4">
      <div className="flex justify-between items-center">
        <h2 className="text-2xl font-bold">Alert Rules</h2>
        <button
          onClick={() => setShowCreateModal(true)}
          className="bg-line-green text-white px-4 py-2 rounded-lg"
        >
          Create Rule
        </button>
      </div>
      
      <div className="grid gap-4">
        {rules.map(rule => (
          <div key={rule.id} className="bg-white p-4 rounded-lg shadow">
            <div className="flex justify-between items-start">
              <div>
                <h3 className="font-semibold">{rule.name}</h3>
                <p className="text-gray-600">
                  {rule.condition.metric} {rule.condition.operator} {rule.condition.threshold}
                </p>
              </div>
              <div className="flex gap-2">
                <StatusBadge status={rule.status} />
                <button className="text-red-600">Delete</button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

データベース設計

-- alerts table
CREATE TABLE alerts (
  id TEXT PRIMARY KEY,
  rule_id TEXT NOT NULL,
  name TEXT NOT NULL,
  severity TEXT NOT NULL,
  status TEXT NOT NULL,
  triggered_at TIMESTAMP NOT NULL,
  resolved_at TIMESTAMP,
  acknowledged_at TIMESTAMP,
  acknowledged_by TEXT,
  metadata JSON
);

-- backup_history table
CREATE TABLE backup_history (
  id TEXT PRIMARY KEY,
  type TEXT NOT NULL,
  status TEXT NOT NULL,
  started_at TIMESTAMP NOT NULL,
  completed_at TIMESTAMP,
  size_bytes INTEGER,
  targets JSON,
  error TEXT,
  metadata JSON
);

成果物

  • 完全実装されたログ管理システム
  • バックアップ・復旧システム
  • 通知・アラートシステム
  • 統合テスト済みのコード
  • APIドキュメント
  • ユーザーマニュアル

表示するデータがありません

他の形式にエクスポート: Atom PDF