本番環境でNode.jsアプリケーションを運用する際、適切なログ出力は障害調査やパフォーマンス分析に不可欠です。console.logによる簡易的なログ出力では、運用時の要件を満たすことができません。本記事では、console.logの限界を理解した上で、Node.jsの主要ロギングライブラリであるpinoとwinstonを使用した構造化ログの実装方法を解説します。ログレベルの設定からログローテーション、本番環境でのログ出力戦略まで、実運用に耐えうるロギングシステムの構築方法を習得できます。
前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の知識を持っていることを前提としています。
- JavaScriptの基礎知識
- Node.jsの基本API理解(
fs、pathなど)
- npmによるパッケージ管理の基本
console.logの限界#
Node.jsの標準出力であるconsole.logは、開発時のデバッグには便利ですが、本番運用では多くの課題があります。
console.logの問題点#
1
2
3
4
5
|
// src/console-example.js
// 典型的なconsole.logによるログ出力
console.log('ユーザー登録処理を開始');
console.log('ユーザーID:', userId, 'メールアドレス:', email);
console.log('エラーが発生しました:', error);
|
上記のようなログ出力には、以下の問題があります。
| 問題点 |
説明 |
| ログレベルがない |
エラー、警告、情報を区別できない |
| タイムスタンプがない |
いつ発生したログか分からない |
| 構造化されていない |
ログ収集ツールでのパース・検索が困難 |
| 出力先の制御ができない |
ファイル出力やローテーションが困難 |
| パフォーマンスへの影響 |
同期的な出力がアプリケーションをブロック |
本番環境で求められるログ要件#
運用に適したロギングシステムには、以下の要件が求められます。
flowchart TB
subgraph 構造化ログの要件
A[ログレベル制御] --> E[運用ログシステム]
B[タイムスタンプ] --> E
C[JSON形式出力] --> E
D[ファイルローテーション] --> E
end
E --> F[ログ収集基盤]
F --> G[Elasticsearch]
F --> H[CloudWatch]
F --> I[Datadog]
- ログレベル: 環境に応じてログ出力量を制御できる
- 構造化フォーマット: JSON形式でログ収集ツールと連携しやすい
- タイムスタンプ: 発生時刻を正確に記録する
- コンテキスト情報: リクエストIDやユーザーIDを付与できる
- 非同期出力: アプリケーションのパフォーマンスに影響を与えない
pinoによる高速構造化ログ#
pinoは、Node.js向けの超高速ロガーです。他のロガーと比較して5倍以上高速であり、本番環境での使用に最適です。
pinoのインストール#
1
|
npm install pino pino-pretty
|
pino-prettyは開発時にログを読みやすく整形するためのパッケージです。
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/pino-basic.js
import pino from 'pino';
// ロガーインスタンスの作成
const logger = pino({
level: 'info'
});
// 各ログレベルでの出力
logger.trace('トレースログ - 最も詳細なログ');
logger.debug('デバッグログ - 開発時の詳細情報');
logger.info('情報ログ - 一般的な情報');
logger.warn('警告ログ - 注意が必要な状況');
logger.error('エラーログ - エラー発生時');
logger.fatal('致命的エラー - アプリケーション継続不能');
|
上記のコードを実行すると、以下のようなJSON形式でログが出力されます。
1
2
3
4
|
{"level":30,"time":1704603600000,"pid":12345,"hostname":"server-01","msg":"情報ログ - 一般的な情報"}
{"level":40,"time":1704603600001,"pid":12345,"hostname":"server-01","msg":"警告ログ - 注意が必要な状況"}
{"level":50,"time":1704603600002,"pid":12345,"hostname":"server-01","msg":"エラーログ - エラー発生時"}
{"level":60,"time":1704603600003,"pid":12345,"hostname":"server-01","msg":"致命的エラー - アプリケーション継続不能"}
|
pinoのログレベル#
pinoは以下の6つのログレベルをサポートしています。数値が小さいほど詳細なログです。
| ログレベル |
数値 |
用途 |
| trace |
10 |
最も詳細なトレース情報 |
| debug |
20 |
デバッグ情報 |
| info |
30 |
一般的な情報 |
| warn |
40 |
警告 |
| error |
50 |
エラー |
| fatal |
60 |
致命的エラー |
設定したレベル以上のログのみが出力されます。例えばlevel: 'warn'と設定すると、warn、error、fatalのみが出力されます。
オブジェクトのログ出力#
pinoでは、第1引数にオブジェクトを渡すことで、構造化されたデータをログに含められます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/pino-object.js
import pino from 'pino';
const logger = pino({ level: 'info' });
// ユーザー操作のログ
logger.info(
{ userId: 'user-123', action: 'login', ip: '192.168.1.1' },
'ユーザーがログインしました'
);
// エラーのログ
const error = new Error('データベース接続エラー');
logger.error(
{ err: error, database: 'postgres', retryCount: 3 },
'データベース接続に失敗しました'
);
|
出力結果は以下のようになります。
1
2
|
{"level":30,"time":1704603600000,"pid":12345,"hostname":"server-01","userId":"user-123","action":"login","ip":"192.168.1.1","msg":"ユーザーがログインしました"}
{"level":50,"time":1704603600001,"pid":12345,"hostname":"server-01","err":{"type":"Error","message":"データベース接続エラー","stack":"Error: データベース接続エラー\n at ..."},"database":"postgres","retryCount":3,"msg":"データベース接続に失敗しました"}
|
子ロガー(Child Logger)#
子ロガーを使用すると、特定のコンテキスト情報を継承したロガーを作成できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// src/pino-child.js
import pino from 'pino';
const logger = pino({ level: 'info' });
// リクエスト処理用の子ロガーを作成
function handleRequest(req) {
const requestLogger = logger.child({
requestId: req.id,
method: req.method,
path: req.path
});
requestLogger.info('リクエスト処理開始');
// 処理中のログも自動的にrequestIdが付与される
requestLogger.info({ userId: req.userId }, 'ユーザー認証完了');
requestLogger.info('リクエスト処理完了');
}
// 使用例
handleRequest({
id: 'req-abc123',
method: 'POST',
path: '/api/users',
userId: 'user-456'
});
|
すべてのログにrequestId、method、pathが自動的に付与されます。
開発環境でのpino-pretty#
本番環境ではJSON形式が適していますが、開発時は読みやすい形式が便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/pino-dev.js
import pino from 'pino';
const isDevelopment = process.env.NODE_ENV !== 'production';
const logger = pino({
level: isDevelopment ? 'debug' : 'info',
transport: isDevelopment
? {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
: undefined
});
logger.info({ userId: 'user-123' }, 'ユーザー情報を取得しました');
|
開発環境では以下のような読みやすい形式で出力されます。
[2026-01-07 12:00:00] INFO: ユーザー情報を取得しました
userId: "user-123"
pinoでのファイル出力#
pinoでファイルにログを出力するには、pino.transportを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src/pino-file.js
import pino from 'pino';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const logger = pino({
level: 'info',
transport: {
targets: [
// コンソール出力
{
target: 'pino/file',
options: { destination: 1 }, // 1 = stdout
level: 'info'
},
// ファイル出力
{
target: 'pino/file',
options: {
destination: path.join(__dirname, '../logs/app.log'),
mkdir: true
},
level: 'info'
},
// エラーログは別ファイル
{
target: 'pino/file',
options: {
destination: path.join(__dirname, '../logs/error.log'),
mkdir: true
},
level: 'error'
}
]
}
});
logger.info('アプリケーションが起動しました');
logger.error('エラーが発生しました');
|
pinoでのログローテーション#
pinoでログローテーションを実現するには、pino-rollパッケージを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// src/pino-rotate.js
import pino from 'pino';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const logger = pino({
level: 'info',
transport: {
target: 'pino-roll',
options: {
file: path.join(__dirname, '../logs/app'),
frequency: 'daily', // 日次ローテーション
extension: '.log',
mkdir: true,
dateFormat: 'yyyy-MM-dd',
limit: { count: 7 } // 7日分保持
}
}
});
logger.info('ログローテーションが設定されました');
|
winstonによる柔軟なログ管理#
winstonは、Node.jsで最も広く使用されているロギングライブラリです。柔軟な設定と豊富なトランスポート(出力先)のサポートが特徴です。
winstonのインストール#
基本的な使い方#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/winston-basic.js
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.Console()
]
});
// 各ログレベルでの出力
logger.error('エラーが発生しました');
logger.warn('警告メッセージ');
logger.info('情報メッセージ');
logger.http('HTTPリクエスト');
logger.verbose('詳細情報');
logger.debug('デバッグ情報');
logger.silly('最も詳細な情報');
|
winstonのログレベル#
winstonはnpmスタイルのログレベルをデフォルトで使用します。数値が小さいほど重要度が高いです。
| ログレベル |
数値 |
用途 |
| error |
0 |
エラー |
| warn |
1 |
警告 |
| info |
2 |
一般的な情報 |
| http |
3 |
HTTPリクエスト |
| verbose |
4 |
詳細情報 |
| debug |
5 |
デバッグ情報 |
| silly |
6 |
最も詳細な情報 |
フォーマットのカスタマイズ#
winstonの強みは、フォーマットを柔軟にカスタマイズできる点です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src/winston-format.js
import winston from 'winston';
const { combine, timestamp, json, errors, printf } = winston.format;
// JSON形式のロガー(本番環境向け)
const productionLogger = winston.createLogger({
level: 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
errors({ stack: true }),
json()
),
defaultMeta: { service: 'api-server' },
transports: [
new winston.transports.Console()
]
});
// カスタム形式のロガー(開発環境向け)
const customFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level.toUpperCase()}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});
const developmentLogger = winston.createLogger({
level: 'debug',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
customFormat
),
transports: [
new winston.transports.Console()
]
});
productionLogger.info('本番環境ログ', { userId: 'user-123' });
developmentLogger.info('開発環境ログ', { userId: 'user-123' });
|
本番環境での出力は以下のようになります。
1
|
{"level":"info","message":"本番環境ログ","service":"api-server","timestamp":"2026-01-07 12:00:00.000","userId":"user-123"}
|
開発環境での出力は以下のようになります。
2026-01-07 12:00:00 [INFO]: 開発環境ログ {"service":"api-server","userId":"user-123"}
複数のトランスポート#
winstonでは、複数の出力先(トランスポート)を設定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src/winston-transports.js
import winston from 'winston';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { combine, timestamp, json, errors, colorize, simple } = winston.format;
const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
errors({ stack: true }),
json()
),
defaultMeta: { service: 'api-server' },
transports: [
// エラーログはerror.logに出力
new winston.transports.File({
filename: path.join(__dirname, '../logs/error.log'),
level: 'error'
}),
// すべてのログはcombined.logに出力
new winston.transports.File({
filename: path.join(__dirname, '../logs/combined.log')
})
]
});
// 本番環境以外ではコンソールにも出力
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
simple()
)
}));
}
logger.info('アプリケーションが起動しました');
logger.error('エラーが発生しました', { code: 'DB_ERROR' });
|
winstonでのログローテーション#
winstonでログローテーションを実現するには、winston-daily-rotate-fileパッケージを使用します。
1
|
npm install winston-daily-rotate-file
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
// src/winston-rotate.js
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { combine, timestamp, json, errors } = winston.format;
// 日次ローテーションのトランスポート設定
const fileRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, '../logs/app-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // 古いログをgzip圧縮
maxSize: '20m', // ファイルサイズ上限
maxFiles: '14d', // 14日分保持
level: 'info'
});
// エラーログ専用のローテーション設定
const errorRotateTransport = new DailyRotateFile({
filename: path.join(__dirname, '../logs/error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d', // エラーログは30日保持
level: 'error'
});
// ローテーションイベントのハンドリング
fileRotateTransport.on('rotate', (oldFilename, newFilename) => {
console.log(`ログファイルがローテーションされました: ${oldFilename} -> ${newFilename}`);
});
fileRotateTransport.on('error', (error) => {
console.error('ログファイルエラー:', error);
});
const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
errors({ stack: true }),
json()
),
defaultMeta: { service: 'api-server' },
transports: [
fileRotateTransport,
errorRotateTransport,
new winston.transports.Console()
]
});
logger.info('ログローテーションが設定されました');
|
子ロガーの作成#
winstonでも子ロガーを作成して、コンテキスト情報を継承できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// src/winston-child.js
import winston from 'winston';
const { combine, timestamp, json } = winston.format;
const logger = winston.createLogger({
level: 'info',
format: combine(timestamp(), json()),
transports: [new winston.transports.Console()]
});
// リクエスト処理用の子ロガーを作成
function handleRequest(req) {
const requestLogger = logger.child({
requestId: req.id,
method: req.method,
path: req.path
});
requestLogger.info('リクエスト処理開始');
requestLogger.info('ユーザー認証完了', { userId: req.userId });
requestLogger.info('リクエスト処理完了');
}
handleRequest({
id: 'req-abc123',
method: 'POST',
path: '/api/users',
userId: 'user-456'
});
|
pinoとwinstonの比較#
両ライブラリにはそれぞれ特徴があり、ユースケースによって選択が異なります。
| 観点 |
pino |
winston |
| パフォーマンス |
非常に高速(他ロガーの5倍以上) |
標準的な速度 |
| 設定の柔軟性 |
シンプル |
非常に柔軟 |
| トランスポート |
基本的な出力先 |
豊富なプラグイン |
| 学習コスト |
低い |
やや高い |
| エコシステム |
成長中 |
成熟している |
| ファイルサイズ |
軽量 |
やや大きい |
| GitHub Stars |
17.1k |
24.3k |
| 週間ダウンロード |
約970万 |
約1,100万 |
選択の指針#
flowchart TD
A[ロガー選択] --> B{パフォーマンス最優先?}
B -->|Yes| C[pino]
B -->|No| D{複雑な出力設定が必要?}
D -->|Yes| E[winston]
D -->|No| F{既存プロジェクトとの互換性?}
F -->|winston使用中| G[winston]
F -->|新規プロジェクト| H[pino推奨]
- pinoを選ぶべきケース: 高スループットが求められるAPI、マイクロサービス、シンプルな設定で済む場合
- winstonを選ぶべきケース: 複雑なログ出力要件、多様なトランスポートが必要、既存エコシステムとの統合
本番環境でのログ出力戦略#
本番環境でのログ運用には、いくつかの重要なポイントがあります。
環境別のログ設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
// src/logger/index.js
import pino from 'pino';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function createLogger() {
const isDevelopment = process.env.NODE_ENV !== 'production';
const logLevel = process.env.LOG_LEVEL || (isDevelopment ? 'debug' : 'info');
const baseOptions = {
level: logLevel,
base: {
service: process.env.SERVICE_NAME || 'app',
version: process.env.APP_VERSION || '1.0.0',
env: process.env.NODE_ENV || 'development'
}
};
if (isDevelopment) {
// 開発環境: 読みやすい形式でコンソール出力
return pino({
...baseOptions,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
});
}
// 本番環境: JSON形式でファイル出力
return pino({
...baseOptions,
transport: {
targets: [
{
target: 'pino/file',
options: { destination: 1 }, // stdout
level: logLevel
},
{
target: 'pino-roll',
options: {
file: path.join(__dirname, '../../logs/app'),
frequency: 'daily',
extension: '.log',
mkdir: true,
dateFormat: 'yyyy-MM-dd',
limit: { count: 14 }
},
level: logLevel
}
]
}
});
}
export const logger = createLogger();
|
リクエストログミドルウェア#
HTTPリクエストのログを記録するミドルウェアを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// src/middleware/request-logger.js
import { randomUUID } from 'node:crypto';
import { logger } from '../logger/index.js';
export function requestLogger(req, res, next) {
const requestId = req.headers['x-request-id'] || randomUUID();
const startTime = Date.now();
// リクエストごとの子ロガーを作成
req.logger = logger.child({
requestId,
method: req.method,
path: req.url,
userAgent: req.headers['user-agent']
});
req.logger.info('リクエスト受信');
// レスポンス完了時のログ
res.on('finish', () => {
const duration = Date.now() - startTime;
req.logger.info({
statusCode: res.statusCode,
duration: `${duration}ms`
}, 'リクエスト完了');
});
// エラー発生時のログ
res.on('error', (error) => {
req.logger.error({ err: error }, 'レスポンスエラー');
});
next();
}
|
エラーログの設計#
エラーログには、障害調査に必要な情報を含めます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// src/utils/error-logger.js
import { logger } from '../logger/index.js';
export function logError(error, context = {}) {
const errorInfo = {
err: {
name: error.name,
message: error.message,
stack: error.stack,
code: error.code
},
...context
};
// エラーの種類に応じてログレベルを変更
if (error.name === 'ValidationError') {
logger.warn(errorInfo, 'バリデーションエラー');
} else if (error.code === 'ECONNREFUSED') {
logger.error(errorInfo, '外部サービス接続エラー');
} else {
logger.error(errorInfo, '予期しないエラー');
}
}
// 使用例
try {
await someOperation();
} catch (error) {
logError(error, {
operation: 'someOperation',
userId: currentUser.id
});
throw error;
}
|
機密情報のマスキング#
ログに機密情報を出力しないよう、リダクション(マスキング)を設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// src/logger/secure-logger.js
import pino from 'pino';
const logger = pino({
level: 'info',
redact: {
paths: [
'password',
'creditCard',
'token',
'authorization',
'*.password',
'*.creditCard',
'user.email',
'headers.authorization'
],
censor: '[REDACTED]'
}
});
// 機密情報がマスキングされる
logger.info({
user: {
id: 'user-123',
email: 'test@example.com', // [REDACTED]に置換
password: 'secret123' // [REDACTED]に置換
},
token: 'jwt-token-xxx' // [REDACTED]に置換
}, 'ユーザー情報');
|
ログ収集基盤との連携#
本番環境では、ログを中央集約型のログ収集基盤に送信することが一般的です。
flowchart LR
A[Node.jsアプリ] -->|stdout| B[コンテナランタイム]
B -->|JSON| C[Fluentd/Fluent Bit]
C --> D[Elasticsearch]
C --> E[CloudWatch Logs]
C --> F[Datadog]
D --> G[Kibana]
E --> H[CloudWatch Insights]
F --> I[Datadog Dashboard]コンテナ環境では、標準出力(stdout)にJSON形式でログを出力し、ログドライバーやサイドカーコンテナで収集する方式が推奨されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/logger/production.js
import pino from 'pino';
// コンテナ環境向けの設定
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// 標準出力にJSON形式で出力
// コンテナのログドライバーが収集
base: {
service: process.env.SERVICE_NAME,
version: process.env.APP_VERSION,
env: process.env.NODE_ENV
},
// タイムスタンプをISO形式で出力
timestamp: pino.stdTimeFunctions.isoTime
});
export { logger };
|
まとめ#
本記事では、Node.jsにおける構造化ログの実装方法を解説しました。
学んだこと#
- console.logの限界: 本番環境では、ログレベル、タイムスタンプ、構造化フォーマットが必要
- pinoの特徴: 高速なJSON形式ログ出力、子ロガー、pino-prettyによる開発時の可読性向上
- winstonの特徴: 柔軟な設定、複数トランスポート、豊富なエコシステム
- ログローテーション: pino-rollやwinston-daily-rotate-fileによる日次ローテーション
- 本番環境戦略: 環境別設定、リクエストログ、機密情報のマスキング
次のステップ#
- ログ収集基盤(Elasticsearch + Kibana、CloudWatch Logs)との連携
- 分散トレーシングの導入(OpenTelemetry)
- アラート設定とログベースの監視
適切なロギングシステムを構築することで、障害調査の効率化とシステムの可観測性向上を実現できます。
参考リンク#