NestJS Winstonでロギングを整える
nest-winston を使って構造化ログ(JSON)を出力し、ログレベル・フォーマットを環境ごとに切り替える
NestJSの基礎まで終えている前提。
この記事では、NestJS 標準の Logger を Winston に差し替えて、開発環境では見やすいカラーログ、本番環境では JSON ログを出力する最小構成を実装する。
この記事でやること
winston/nest-winstonを導入するAppModuleにWinstonModuleを設定し、標準 Logger を差し替える- 開発環境:カラー付きテキストログ、本番環境:JSON ログを切り替える
UsersServiceにロガーを注入してログを出力する- 実際に動かしてログを確認する
触るファイル一覧
api-sample/
└─ src/
├─ app.module.ts # WinstonModule を設定・NestJS Logger を差し替え
├─ main.ts # app.useLogger() を追加
└─ users/
└─ users.service.ts # Logger を注入してログ出力1. パッケージを追加する
pnpm の場合。
pnpm add winston nest-winstonnpm の場合。
npm install winston nest-winston2. AppModule に WinstonModule を設定する
編集するファイルは api-sample/src/app.module.ts。
import { Module } from "@nestjs/common";
import { WinstonModule } from "nest-winston";
import * as winston from "winston";
import { UsersModule } from "./users/users.module";
const isDev = process.env.NODE_ENV !== "production";
@Module({
imports: [
WinstonModule.forRoot({
level: isDev ? "debug" : "info",
transports: [
new winston.transports.Console({
format: isDev
? winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: "HH:mm:ss" }),
winston.format.printf(
({ level, message, timestamp, context }) =>
`${timestamp} [${context ?? "App"}] ${level}: ${message}`
)
)
: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
],
}),
UsersModule,
],
})
export class AppModule {}ポイント
NODE_ENVがproduction以外の場合は開発向けフォーマット(カラー + 短いタイムスタンプ)を使う。- 本番では
winston.format.json()で1行JSONを出力する。CloudWatch / Datadog などのログ収集ツールが自動パースできる形式。 level: "debug"にするとdebug以上すべてを出力する。本番で"info"にするとdebugログが出ない。
3. main.ts でアプリのロガーを差し替える
編集するファイルは api-sample/src/main.ts。
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// NestJS 内部ログ(起動ログ等)も Winston に差し替える
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
await app.listen(3000);
}
bootstrap();WINSTON_MODULE_NEST_PROVIDER は nest-winston が提供するトークン。
これを useLogger に渡すことで、フレームワーク自身のログ(モジュール起動ログ等)も Winston 経由で出力される。
4. UsersService にロガーを注入する
編集するファイルは api-sample/src/users/users.service.ts。
import { Injectable, Inject, NotFoundException } from "@nestjs/common";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { Logger } from "winston";
@Injectable()
export class UsersService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {}
findAll() {
this.logger.info("全ユーザーを取得", { context: "UsersService" });
return [{ id: 1, name: "Alice" }];
}
findOne(id: number) {
this.logger.debug(`ユーザーを取得: id=${id}`, { context: "UsersService" });
const user = { id, name: "Alice" };
if (!user) {
this.logger.warn(`ユーザーが見つからない: id=${id}`, { context: "UsersService" });
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
}5. 動かして確認する
サーバーを起動する。
pnpm start:dev開発環境の出力例
12:34:56 [NestFactory] info: Starting Nest application...
12:34:56 [InstanceLoader] info: AppModule dependencies initialized
12:34:56 [RoutesResolver] info: UsersController {/users}:
12:34:56 [NestApplication] info: Nest application successfully startedGET /users を叩く。
curl http://localhost:3000/users12:34:57 [UsersService] info: 全ユーザーを取得本番環境の出力例(JSON)
NODE_ENV=production で起動すると1行JSONになる。
{"level":"info","message":"全ユーザーを取得","context":"UsersService","timestamp":"2026-06-05T03:34:57.123Z"}ログレベルの使い分け
| レベル | 用途の目安 |
|---|---|
error | 予期しないエラー、例外キャッチ |
warn | 異常ではないが注意が必要な状態(リソース不足など) |
info | 正常な操作の記録(リクエスト受付、処理完了など) |
debug | 開発時のデバッグ情報(変数の中身など) |
本番では level: "info" にして debug ログを出力しないのが一般的。
ファイルへの出力を追加する(オプション)
コンソールに加えてファイルにも出力したい場合は transports に追加する。
transports: [
new winston.transports.Console({ ... }),
new winston.transports.File({
filename: "logs/error.log",
level: "error", // error ログのみ記録
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
new winston.transports.File({
filename: "logs/combined.log", // 全レベルを記録
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
],logs/ ディレクトリは事前に作成するか、.gitignore に追加しておく。
mkdir -p logs
echo "logs/" >> .gitignoreSLF4J / Logbackとの対応関係
Java(Spring Boot)経験者向けの比較。概念的に対応しているが、直接の関係はない。
| Java(Spring Boot) | Node.js(NestJS) | 役割 |
|---|---|---|
| SLF4J | LoggerService(@nestjs/common) | ログ API の窓口(インターフェース)。アプリコードはここだけ触る |
| Logback | Winston | 実際にログを書き出す実装エンジン |
| SLF4J Binding | nest-winston | インターフェースと実装をつなぐ連結部品 |
logback.xml | WinstonModule.forRoot() | フォーマット・出力先・レベルの設定 |
どちらも「アプリコードは抽象的な窓口(SLF4J / LoggerService)に対して書き、実装(Logback / Winston)は後から差し替えられる」という設計思想が共通している。
NestJS でこの設計を活かすには WINSTON_MODULE_NEST_PROVIDER を使う。すると、アプリコードが LoggerService インターフェース越しにログを書くため、Winston を別のライブラリに差し替えても呼び出し側のコードを変える必要がない。
// LoggerService インターフェース越しに使う(NestJS の SLF4J 的な使い方)
@Injectable()
export class UsersService {
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService
) {}
findAll() {
this.logger.log('全ユーザーを取得', 'UsersService'); // .log() は LoggerService の共通メソッド
}
}WinstonのJSON出力をFluentdで収集する
直接の関係がある組み合わせ。Winston が出力した JSON ログを Fluentd が読み取り、Elasticsearch などに転送する構成が実際の本番環境でよく使われる。
なぜ本番でJSON形式にするのか
この記事の設定で winston.format.json() を使っているのは、まさに Fluentd などの収集ツールがそのままパースできる形にするためです。
{"level":"info","message":"全ユーザーを取得","context":"UsersService","timestamp":"2026-06-05T03:34:57.123Z"}テキスト形式のログを Fluentd に読み込ませると、Fluentd 側でパース設定(正規表現など)が必要になる。JSON 形式なら設定なしで自動パースできる。
全体の流れ
NestJS(Winston)
↓ JSON ログを標準出力またはファイルに書き出す
Fluentd(収集エージェント)
↓ ログを読み取り、Elasticsearch などへ転送する
Elasticsearch + Kibana
↓ 蓄積・検索・可視化コンテナ環境(Docker / ECS)での構成
コンテナでは「標準出力に書く → コンテナランタイムが収集する」が基本。Winston のコンソール Transport だけ設定し、ファイル出力は不要。
// 本番:標準出力にJSONを書くだけ。収集はFluentd / CloudWatch Agentに任せる
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
})ECS の場合、タスク定義のログドライバーを awslogs にするだけで CloudWatch Logs が自動収集してくれるため、Fluentd エージェント自体を置く必要もなくなる。
サーバー(EC2 / オンプレ)での構成
ファイル Transport でログをファイルに書き出し、同じサーバー上の Fluentd エージェントがそのファイルを監視して転送する。
// Winstonはファイルに書き出すだけ
new winston.transports.File({
filename: '/var/log/app/app.log',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
})# Fluentd設定(fluent.conf):Winstonが書いたファイルを監視して転送する
<source>
@type tail
path /var/log/app/app.log
pos_file /var/log/td-agent/app.pos
tag app.log
<parse>
@type json # Winston の JSON 出力をそのままパースできる
</parse>
</source>
<match app.log>
@type elasticsearch
host elasticsearch.example.com
port 9200
index_name nestjs-app-logs
</match>役割分担のまとめ
| ツール | 役割 | 設定場所 |
|---|---|---|
| Winston | JSON ログを標準出力 / ファイルに書き出す | WinstonModule.forRoot() |
| Fluentd | ログファイルを監視して Elasticsearch 等に転送する | fluent.conf |
| Elasticsearch | ログを蓄積・全文検索できる形で保存する | Elasticsearch 設定 |
| Kibana | ログを検索・グラフ化する UI | Kibana 設定 |
Winston は「書き出す」だけに専念し、「どこに集めてどう見るか」は Fluentd 以降が担う。この役割分担がある限り、Winston 側のコードを変えずに転送先(Elasticsearch → CloudWatch など)を切り替えることもできる。