本番環境でアプリケーションを運用する際、ログは障害調査やパフォーマンス分析に不可欠です。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();

上記の設定では、debugverboseのログは出力されません。

ログを完全に無効化

開発中のテストなど、ログ出力を完全に無効化したい場合は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以降では、ConsoleLoggerjsonオプションで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の活用から外部ライブラリとの統合まで、最適な方法を選択してください。

参考リンク