プロジェクト

全般

プロフィール

バグ #762

未完了

【改善】インフラヘルパー - エラーハンドリング・ログ機能強化

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

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

0%

予定工数:

説明

🎯 概要

インフラヘルパーサービスのエラーハンドリングとログ機能を強化し、問題の早期発見と迅速な対応を可能にする。

📋 実装内容

1. 構造化ログシステムの実装

backend/utils/logger.js

const winston = require('winston');
const path = require('path');

// カスタムフォーマット
const customFormat = winston.format.combine(
  winston.format.timestamp({
    format: 'YYYY-MM-DD HH:mm:ss.SSS'
  }),
  winston.format.errors({ stack: true }),
  winston.format.json(),
  winston.format.printf(({ timestamp, level, message, ...metadata }) => {
    let msg = `${timestamp} [${level.toUpperCase()}] ${message}`;
    if (Object.keys(metadata).length > 0) {
      msg += ` ${JSON.stringify(metadata)}`;
    }
    return msg;
  })
);

// ログレベル定義
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
};

// ログローテーション設定
const DailyRotateFile = require('winston-daily-rotate-file');

const fileRotateTransport = new DailyRotateFile({
  filename: path.join(__dirname, '../../logs/infra-helper-%DATE%.log'),
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',
  maxFiles: '14d',
  format: customFormat
});

// Logger インスタンス作成
const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  levels,
  format: customFormat,
  transports: [
    // ファイル出力(全ログ)
    fileRotateTransport,
    
    // エラーログ専用ファイル
    new DailyRotateFile({
      filename: path.join(__dirname, '../../logs/error-%DATE%.log'),
      datePattern: 'YYYY-MM-DD',
      maxSize: '20m',
      maxFiles: '30d',
      level: 'error',
      format: customFormat
    })
  ],
});

// 開発環境ではコンソール出力も追加
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }));
}

// HTTPリクエストログ用ミドルウェア
const httpLogger = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    
    logger.http('HTTP Request', {
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip,
      userAgent: req.get('user-agent'),
      user: req.user?.login
    });
  });
  
  next();
};

// 互換性関数へのログ追加
function addCompatibilityLogging(compatibilityModule) {
  const originalGetSystemUptime = compatibilityModule.getSystemUptime;
  
  compatibilityModule.getSystemUptime = async function() {
    try {
      const result = await originalGetSystemUptime();
      logger.debug('System uptime retrieved', { uptime: result, method: 'compatibility' });
      return result;
    } catch (error) {
      logger.error('Failed to get system uptime', { 
        error: error.message, 
        stack: error.stack,
        fallback: 'Unknown'
      });
      throw error;
    }
  };
  
  return compatibilityModule;
}

module.exports = {
  logger,
  httpLogger,
  addCompatibilityLogging
};

2. エラーハンドリングの強化

backend/middleware/errorHandler.js

const { logger } = require('../utils/logger');

// カスタムエラークラス
class AppError extends Error {
  constructor(message, statusCode = 500, code = 'INTERNAL_ERROR', details = null) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.isOperational = true;
  }
}

// エラータイプ別のハンドラー
const errorHandlers = {
  ValidationError: (err) => {
    return new AppError('Validation failed', 400, 'VALIDATION_ERROR', err.details);
  },
  
  UnauthorizedError: (err) => {
    return new AppError('Authentication required', 401, 'AUTH_ERROR');
  },
  
  ForbiddenError: (err) => {
    return new AppError('Access denied', 403, 'FORBIDDEN');
  },
  
  NotFoundError: (err) => {
    return new AppError('Resource not found', 404, 'NOT_FOUND');
  },
  
  DockerError: (err) => {
    logger.error('Docker operation failed', { 
      error: err.message,
      statusCode: err.statusCode,
      reason: err.reason
    });
    return new AppError('Docker operation failed', 503, 'DOCKER_ERROR', {
      message: err.message
    });
  }
};

// 非同期エラーラッパー
const asyncErrorHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// グローバルエラーハンドラー
const globalErrorHandler = (err, req, res, next) => {
  let error = err;
  
  // 既知のエラータイプを変換
  if (err.constructor.name in errorHandlers) {
    error = errorHandlers[err.constructor.name](err);
  }
  
  // 運用エラーでない場合は汎用エラーに変換
  if (!error.isOperational) {
    logger.error('Unexpected error occurred', {
      error: err.message,
      stack: err.stack,
      url: req.url,
      method: req.method,
      ip: req.ip,
      user: req.user?.login
    });
    
    error = new AppError(
      process.env.NODE_ENV === 'production' 
        ? 'Internal server error' 
        : err.message,
      500,
      'INTERNAL_ERROR'
    );
  }
  
  // エラーレスポンス送信
  res.status(error.statusCode).json({
    success: false,
    error: {
      code: error.code,
      message: error.message,
      ...(error.details && { details: error.details }),
      ...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
    },
    timestamp: new Date().toISOString(),
    path: req.path
  });
};

// 未処理の Promise rejection ハンドラー
process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection', {
    reason: reason,
    promise: promise
  });
  
  // 開発環境では詳細を出力
  if (process.env.NODE_ENV !== 'production') {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  }
});

// 未処理の例外ハンドラー
process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception', {
    error: error.message,
    stack: error.stack
  });
  
  // グレースフルシャットダウン
  logger.info('Shutting down gracefully...');
  process.exit(1);
});

module.exports = {
  AppError,
  asyncErrorHandler,
  globalErrorHandler
};

3. 互換性モジュールへのエラーハンドリング追加

backend/utils/compatibility.js (改善版)

const { logger } = require('./logger');

async function getSystemUptime() {
    const methods = [
        {
            name: 'proc_uptime',
            fn: async () => {
                const uptimeData = await fs.readFile('/proc/uptime', 'utf8');
                const uptimeSeconds = parseFloat(uptimeData.split(' ')[0]);
                return formatUptime(Math.floor(uptimeSeconds));
            }
        },
        {
            name: 'uptime_command',
            fn: async () => {
                const { stdout } = await execAsync('uptime');
                return parseBasicUptime(stdout);
            }
        },
        {
            name: 'docker_info',
            fn: async () => {
                const info = await docker.info();
                const uptimeMs = Date.now() - new Date(info.SystemTime).getTime();
                return formatUptime(Math.floor(uptimeMs / 1000));
            }
        }
    ];
    
    for (const method of methods) {
        try {
            const result = await method.fn();
            logger.debug(`Uptime retrieved using ${method.name}`, { uptime: result });
            return result;
        } catch (error) {
            logger.warn(`Failed to get uptime using ${method.name}`, {
                error: error.message,
                method: method.name
            });
            continue;
        }
    }
    
    logger.error('All uptime retrieval methods failed');
    return 'Unknown';
}

async function getMemoryInfo() {
    try {
        const memInfo = await fs.readFile('/proc/meminfo', 'utf8');
        // ... 解析ロジック
        
        logger.debug('Memory info retrieved', { 
            total: result.total, 
            percentage: result.percentage 
        });
        
        return result;
    } catch (error) {
        logger.error('Failed to read memory info', {
            error: error.message,
            fallback: 'using free command'
        });
        
        // フォールバック処理
        try {
            const { stdout } = await execAsync('free -h');
            // ... 解析ロジック
            return result;
        } catch (fallbackError) {
            logger.error('Fallback memory info retrieval failed', {
                error: fallbackError.message
            });
            
            return {
                total: 'Unknown',
                used: 'Unknown',
                free: 'Unknown',
                percentage: 'N/A'
            };
        }
    }
}

4. メトリクスとアラート設定

backend/utils/metrics.js

const prometheus = require('prom-client');
const { logger } = require('./logger');

// Prometheusレジストリ
const register = new prometheus.Registry();

// デフォルトメトリクス
prometheus.collectDefaultMetrics({ register });

// カスタムメトリクス
const httpRequestDuration = new prometheus.Histogram({
    name: 'http_request_duration_seconds',
    help: 'Duration of HTTP requests in seconds',
    labelNames: ['method', 'route', 'status'],
    buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});

const compatibilityMethodUsage = new prometheus.Counter({
    name: 'compatibility_method_usage_total',
    help: 'Usage count of compatibility methods',
    labelNames: ['method', 'success']
});

const errorCounter = new prometheus.Counter({
    name: 'application_errors_total',
    help: 'Total number of application errors',
    labelNames: ['type', 'code']
});

register.registerMetric(httpRequestDuration);
register.registerMetric(compatibilityMethodUsage);
register.registerMetric(errorCounter);

// メトリクス記録ミドルウェア
const metricsMiddleware = (req, res, next) => {
    const start = Date.now();
    
    res.on('finish', () => {
        const duration = (Date.now() - start) / 1000;
        httpRequestDuration
            .labels(req.method, req.route?.path || req.path, res.statusCode)
            .observe(duration);
    });
    
    next();
};

// エラーメトリクス記録
const recordError = (type, code) => {
    errorCounter.labels(type, code).inc();
    
    // エラー率が閾値を超えた場合のアラート
    const errorRate = errorCounter.get().values.reduce((sum, v) => sum + v.value, 0);
    if (errorRate > 100) {
        logger.error('High error rate detected', {
            rate: errorRate,
            threshold: 100,
            alert: true
        });
    }
};

module.exports = {
    register,
    metricsMiddleware,
    compatibilityMethodUsage,
    recordError
};

5. サーバー設定の更新

backend/server.js (改善部分)

const { logger, httpLogger } = require('./utils/logger');
const { asyncErrorHandler, globalErrorHandler } = require('./middleware/errorHandler');
const { metricsMiddleware, register } = require('./utils/metrics');

// ログミドルウェア
app.use(httpLogger);

// メトリクスミドルウェア
app.use(metricsMiddleware);

// エラーハンドリングでラップ
app.get("/api/v1/vps/status", authenticateToken, asyncErrorHandler(async (req, res) => {
    try {
        // ... 既存のロジック
        
        logger.info('VPS status retrieved successfully', {
            user: req.user.login,
            containers: statusData.containers
        });
        
        res.json({
            success: true,
            data: statusData
        });
    } catch (error) {
        logger.error('VPS status retrieval failed', {
            error: error.message,
            user: req.user.login
        });
        throw error; // グローバルハンドラーで処理
    }
}));

// Prometheusメトリクスエンドポイント
app.get('/metrics', authenticateToken, (req, res) => {
    res.set('Content-Type', register.contentType);
    register.metrics().then(data => res.send(data));
});

// グローバルエラーハンドラー(最後に設定)
app.use(globalErrorHandler);

// グレースフルシャットダウン
const gracefulShutdown = () => {
    logger.info('Received shutdown signal, closing server gracefully...');
    
    server.close(() => {
        logger.info('HTTP server closed');
        
        // WebSocket接続をクローズ
        wsManager.closeAll();
        
        // データベース接続をクローズ
        // db.close();
        
        logger.info('Shutdown complete');
        process.exit(0);
    });
    
    // 30秒後に強制終了
    setTimeout(() => {
        logger.error('Forced shutdown after timeout');
        process.exit(1);
    }, 30000);
};

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

📊 期待される効果

  1. 問題の早期発見

    • 詳細なログによる問題の追跡
    • エラーパターンの分析
    • パフォーマンス問題の特定
  2. 運用の安定性向上

    • グレースフルシャットダウン
    • エラーからの自動復旧
    • リソースリークの防止
  3. デバッグ効率の向上

    • 構造化ログによる検索性向上
    • エラーコンテキストの充実
    • メトリクスによる傾向分析

🔧 実装計画

  1. ログシステムの実装(1日)
  2. エラーハンドリングミドルウェア実装(1日)
  3. 既存コードへの適用(2日)
  4. メトリクス実装(1日)
  5. 動作確認とチューニング(1日)

✅ 完了条件

  • すべてのエラーが構造化ログに記録される
  • エラーレスポンスが統一フォーマット
  • Prometheusメトリクスが公開される
  • グレースフルシャットダウンが動作
  • ログローテーションが設定される

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

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