NestJS 監視運用:SLI/SLO とダッシュボード設計(Prometheus/Grafana/Loki)

本番環境で NestJS アプリケーションを安定運用するには、単なる監視だけではなく「どの指標をどう測るか」が重要です。この記事では、サービスレベル指標(SLI)とサービスレベル目標(SLO)の定義から、Prometheus と Grafana、Loki を組み合わせた実践的なダッシュボード設計まで、初心者の方にも分かりやすく解説します。
エラー率や応答時間といったメトリクスを可視化し、問題が起きる前に気づくための監視体制を、一緒に構築していきましょう。
背景
SLI と SLO が必要な理由
従来の監視では「サーバーが落ちていないか」「CPU 使用率は何%か」といったインフラ視点の指標を中心に見ていました。しかし、それだけでは ユーザー体験が良好かどうか を判断できません。
SRE(Site Reliability Engineering)の考え方では、ユーザーにとって重要な指標を定量化し、それに基づいて目標を設定します。これが SLI(Service Level Indicator)と SLO(Service Level Objective)です。
以下の図は、従来型監視と SLI/SLO ベースの監視の違いを示します。
mermaidflowchart TB
subgraph old["従来型監視"]
cpu["CPU使用率"]
mem["メモリ使用率"]
disk["ディスク容量"]
end
subgraph new["SLI/SLOベース監視"]
availability["可用性<br/>(エラー率)"]
latency["レイテンシ<br/>(応答時間)"]
throughput["スループット<br/>(リクエスト数)"]
end
old -.->|インフラ視点| infra["サーバー健全性"]
new -.->|ユーザー視点| user["ユーザー体験"]
図で理解できる要点:
- 従来型はインフラリソースを監視
- SLI/SLO はユーザー体験を指標化
- ユーザー視点での品質保証が可能
NestJS における監視の役割
NestJS は Express または Fastify をベースとした TypeScript フレームワークで、マイクロサービスや REST API の構築に広く使われています。本番運用では、以下のような課題が生じます。
- エンドポイントごとの応答速度: どの API が遅いのか
- エラー発生率: どのルートでエラーが多いか
- 依存サービスの影響: データベースや外部 API の遅延がアプリに波及していないか
これらを可視化するために、Prometheus でメトリクスを収集し、Grafana でダッシュボード化、Loki でログを集約する構成が有効です。
mermaidflowchart LR
nestjs["NestJS アプリ"] -->|メトリクス| prom["Prometheus"]
nestjs -->|ログ| loki["Loki"]
prom -->|データソース| grafana["Grafana"]
loki -->|データソース| grafana
grafana -->|可視化| user["運用者"]
図の要点:
- NestJS から Prometheus と Loki にデータを送信
- Grafana で一元的にダッシュボード化
- 運用者は一つの画面でメトリクスとログを確認可能
課題
SLI/SLO を定義しないとどうなるか
目標が曖昧なまま監視を導入すると、以下のような問題が発生します。
# | 課題 | 影響 |
---|---|---|
1 | アラートが多すぎて無視される | 重要な障害を見逃す |
2 | 何を改善すべきか分からない | 優先順位が不明確 |
3 | ユーザー体験の劣化に気づけない | 信頼性低下 |
SLO を設定することで「99.9% の可用性を保つ」「95 パーセンタイルで応答時間 200ms 以内」といった 明確な基準 が生まれ、チーム全体で改善方針を共有できます。
NestJS でのメトリクス収集の難しさ
NestJS は標準でメトリクス出力機能を持っていません。以下のような工夫が必要です。
- インターセプターやミドルウェアでメトリクス収集: リクエストごとに HTTP ステータス、レイテンシ、メソッド、パスを記録
- Prometheus フォーマットでエクスポート:
/metrics
エンドポイントを用意し、Prometheus がスクレイプできる形式で公開 - ログとメトリクスの統合: エラー発生時にログと紐づけ、原因調査を迅速化
これらを手動で実装すると手間がかかるため、ライブラリやベストプラクティスを活用することが重要です。
mermaidflowchart TB
req["HTTPリクエスト"] --> interceptor["メトリクス<br/>インターセプター"]
interceptor -->|収集| counter["リクエストカウンタ"]
interceptor -->|収集| histogram["レイテンシヒストグラム"]
counter --> metrics["/metricsエンドポイント"]
histogram --> metrics
metrics -->|スクレイプ| prom["Prometheus"]
図の補足:
- インターセプターで各リクエストのメトリクスを収集
- カウンターとヒストグラムに記録
/metrics
を通じて Prometheus が定期的に取得
解決策
SLI と SLO の定義方法
まず、ユーザーにとって重要な指標を洗い出します。NestJS の REST API を例にすると、以下が一般的です。
# | SLI | 説明 | 測定方法 |
---|---|---|---|
1 | 可用性 | 成功したリクエスト / 全リクエスト | HTTP ステータスコード 2xx, 3xx を成功とカウント |
2 | レイテンシ | 95 パーセンタイルの応答時間 | ヒストグラムで p95 を算出 |
3 | スループット | 秒あたりのリクエスト数 | リクエストカウンターの増加率 |
次に、**SLO(目標値)**を設定します。
- 可用性: 99.9%(月間約 43 分までのダウンタイムを許容)
- レイテンシ: p95 で 200ms 以内
- スループット: 最低 100 req/s を維持
これらを Grafana で可視化し、SLO を下回ったらアラートを発火させる仕組みを作ります。
Prometheus と Grafana、Loki の役割分担
監視スタックの各コンポーネントは以下のように役割を分担します。
Prometheus:
- 時系列データの収集・保存
- メトリクスのスクレイプ(定期取得)
- アラートルールの評価
Grafana:
- ダッシュボードの構築
- 複数データソース(Prometheus + Loki)の統合表示
- アラート通知(Slack、Email など)
Loki:
- ログの収集・保存
- ログクエリと検索
- メトリクスと連携したログ調査
mermaidflowchart LR
app["NestJSアプリ"] -->|メトリクス| prom["Prometheus"]
app -->|ログ| loki["Loki"]
prom -->|クエリ| grafana["Grafana"]
loki -->|クエリ| grafana
grafana -->|アラート| slack["Slack"]
grafana -->|ダッシュボード| operator["運用者"]
図の要点:
- Prometheus と Loki がそれぞれメトリクスとログを担当
- Grafana が統合ビューを提供
- アラートは Slack などに通知
NestJS でのメトリクス実装
NestJS でメトリクスを収集するには、@willsoto/nestjs-prometheus
パッケージが便利です。
パッケージのインストール
bashyarn add @willsoto/nestjs-prometheus prom-client
モジュールの設定
app.module.ts
に Prometheus モジュールをインポートします。
typescriptimport { Module } from '@nestjs/common';
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
@Module({
imports: [
PrometheusModule.register({
// /metrics エンドポイントを自動で作成
defaultMetrics: {
enabled: true,
},
}),
// 他のモジュール
],
})
export class AppModule {}
これで、NestJS アプリケーションに /metrics
エンドポイントが追加され、デフォルトのシステムメトリクス(CPU、メモリなど)が取得できるようになります。
カスタムメトリクスの追加
リクエストごとのメトリクスを収集するため、インターセプターを作成します。
typescriptimport {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Counter, Histogram } from 'prom-client';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
上記は必要なインポート文です。次に、インターセプター本体を実装します。
typescript@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(
@InjectMetric('http_requests_total')
private readonly requestCounter: Counter<string>,
@InjectMetric('http_request_duration_seconds')
private readonly requestHistogram: Histogram<string>
) {}
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, path } = request;
const start = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = (Date.now() - start) / 1000;
const response = context
.switchToHttp()
.getResponse();
const statusCode = response.statusCode;
// カウンターを増加
this.requestCounter.inc({
method,
path,
status: statusCode,
});
// ヒストグラムに記録
this.requestHistogram.observe(
{ method, path },
duration
);
},
error: (err) => {
const duration = (Date.now() - start) / 1000;
// エラー時も記録
this.requestCounter.inc({
method,
path,
status: 500,
});
this.requestHistogram.observe(
{ method, path },
duration
);
},
})
);
}
}
このインターセプターは、各リクエストの メソッド、パス、ステータスコード、処理時間 を記録します。
メトリクスの登録
app.module.ts
でメトリクスを登録します。
typescriptimport { Module } from '@nestjs/common';
import {
PrometheusModule,
makeCounterProvider,
makeHistogramProvider,
} from '@willsoto/nestjs-prometheus';
@Module({
imports: [PrometheusModule.register()],
providers: [
makeCounterProvider({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'path', 'status'],
}),
makeHistogramProvider({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'path'],
buckets: [0.1, 0.3, 0.5, 1, 2, 5],
}),
MetricsInterceptor,
],
})
export class AppModule {}
バケットは、ヒストグラムの区間を定義します。ここでは 0.1 秒、0.3 秒、0.5 秒、1 秒、2 秒、5 秒という区間を設定しています。
インターセプターのグローバル適用
main.ts
でインターセプターを全エンドポイントに適用します。
typescriptimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MetricsInterceptor } from './metrics.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// グローバルインターセプター
const metricsInterceptor = app.get(MetricsInterceptor);
app.useGlobalInterceptors(metricsInterceptor);
await app.listen(3000);
}
bootstrap();
これで、すべてのリクエストでメトリクスが自動収集されるようになります。
Prometheus の設定
Prometheus は定期的に /metrics
をスクレイプしてデータを収集します。以下は prometheus.yml
の設定例です。
yamlglobal:
scrape_interval: 15s # 15秒ごとにスクレイプ
scrape_configs:
- job_name: 'nestjs-app'
static_configs:
- targets: ['nestjs:3000'] # NestJSアプリのホスト名とポート
metrics_path: '/metrics'
この設定により、Prometheus は 15 秒ごとに NestJS の /metrics
エンドポイントにアクセスし、メトリクスを取得します。
Docker Compose での構成例
NestJS、Prometheus、Grafana、Loki を Docker Compose で構成します。
yamlversion: '3.8'
services:
nestjs:
build: .
ports:
- '3000:3000'
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- '9090:9090'
networks:
- monitoring
grafana:
image: grafana/grafana:latest
ports:
- '3001:3000'
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
networks:
- monitoring
loki:
image: grafana/loki:latest
ports:
- '3100:3100'
networks:
- monitoring
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
- ./promtail-config.yml:/etc/promtail/config.yml
networks:
- monitoring
networks:
monitoring:
driver: bridge
この構成では、すべてのコンポーネントが monitoring
ネットワーク上で通信します。
Loki でのログ収集
Loki はログを収集・保存するシステムです。Promtail がログファイルを読み取り、Loki に送信します。
Promtail の設定
promtail-config.yml
を作成します。
yamlserver:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: nestjs
static_configs:
- targets:
- localhost
labels:
job: nestjs
__path__: /var/log/nestjs/*.log
この設定により、Promtail は /var/log/nestjs/*.log
を監視し、Loki に送信します。
NestJS のログ出力
NestJS でログをファイルに出力するには、Winston などのロガーを使います。
bashyarn add winston nest-winston
次に、ロガーをセットアップします。
typescriptimport { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
const logger = WinstonModule.createLogger({
transports: [
new winston.transports.File({
filename: '/var/log/nestjs/app.log',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
}),
],
});
main.ts
でロガーを適用します。
typescriptimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger,
});
await app.listen(3000);
}
bootstrap();
これで、NestJS のログが /var/log/nestjs/app.log
に JSON 形式で出力され、Promtail が Loki に送信します。
Grafana でのダッシュボード設計
Grafana では、Prometheus と Loki をデータソースとして追加し、ダッシュボードを構築します。
データソースの追加
Grafana にログインし、Configuration > Data Sources から以下を追加します。
# | 名前 | タイプ | URL |
---|---|---|---|
1 | Prometheus | Prometheus | http://prometheus:9090 |
2 | Loki | Loki | http://loki:3100 |
SLI ダッシュボードの作成
以下の指標をパネルとして配置します。
可用性(エラー率):
promql1 - (
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
)
このクエリは、過去 5 分間の成功率を計算します。SLO が 99.9% なら、閾値を 0.999 に設定します。
レイテンシ(p95):
promqlhistogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, path))
このクエリは、95 パーセンタイルの応答時間をパスごとに算出します。
スループット:
promqlsum(rate(http_requests_total[5m])) by (method, path)
このクエリは、メソッドとパスごとのリクエスト数を表示します。
ログパネルの追加
Loki をデータソースとして使い、エラーログを表示します。
logql{job="nestjs"} |= "error" | json
このクエリは、job="nestjs"
のログから error
を含む行を抽出し、JSON としてパースします。
アラートの設定
Grafana でアラートを設定すると、SLO を下回った際に通知を受け取れます。
可用性アラート:
- 条件: 可用性が 0.999 を下回る
- 期間: 5 分間
- 通知先: Slack
Grafana の Alerting > Notification channels で Slack Webhook URL を設定し、パネルの Alert タブで条件を設定します。
具体例
サンプルアプリケーションの構築
実際に動作する NestJS アプリを構築し、監視スタックを統合します。
プロジェクトの初期化
bashyarn global add @nestjs/cli
nest new nestjs-monitoring
cd nestjs-monitoring
必要なパッケージのインストール
bashyarn add @willsoto/nestjs-prometheus prom-client winston nest-winston
メトリクスとロガーのセットアップ
前述の手順に従い、メトリクスインターセプターとロガーを設定します。
サンプルエンドポイントの作成
src/app.controller.ts
にエンドポイントを追加します。
typescriptimport {
Controller,
Get,
InternalServerErrorException,
} from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('slow')
async getSlow(): Promise<string> {
// 意図的に遅延を発生
await new Promise((resolve) =>
setTimeout(resolve, 2000)
);
return 'Slow response';
}
@Get('error')
getError(): never {
// 意図的にエラーを発生
throw new InternalServerErrorException(
'Something went wrong'
);
}
}
これらのエンドポイントは、レイテンシやエラー率をテストするために使います。
Docker Compose でのデプロイ
前述の docker-compose.yml
を使い、すべてのサービスを起動します。
bashdocker-compose up -d
メトリクスの確認
ブラウザで http://localhost:3000/metrics
にアクセスすると、以下のようなメトリクスが表示されます。
ini# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/",status="200"} 10
http_requests_total{method="GET",path="/slow",status="200"} 2
http_requests_total{method="GET",path="/error",status="500"} 5
# HELP http_request_duration_seconds Duration of HTTP requests in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1",method="GET",path="/"} 10
http_request_duration_seconds_bucket{le="0.3",method="GET",path="/"} 10
http_request_duration_seconds_bucket{le="0.5",method="GET",path="/"} 10
http_request_duration_seconds_bucket{le="1",method="GET",path="/"} 10
http_request_duration_seconds_bucket{le="2",method="GET",path="/slow"} 0
http_request_duration_seconds_bucket{le="5",method="GET",path="/slow"} 2
http_request_duration_seconds_sum{method="GET",path="/"} 0.5
http_request_duration_seconds_count{method="GET",path="/"} 10
この出力から、各エンドポイントのリクエスト数とレイテンシが確認できます。
Grafana でのダッシュボード構築例
Grafana(http://localhost:3001
)にアクセスし、以下のパネルを配置します。
パネル 1:可用性(Success Rate)
- クエリ: 前述の可用性クエリ
- 可視化: Stat(数値表示)
- 閾値: 0.999 以下で赤色
パネル 2:p95 レイテンシ
- クエリ: 前述のレイテンシクエリ
- 可視化: Graph(折れ線グラフ)
- 閾値: 200ms 以上で黄色
パネル 3:リクエスト数
- クエリ: 前述のスループットクエリ
- 可視化: Graph
- 凡例: メソッドとパスごとに分ける
パネル 4:エラーログ
- データソース: Loki
- クエリ:
{job="nestjs"} |= "error"
- 可視化: Logs
以下は、ダッシュボードの構造を示す図です。
mermaidflowchart TB
dashboard["Grafanaダッシュボード"]
dashboard --> panel1["可用性<br/>(Success Rate)"]
dashboard --> panel2["p95レイテンシ"]
dashboard --> panel3["リクエスト数"]
dashboard --> panel4["エラーログ"]
panel1 --> prom1["Prometheusクエリ"]
panel2 --> prom2["Prometheusクエリ"]
panel3 --> prom3["Prometheusクエリ"]
panel4 --> loki1["Lokiクエリ"]
図の補足:
- ダッシュボードは 4 つのパネルで構成
- パネル 1〜3 は Prometheus、パネル 4 は Loki を使用
- 一つの画面でメトリクスとログを統合表示
負荷テストと SLO 検証
実際に負荷をかけて、SLO が守られているか検証します。
負荷テストツールのインストール
bashyarn global add autocannon
負荷テストの実行
bashautocannon -c 100 -d 60 http://localhost:3000
このコマンドは、100 接続で 60 秒間リクエストを送り続けます。
Grafana での確認
負荷テスト中に Grafana を見ると、リアルタイムでメトリクスが更新されます。可用性が 99.9% を下回ったり、p95 レイテンシが 200ms を超えたりした場合、アラートが発火します。
エラーエンドポイントへの負荷テスト
bashautocannon -c 50 -d 30 http://localhost:3000/error
このテストでは、エラー率が急上昇し、アラートが Slack に通知されることを確認できます。
ログとメトリクスの相関分析
エラー率が上昇した際、Loki でログを検索して原因を特定します。
logql{job="nestjs"} |= "InternalServerErrorException" | json | line_format "{{.message}}"
このクエリは、エラーメッセージを抽出し、どのエンドポイントでエラーが発生しているかを特定します。
Grafana の Explore 機能を使うと、Prometheus のメトリクスと Loki のログを同じ時間軸で表示でき、相関分析が容易になります。
まとめ
この記事では、NestJS アプリケーションにおける SLI/SLO ベースの監視運用を、Prometheus、Grafana、Loki を使って実現する方法を解説しました。
重要なポイントをおさらいしましょう。
SLI と SLO の定義:
- ユーザー視点で重要な指標を選ぶ
- 可用性、レイテンシ、スループットが基本
- 定量的な目標を設定し、チームで共有する
メトリクス収集の実装:
@willsoto/nestjs-prometheus
でメトリクスを収集- インターセプターで HTTP リクエストを記録
/metrics
エンドポイントで Prometheus に公開
ログ収集の統合:
- Winston でログをファイル出力
- Promtail が Loki に転送
- Grafana でメトリクスとログを統合表示
ダッシュボード設計:
- SLO に基づいたパネルを配置
- アラートで SLO 違反を通知
- 負荷テストで検証し、継続的に改善
これらの仕組みを導入することで、問題が発生する前に気づき、迅速に対処できる体制が整います。ユーザー体験を守りながら、運用負荷を減らすことができるでしょう。
最初は小さく始めて、少しずつ改善していくのがおすすめです。まずは可用性とレイテンシの 2 つの SLI から始め、運用に慣れてきたら指標を増やしていきましょう。
関連リンク
- article
NestJS 監視運用:SLI/SLO とダッシュボード設計(Prometheus/Grafana/Loki)
- article
NestJS クリーンアーキテクチャ:UseCase/Domain/Adapter を疎結合に保つ設計術
- article
NestJS Decorator 速見表:Controller/Param/Custom Decorator の定型パターン
- article
NestJS 最短セットアップ:Fastify + TypeScript + ESLint + Prettier を 10 分で
- article
NestJS × ExpressAdapter vs FastifyAdapter:レイテンシ/スループットを実測比較
- article
NestJS 依存循環(circular dependency)を断ち切る:ModuleRef と forwardRef の実戦対処
- article
NestJS 監視運用:SLI/SLO とダッシュボード設計(Prometheus/Grafana/Loki)
- article
WebRTC AV1/VP9/H.264 ベンチ比較 2025:画質・CPU/GPU 負荷・互換性を実測
- article
MySQL アラート設計としきい値:レイテンシ・エラー率・レプリカ遅延の基準
- article
Vitest フレーク検知技術の運用:`--retry` / シード固定 / ランダム順序で堅牢化
- article
Motion(旧 Framer Motion)デザインレビュー運用:Figma パラメータ同期と差分共有のワークフロー
- article
esbuild プリバンドルを理解する:Vite の optimizeDeps 深掘り
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来