Morgan
Express.js専用のHTTPリクエストロギングミドルウェア。アクセスログの生成に特化し、common、combined、devなどの定義済みフォーマットを提供。他のロギングライブラリ(WinstonやPino)と組み合わせて使用されることが多い。
GitHub概要
スター8,080
ウォッチ91
フォーク536
作成日:2014年2月8日
言語:JavaScript
ライセンス:MIT License
トピックス
expressjavascriptloggernodejs
スター履歴
データ取得日時: 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);