本番環境でアプリケーションを運用する際、ログは障害調査やパフォーマンス分析に不可欠です。NestJSは、アプリケーションのブートストラップや例外表示に使用される組み込みのLoggerクラスを提供しています。本記事では、この組み込みLoggerの活用方法から、LoggerServiceインターフェースを実装したカスタムロガーの作成、JSONログ出力、リクエストログと例外ログの設計パターンまで、本番運用に適した構造化ログシステムの構築方法を解説します。
前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| TypeScript |
5.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
- Module、Controller、Serviceの基本理解
NestJSプロジェクトの作成方法はNestJS入門記事を参照してください。
NestJS組み込みLoggerの基本#
NestJSには@nestjs/commonパッケージにLoggerクラスが含まれています。このクラスを使用することで、一貫したフォーマットでログを出力できます。
Loggerクラスのインスタンス化#
サービスクラス内でLoggerを使用する際は、contextとしてクラス名を渡すことで、ログ出力時にどのクラスから出力されたかを識別できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/users/users.service.ts
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
findAll() {
this.logger.log('Finding all users');
return [];
}
findOne(id: number) {
this.logger.debug(`Finding user with id: ${id}`);
return { id, name: 'Test User' };
}
}
|
上記のコードを実行すると、以下のような形式でログが出力されます。
[Nest] 19096 - 01/06/2026, 9:00:00 PM [UsersService] Finding all users
[UsersService]の部分がcontextとして表示され、どのクラスからのログかが一目で分かります。
ログレベルの種類#
NestJSの組み込みLoggerは、以下の6つのログレベルをサポートしています。
| ログレベル |
メソッド |
用途 |
| log |
logger.log() |
一般的な情報の出力 |
| fatal |
logger.fatal() |
アプリケーションが継続不能な致命的エラー |
| error |
logger.error() |
エラー情報の出力 |
| warn |
logger.warn() |
警告情報の出力 |
| debug |
logger.debug() |
デバッグ情報の出力 |
| verbose |
logger.verbose() |
詳細なデバッグ情報の出力 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/example/example.service.ts
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class ExampleService {
private readonly logger = new Logger(ExampleService.name);
demonstrateLogLevels() {
this.logger.log('This is a log message');
this.logger.fatal('This is a fatal message');
this.logger.error('This is an error message');
this.logger.warn('This is a warning message');
this.logger.debug('This is a debug message');
this.logger.verbose('This is a verbose message');
}
}
|
ログレベルの設定#
アプリケーション起動時に、有効にするログレベルを設定できます。本番環境ではdebugやverboseを無効化し、開発環境ではすべてのログを有効にするといった使い分けが一般的です。
特定のログレベルのみを有効化#
NestFactory.create()の第2引数で、有効にするログレベルを配列で指定します。
1
2
3
4
5
6
7
8
9
10
11
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log'],
});
await app.listen(3000);
}
bootstrap();
|
上記の設定では、debugとverboseのログは出力されません。
ログを完全に無効化#
開発中のテストなど、ログ出力を完全に無効化したい場合はfalseを指定します。
1
2
3
4
|
// src/main.ts
const app = await NestFactory.create(AppModule, {
logger: false,
});
|
環境変数でログレベルを制御#
環境に応じてログレベルを動的に切り替える実装パターンを紹介します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LogLevel } from '@nestjs/common';
async function bootstrap() {
const isProduction = process.env.NODE_ENV === 'production';
const logLevels: LogLevel[] = isProduction
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'];
const app = await NestFactory.create(AppModule, {
logger: logLevels,
});
await app.listen(3000);
}
bootstrap();
|
タイムスタンプ付きログ#
各ログメッセージに前回のログからの経過時間を表示するには、timestampオプションを有効にします。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// src/users/users.service.ts
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name, { timestamp: true });
processData() {
this.logger.log('Starting data processing');
// 何らかの処理
this.logger.log('Data processing completed');
}
}
|
出力例:
[Nest] 19096 - 01/06/2026, 9:00:00 PM [UsersService] Starting data processing
[Nest] 19096 - 01/06/2026, 9:00:00 PM [UsersService] Data processing completed +150ms
+150msの部分が前回のログからの経過時間を示しています。
JSONログ出力(構造化ログ)#
本番環境では、ログ管理システム(AWS CloudWatch、Datadog、Elasticsearchなど)との連携のために、JSON形式での構造化ログ出力が求められることが多くあります。NestJS 11以降では、ConsoleLoggerのjsonオプションでJSON出力を有効化できます。
ConsoleLoggerによるJSON出力#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ConsoleLogger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new ConsoleLogger({
json: true,
}),
});
await app.listen(3000);
}
bootstrap();
|
JSON形式で出力されるログの例:
1
2
3
4
5
6
7
|
{
"level": "log",
"pid": 19096,
"timestamp": 1736172000000,
"message": "Starting Nest application...",
"context": "NestFactory"
}
|
ConsoleLoggerのオプション一覧#
ConsoleLoggerには多くのカスタマイズオプションがあります。
| オプション |
説明 |
デフォルト値 |
| logLevels |
有効なログレベルの配列 |
すべて有効 |
| timestamp |
タイムスタンプ(経過時間)の表示 |
false |
| prefix |
ログメッセージのプレフィックス |
Nest |
| json |
JSON形式での出力 |
false |
| colors |
カラー出力の有効化 |
jsonがfalseの場合true |
| context |
ロガーのコンテキスト |
undefined |
| compact |
単一行での出力 |
true |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ConsoleLogger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const isProduction = process.env.NODE_ENV === 'production';
const app = await NestFactory.create(AppModule, {
logger: new ConsoleLogger({
json: isProduction,
colors: !isProduction,
prefix: 'MyApp',
logLevels: isProduction
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
}),
});
await app.listen(3000);
}
bootstrap();
|
カスタムLoggerServiceの実装#
より高度なログ機能が必要な場合、LoggerServiceインターフェースを実装したカスタムロガーを作成します。
LoggerServiceインターフェース#
LoggerServiceインターフェースは以下のメソッドを定義しています。
1
2
3
4
5
6
7
8
|
export interface LoggerService {
log(message: any, ...optionalParams: any[]): any;
fatal(message: any, ...optionalParams: any[]): any;
error(message: any, ...optionalParams: any[]): any;
warn(message: any, ...optionalParams: any[]): any;
debug?(message: any, ...optionalParams: any[]): any;
verbose?(message: any, ...optionalParams: any[]): any;
}
|
シンプルなカスタムLoggerの実装#
まず、基本的なカスタム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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// src/common/logger/custom-logger.service.ts
import { Injectable, LoggerService, LogLevel } from '@nestjs/common';
@Injectable()
export class CustomLoggerService implements LoggerService {
private context?: string;
setContext(context: string) {
this.context = context;
}
log(message: any, context?: string) {
this.printLog('LOG', message, context);
}
fatal(message: any, context?: string) {
this.printLog('FATAL', message, context);
}
error(message: any, trace?: string, context?: string) {
this.printLog('ERROR', message, context);
if (trace) {
console.error(trace);
}
}
warn(message: any, context?: string) {
this.printLog('WARN', message, context);
}
debug(message: any, context?: string) {
this.printLog('DEBUG', message, context);
}
verbose(message: any, context?: string) {
this.printLog('VERBOSE', message, context);
}
private printLog(level: string, message: any, context?: string) {
const timestamp = new Date().toISOString();
const ctx = context || this.context || 'Application';
console.log(JSON.stringify({
timestamp,
level,
context: ctx,
message,
}));
}
}
|
ConsoleLoggerを継承したカスタムLogger#
既存のConsoleLoggerを継承することで、基本機能を維持しながら必要な部分のみをカスタマイズできます。
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
|
// src/common/logger/extended-logger.service.ts
import { ConsoleLogger, Injectable } from '@nestjs/common';
@Injectable()
export class ExtendedLoggerService extends ConsoleLogger {
private requestId?: string;
setRequestId(requestId: string) {
this.requestId = requestId;
}
protected formatMessage(
logLevel: string,
message: unknown,
pidMessage: string,
formattedLogLevel: string,
contextMessage: string,
timestampDiff: string,
): string {
const requestIdPart = this.requestId ? `[${this.requestId}] ` : '';
const baseMessage = super.formatMessage(
logLevel,
message,
pidMessage,
formattedLogLevel,
contextMessage,
timestampDiff,
);
return requestIdPart + baseMessage;
}
error(message: any, stack?: string, context?: string) {
// エラー時に追加のメタデータを記録
super.error(message, stack, context);
}
}
|
依存性注入を活用したLoggerの実装#
カスタムLoggerでConfigServiceなどの他のサービスを利用する場合、依存性注入(DI)を活用します。
LoggerModuleの作成#
1
2
3
4
5
6
7
8
9
|
// src/common/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { AppLoggerService } from './app-logger.service';
@Module({
providers: [AppLoggerService],
exports: [AppLoggerService],
})
export class LoggerModule {}
|
ConfigServiceを注入した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
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
64
65
66
|
// src/common/logger/app-logger.service.ts
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable({ scope: Scope.TRANSIENT })
export class AppLoggerService extends ConsoleLogger {
private readonly isProduction: boolean;
constructor(private readonly configService: ConfigService) {
super();
this.isProduction = this.configService.get('NODE_ENV') === 'production';
}
log(message: any, context?: string) {
if (this.isProduction) {
// 本番環境ではJSON形式で出力
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'log',
context: context || this.context,
message,
}));
} else {
super.log(message, context);
}
}
error(message: any, stack?: string, context?: string) {
if (this.isProduction) {
console.error(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'error',
context: context || this.context,
message,
stack,
}));
} else {
super.error(message, stack, context);
}
}
warn(message: any, context?: string) {
if (this.isProduction) {
console.warn(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'warn',
context: context || this.context,
message,
}));
} else {
super.warn(message, context);
}
}
debug(message: any, context?: string) {
if (!this.isProduction) {
super.debug(message, context);
}
}
verbose(message: any, context?: string) {
if (!this.isProduction) {
super.verbose(message, context);
}
}
}
|
Transientスコープの活用#
Scope.TRANSIENTを指定することで、Loggerを注入するたびに新しいインスタンスが生成されます。これにより、各サービスで独自のcontextを設定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { AppLoggerService } from '../common/logger/app-logger.service';
@Injectable()
export class UsersService {
constructor(private readonly logger: AppLoggerService) {
this.logger.setContext(UsersService.name);
}
findAll() {
this.logger.log('Finding all users');
return [];
}
}
|
アプリケーション全体でカスタムLoggerを使用#
カスタムLoggerをNestJSのシステムログ(起動時のログや内部エラーログ)にも適用するには、app.useLogger()を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AppLoggerService } from './common/logger/app-logger.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
// DIコンテナからLoggerインスタンスを取得
const logger = app.get(AppLoggerService);
app.useLogger(logger);
await app.listen(3000);
}
bootstrap();
|
bufferLogs: trueを設定すると、カスタムLoggerが設定されるまでログがバッファリングされ、起動時のログも漏れなくカスタムLoggerで処理されます。
リクエストログの設計パターン#
本番環境では、各HTTPリクエストの情報をログに記録することが重要です。Interceptorを使用したリクエストログの実装パターンを紹介します。
リクエストログInterceptorの実装#
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
64
65
66
67
|
// src/common/interceptors/logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Request, Response } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
const requestId = uuidv4();
const { method, originalUrl, ip } = request;
const userAgent = request.get('user-agent') || '';
const startTime = Date.now();
return next.handle().pipe(
tap({
next: () => {
const { statusCode } = response;
const contentLength = response.get('content-length') || 0;
const duration = Date.now() - startTime;
this.logger.log(
JSON.stringify({
requestId,
method,
url: originalUrl,
statusCode,
contentLength,
duration: `${duration}ms`,
ip,
userAgent,
}),
);
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(
JSON.stringify({
requestId,
method,
url: originalUrl,
statusCode: error.status || 500,
duration: `${duration}ms`,
ip,
userAgent,
error: error.message,
}),
);
},
}),
);
}
}
|
uuidパッケージを使用する場合は、事前にインストールしてください。
1
2
|
npm install uuid
npm install -D @types/uuid
|
Interceptorのグローバル登録#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
|
リクエストログの出力例#
リクエスト成功時:
1
2
3
4
5
6
7
8
9
10
|
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"method": "GET",
"url": "/users",
"statusCode": 200,
"contentLength": 256,
"duration": "45ms",
"ip": "127.0.0.1",
"userAgent": "Mozilla/5.0..."
}
|
リクエスト失敗時:
1
2
3
4
5
6
7
8
9
10
|
{
"requestId": "550e8400-e29b-41d4-a716-446655440001",
"method": "POST",
"url": "/users",
"statusCode": 400,
"duration": "12ms",
"ip": "127.0.0.1",
"userAgent": "Mozilla/5.0...",
"error": "Validation failed"
}
|
例外ログの設計パターン#
Exception Filterでエラー情報を詳細にログ出力する実装パターンを紹介します。
例外ログ出力Filter#
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
64
|
// src/common/filters/http-exception-logging.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class HttpExceptionLoggingFilter implements ExceptionFilter {
private readonly logger = new Logger('ExceptionFilter');
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
const stack = exception instanceof Error ? exception.stack : undefined;
// エラーログの出力
const logPayload = {
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
statusCode: status,
message,
stack: status >= 500 ? stack : undefined,
body: request.body,
query: request.query,
params: request.params,
ip: request.ip,
userAgent: request.get('user-agent'),
};
if (status >= 500) {
this.logger.error(JSON.stringify(logPayload));
} else if (status >= 400) {
this.logger.warn(JSON.stringify(logPayload));
}
// クライアントへのレスポンス
const errorResponse = {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
};
response.status(status).json(errorResponse);
}
}
|
グローバルFilterとして登録#
1
2
3
4
5
6
7
8
9
10
11
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionLoggingFilter } from './common/filters/http-exception-logging.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionLoggingFilter());
await app.listen(3000);
}
bootstrap();
|
ログ設計のベストプラクティス#
本番運用を見据えたログ設計のベストプラクティスをまとめます。
構造化ログの設計指針#
flowchart TB
subgraph "ログ設計の4原則"
A[構造化] --> A1["JSON形式で機械可読に"]
B[一貫性] --> B1["すべてのログで同じスキーマ"]
C[追跡性] --> C1["requestIdでリクエストを追跡"]
D[レベル分離] --> D1["環境に応じたレベル制御"]
endログに含めるべき情報#
| 情報 |
必須/任意 |
説明 |
| timestamp |
必須 |
ISO 8601形式の日時 |
| level |
必須 |
ログレベル |
| context |
必須 |
発生元のクラス名・モジュール名 |
| message |
必須 |
ログメッセージ |
| requestId |
推奨 |
リクエストを一意に識別するID |
| userId |
任意 |
認証済みユーザーのID |
| duration |
任意 |
処理時間 |
| stack |
条件付き |
エラー時のスタックトレース |
セキュリティ上の注意点#
ログにはセンシティブな情報を含めないよう注意が必要です。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// src/common/utils/log-sanitizer.ts
export function sanitizeForLog(data: Record<string, any>): Record<string, any> {
const sensitiveKeys = ['password', 'token', 'secret', 'authorization', 'cookie'];
const sanitized = { ...data };
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
|
外部ログライブラリとの統合#
より高度なログ機能が必要な場合、Pinoなどの外部ログライブラリを統合できます。
Pinoとの統合例#
まず、必要なパッケージをインストールします。
1
|
npm install pino pino-pretty nestjs-pino
|
LoggerModuleを設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
},
}),
],
})
export class AppModule {}
|
まとめ#
本記事では、NestJSにおけるログ設計の全体像を解説しました。
- 組み込み
Loggerクラスの基本的な使い方とログレベルの設定
ConsoleLoggerのJSONログ出力オプション
LoggerServiceインターフェースを実装したカスタムロガーの作成
- 依存性注入を活用したロガーの実装とTransientスコープの活用
- Interceptorを使用したリクエストログの設計パターン
- Exception Filterを使用した例外ログの設計パターン
- ログ設計のベストプラクティスとセキュリティ上の注意点
適切に設計された構造化ログは、本番環境での障害調査やパフォーマンス分析に大きく貢献します。アプリケーションの規模や運用要件に応じて、組み込みLoggerの活用から外部ライブラリとの統合まで、最適な方法を選択してください。
参考リンク#