Morgan

Express.js専用のHTTPリクエストロギングミドルウェア。アクセスログの生成に特化し、common、combined、devなどの定義済みフォーマットを提供。他のロギングライブラリ(WinstonやPino)と組み合わせて使用されることが多い。

HTTPクライアントExpress.jsミドルウェアアクセスログNode.js

GitHub概要

expressjs/morgan

HTTP request logger middleware for node.js

スター8,080
ウォッチ91
フォーク536
作成日:2014年2月8日
言語:JavaScript
ライセンス:MIT License

トピックス

expressjavascriptloggernodejs

スター履歴

expressjs/morgan Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

Morgan

概要

MorganはExpress.js専用のHTTPリクエストロギングミドルウェアです。Webアプリケーションのアクセスログ生成に特化し、common、combined、devなどの定義済みフォーマットを提供します。他のロギングライブラリ(WinstonやPino)と組み合わせて使用されることが多く、Express.jsアプリケーションにおけるHTTP監視のデファクトスタンダードとして確立されています。

詳細

Morgan 2025年版は、Express.jsエコシステムにおける不可欠な存在として地位を維持しています。軽量なミドルウェアとして動作し、HTTPリクエストの詳細情報を効率的に記録。リクエストメソッド、URL、ステータスコード、レスポンス時間、IPアドレス、ユーザーエージェントなどの包括的な情報を取得できます。カスタムトークン機能により、独自のログフォーマットを作成可能で、ファイル出力や外部ログサービスとの統合もサポートしています。

主な特徴

  • 定義済みフォーマット: Apache Common/Combined、dev、short、tinyなど多様な形式
  • カスタムトークン: 独自のログフィールドを定義可能
  • 条件付きロギング: 特定のリクエストのみをログ出力
  • ストリーム対応: ファイルやクラウドサービスへの直接出力
  • 軽量設計: 最小限のオーバーヘッドで高速動作
  • Express.js統合: seamlessなミドルウェア統合

メリット・デメリット

メリット

  • Express.jsアプリケーションでの設定の簡単さと即座の利用可能性
  • 豊富な定義済みフォーマットにより迅速なセットアップが可能
  • カスタムトークン機能による柔軟なログカスタマイズ
  • 他の主要ロギングライブラリとの優れた互換性と統合性
  • 軽量でパフォーマンスへの影響が最小限
  • 長期間にわたる安定性と信頼性の実績

デメリット

  • Express.js専用のため他のNode.jsフレームワークでは使用不可
  • 単体では基本的なHTTPロギングのみで高度な機能は限定的
  • 大量のトラフィックでのパフォーマンス制約
  • ログ解析機能は別途外部ツールが必要
  • リアルタイム監視機能の不足
  • 複雑なフィルタリング機能の制限

参考ページ

書き方の例

インストールと基本セットアップ

# Morganのインストール
npm install morgan

# Express.jsと一緒にインストール
npm install express morgan

# TypeScript使用時の型定義
npm install --save-dev @types/morgan

# 動作確認
node -p "require('morgan')"

基本的な使用方法(定義済みフォーマット)

const express = require('express');
const morgan = require('morgan');

const app = express();

// 開発環境用(カラー表示付き)
app.use(morgan('dev'));
// GET / 200 51.267 ms - 2

// Apache Combined Log Format(本番環境推奨)
app.use(morgan('combined'));
// 127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET / HTTP/1.1" 200 2326 "-" "Mozilla/5.0..."

// Apache Common Log Format
app.use(morgan('common'));
// 127.0.0.1 - - [25/Dec/2023:10:00:00 +0000] "GET / HTTP/1.1" 200 2326

// 短縮フォーマット
app.use(morgan('short'));
// 127.0.0.1 - GET / HTTP/1.1 200 2326 - 1.234 ms

// 最小フォーマット
app.use(morgan('tiny'));
// GET / 200 2326 - 1.234 ms

// 基本的なルート
app.get('/', (req, res) => {
  res.send('Hello, Morgan Logging!');
});

app.get('/api/users', (req, res) => {
  res.json({ users: ['Alice', 'Bob'] });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

カスタムフォーマットとトークン

const express = require('express');
const morgan = require('morgan');

const app = express();

// カスタムトークンの定義
morgan.token('id', (req) => {
  return req.headers['x-request-id'] || 'unknown';
});

morgan.token('host', (req) => {
  return req.hostname;
});

morgan.token('body', (req) => {
  return JSON.stringify(req.body);
});

morgan.token('query', (req) => {
  return JSON.stringify(req.query);
});

// カスタムフォーマット文字列
const customFormat = ':method :host :url :status :res[content-length] - :response-time ms [ID: :id]';

app.use(express.json()); // bodyパーサー
app.use(morgan(customFormat));

// より詳細なカスタムフォーマット
const detailedFormat = [
  ':method :url',
  'Status: :status',
  'Size: :res[content-length]',
  'Time: :response-time ms',
  'Host: :host',
  'User-Agent: :user-agent',
  'Query: :query'
].join(' | ');

app.use(morgan(detailedFormat));

// 関数形式でのカスタムフォーマット
morgan.format('detailed', (tokens, req, res) => {
  return [
    new Date().toISOString(),
    tokens.method(req, res),
    tokens.url(req, res),
    tokens.status(req, res),
    tokens.res(req, res, 'content-length'), '-',
    tokens['response-time'](req, res), 'ms',
    'IP:', tokens['remote-addr'](req, res),
    'Referrer:', tokens.referrer(req, res)
  ].join(' ');
});

app.use(morgan('detailed'));

app.get('/custom', (req, res) => {
  res.json({ message: 'Custom logging example' });
});

app.listen(3000);

ファイル出力とローテーション

const express = require('express');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream'); // npm install rotating-file-stream

const app = express();

// ログディレクトリの作成
const logDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir);
}

// 単純なファイル出力
const accessLogStream = fs.createWriteStream(path.join(logDir, 'access.log'), { flags: 'a' });
app.use(morgan('combined', { stream: accessLogStream }));

// 日次ローテーション
const dailyLogStream = rfs.createStream('access.log', {
  interval: '1d', // 1日ごとにローテーション
  path: logDir,
  compress: 'gzip' // 古いログファイルを圧縮
});

app.use(morgan('combined', { stream: dailyLogStream }));

// サイズベースローテーション
const sizeBasedStream = rfs.createStream('access.log', {
  size: '10M', // 10MBごとにローテーション
  maxFiles: 5, // 最大5ファイル保持
  path: logDir
});

app.use(morgan('combined', { stream: sizeBasedStream }));

// 複数出力先(コンソール + ファイル)
app.use(morgan('dev')); // コンソール用
app.use(morgan('combined', { stream: accessLogStream })); // ファイル用

// 条件付きログ出力
app.use(morgan('combined', {
  stream: accessLogStream,
  skip: (req, res) => {
    // 成功レスポンス(2xx)はファイルに出力しない
    return res.statusCode < 400;
  }
}));

// エラーログ専用ストリーム
const errorLogStream = fs.createWriteStream(path.join(logDir, 'error.log'), { flags: 'a' });
app.use(morgan('combined', {
  stream: errorLogStream,
  skip: (req, res) => res.statusCode < 400 // 4xx, 5xxエラーのみ
}));

app.get('/error', (req, res) => {
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000);

Winston・Pinoとの統合

const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const pino = require('pino');

const app = express();

// Winston統合
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
    new winston.transports.Console()
  ]
});

// Winstonストリーム
const winstonStream = {
  write: (message) => {
    logger.info(message.trim());
  }
};

app.use(morgan('combined', { stream: winstonStream }));

// Pino統合
const pinoLogger = pino({
  level: 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true
    }
  }
});

const pinoStream = {
  write: (message) => {
    pinoLogger.info(message.trim());
  }
};

app.use(morgan('combined', { stream: pinoStream }));

// 構造化ログとの統合
morgan.format('json', (tokens, req, res) => {
  return JSON.stringify({
    timestamp: new Date().toISOString(),
    method: tokens.method(req, res),
    url: tokens.url(req, res),
    status: Number(tokens.status(req, res)),
    contentLength: tokens.res(req, res, 'content-length'),
    responseTime: Number(tokens['response-time'](req, res)),
    remoteAddr: tokens['remote-addr'](req, res),
    userAgent: tokens['user-agent'](req, res),
    referrer: tokens.referrer(req, res)
  });
});

const structuredStream = {
  write: (message) => {
    const logData = JSON.parse(message);
    logger.info('HTTP Request', logData);
  }
};

app.use(morgan('json', { stream: structuredStream }));

app.get('/winston', (req, res) => {
  res.json({ logger: 'winston' });
});

app.get('/pino', (req, res) => {
  res.json({ logger: 'pino' });
});

app.listen(3000);

高度な設定とフィルタリング

const express = require('express');
const morgan = require('morgan');

const app = express();

// 環境別設定
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';

if (isDevelopment) {
  // 開発環境:詳細なカラー出力
  app.use(morgan('dev'));
} else if (isProduction) {
  // 本番環境:構造化ログ
  app.use(morgan('combined'));
}

// 条件付きロギング
app.use(morgan('combined', {
  skip: (req, res) => {
    // ヘルスチェックエンドポイントをスキップ
    if (req.url === '/health') return true;
    
    // 静的ファイルをスキップ
    if (req.url.match(/\.(css|js|png|jpg|ico)$/)) return true;
    
    // 2xxレスポンスをスキップ(エラーのみロギング)
    if (res.statusCode >= 200 && res.statusCode < 300) return true;
    
    return false;
  }
}));

// IPアドレス別フィルタリング
app.use(morgan('combined', {
  skip: (req) => {
    const clientIP = req.ip || req.connection.remoteAddress;
    const skipIPs = ['127.0.0.1', '::1', '10.0.0.0']; // 内部IPをスキップ
    return skipIPs.includes(clientIP);
  }
}));

// レスポンス時間による条件付きロギング
app.use(morgan('combined', {
  skip: (req, res) => {
    const responseTime = parseFloat(res.get('X-Response-Time')) || 0;
    return responseTime < 100; // 100ms未満のリクエストはスキップ
  }
}));

// カスタムフィルタ関数
const isAPIRequest = (req) => req.url.startsWith('/api/');
const isStaticRequest = (req) => req.url.match(/\.(css|js|png|jpg|ico|woff|woff2)$/);

// API専用ロガー
app.use(morgan('detailed', {
  skip: (req) => !isAPIRequest(req)
}));

// 静的ファイル専用ロガー(簡略化)
app.use(morgan('tiny', {
  skip: (req) => !isStaticRequest(req)
}));

// セキュリティログ(ログイン試行等)
morgan.format('security', (tokens, req, res) => {
  return JSON.stringify({
    type: 'security',
    timestamp: new Date().toISOString(),
    method: tokens.method(req, res),
    url: tokens.url(req, res),
    status: tokens.status(req, res),
    ip: tokens['remote-addr'](req, res),
    userAgent: tokens['user-agent'](req, res),
    suspicious: res.statusCode === 401 || res.statusCode === 403
  });
});

app.use('/auth', morgan('security'));

// レート制限ログ
app.use(morgan('combined', {
  skip: (req, res) => res.statusCode !== 429 // Too Many Requestsのみ
}));

app.get('/api/data', (req, res) => {
  res.json({ data: 'API response' });
});

app.get('/health', (req, res) => {
  res.json({ status: 'OK' });
});

app.listen(3000);

分析とメトリクス

const express = require('express');
const morgan = require('morgan');
const fs = require('fs');

const app = express();

// メトリクス収集
let requestCount = 0;
let totalResponseTime = 0;
let errorCount = 0;

morgan.token('metrics', (req, res) => {
  requestCount++;
  const responseTime = parseFloat(res.get('X-Response-Time')) || 0;
  totalResponseTime += responseTime;
  
  if (res.statusCode >= 400) {
    errorCount++;
  }
  
  return `[Metrics: requests=${requestCount}, avgTime=${(totalResponseTime/requestCount).toFixed(2)}ms, errors=${errorCount}]`;
});

app.use(morgan(':method :url :status :metrics'));

// パフォーマンス分析
morgan.format('performance', (tokens, req, res) => {
  const responseTime = parseFloat(tokens['response-time'](req, res));
  const contentLength = parseInt(tokens.res(req, res, 'content-length') || '0');
  
  return JSON.stringify({
    url: tokens.url(req, res),
    method: tokens.method(req, res),
    status: parseInt(tokens.status(req, res)),
    responseTime: responseTime,
    contentLength: contentLength,
    throughput: contentLength / responseTime, // bytes per ms
    slow: responseTime > 1000, // 1秒以上は低速とマーク
    timestamp: new Date().toISOString()
  });
});

const performanceLogStream = fs.createWriteStream('logs/performance.log', { flags: 'a' });
app.use(morgan('performance', { stream: performanceLogStream }));

// 統計レポート生成
setInterval(() => {
  const avgResponseTime = requestCount > 0 ? (totalResponseTime / requestCount).toFixed(2) : 0;
  const errorRate = requestCount > 0 ? ((errorCount / requestCount) * 100).toFixed(2) : 0;
  
  console.log(`
=== Server Statistics ===
Total Requests: ${requestCount}
Average Response Time: ${avgResponseTime}ms
Error Rate: ${errorRate}%
Total Errors: ${errorCount}
========================
  `);
}, 60000); // 1分ごと

// ユーザーエージェント分析
const userAgentStats = {};
morgan.token('user-agent-stats', (req) => {
  const ua = req.get('User-Agent') || 'Unknown';
  const browser = ua.includes('Chrome') ? 'Chrome' : 
                 ua.includes('Firefox') ? 'Firefox' : 
                 ua.includes('Safari') ? 'Safari' : 'Other';
  
  userAgentStats[browser] = (userAgentStats[browser] || 0) + 1;
  return `[UA: ${JSON.stringify(userAgentStats)}]`;
});

app.use(morgan(':method :url :user-agent-stats'));

app.get('/stats', (req, res) => {
  res.json({
    requests: requestCount,
    averageResponseTime: (totalResponseTime / requestCount).toFixed(2),
    errorRate: ((errorCount / requestCount) * 100).toFixed(2),
    userAgents: userAgentStats
  });
});

app.listen(3000);