操作
バグ #762
未完了【改善】インフラヘルパー - エラーハンドリング・ログ機能強化
ステータス:
新規
優先度:
高め
担当者:
-
開始日:
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日)
- エラーハンドリングミドルウェア実装(1日)
- 既存コードへの適用(2日)
- メトリクス実装(1日)
- 動作確認とチューニング(1日)
✅ 完了条件¶
- すべてのエラーが構造化ログに記録される
- エラーレスポンスが統一フォーマット
- Prometheusメトリクスが公開される
- グレースフルシャットダウンが動作
- ログローテーションが設定される
表示するデータがありません
操作