モニタリングとロギング
このガイドでは、AWS上のMBC CQRS Serverlessアプリケーション向けの包括的なモニタリングとロギングのセットアップ方法を説明します。
概要
完全な可観測性戦略には以下が含まれます:
- ロギング: アプリケーションイベントとエラーのキャプチャ
- メトリクス: パフォーマンスと使用量の測定
- トレーシング: サービス間のリクエスト追跡
- アラート: 問題発生時の通知
組み込み機能
MBC CQRS Serverlessフレームワークは、以下の可観測性機能をすぐに使える形で提供します:
- RequestLogger: コンテキスト情報(tenantCode、userId、requestId、sourceIp)を含む拡張NestJSロガー
- AWS X-Rayトレーシング: CDKテンプレートでLambdaとStep Functionsに対してデフォルトで有効
- JSONロギングフォーマット: Lambda関数はクエリしやすいJSON形式でログを出力
- 設定可能なログレベル: LOG_LEVEL環境変数で設定
以下のセクションでは、追加のAWS CloudWatch機能(メトリクス、ダッシュボード、アラーム)のセットアップに関するガイダンスを提供します。これらはフレームワークに組み込まれていませんが、本番デプロイメントに推奨されるパターンです。
CloudWatch Logs
Lambdaロギング
フレームワークには、ログにコンテキストを自動的に追加する組み込みのRequestLoggerが含まれています:
// フレームワークのRequestLoggerは自動的に使用されます
// ログ出力には以下が含まれます: context、requestId、ip、tenantCode、userId
import { Logger, Injectable } from '@nestjs/common';
@Injectable()
export class YourService {
private readonly logger = new Logger(YourService.name);
async doSomething() {
// これらのログはLambdaでユーザーコンテキストを自動的に含みます
this.logger.log('操作を開始しました');
this.logger.debug('デバッグ情報');
this.logger.error('エラーが発生しました');
}
}
より詳細な追跡のために、構造化ロギングで強化できます:
import { Logger, Injectable } from '@nestjs/common';
import { getUserContext, IInvoke } from '@mbc-cqrs-serverless/core';
@Injectable()
export class TodoService {
private readonly logger = new Logger(TodoService.name);
async create(dto: CreateTodoDto, opts: { invokeContext: IInvoke }): Promise<Todo> {
const { userId } = getUserContext(opts.invokeContext);
const startTime = Date.now();
this.logger.log({
action: 'create_todo',
input: dto,
userId,
});
try {
const result = await this.save(dto);
this.logger.log({
action: 'todo_created',
todoId: result.id,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
this.logger.error({
action: 'create_todo_failed',
error: error.message,
stack: error.stack,
});
throw error;
}
}
}
ログ保持
CDKでログ保持を設定:
import * as logs from 'aws-cdk-lib/aws-logs';
const logGroup = new logs.LogGroup(this, 'AppLogGroup', {
logGroupName: `/aws/lambda/${props.envName}-${props.appName}`,
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
ログフォーマット
クエリを容易にするための推奨ログフォーマット:
interface LogEntry {
timestamp: string;
level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
message: string;
context: string;
correlationId?: string;
userId?: string;
duration?: number;
error?: {
name: string;
message: string;
stack: string;
};
metadata?: Record<string, any>;
}
CloudWatchメトリクス
カスタムメトリクスはフレームワークに組み込まれていません。以下の例は、AWS SDKを使用してアプリケーションにカスタムメトリクスを実装する方法を示しています。
カスタムメトリクス
アプリケーションからカスタムメトリクスを発行:
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';
const cloudwatch = new CloudWatchClient({});
async function publishMetric(
metricName: string,
value: number,
unit: string = 'Count',
): Promise<void> {
await cloudwatch.send(new PutMetricDataCommand({
Namespace: 'YourApp/Custom',
MetricData: [
{
MetricName: metricName,
Value: value,
Unit: unit,
Dimensions: [
{ Name: 'Environment', Value: process.env.ENVIRONMENT },
],
},
],
}));
}
// Usage
await publishMetric('TodosCreated', 1);
await publishMetric('ProcessingTime', 150, 'Milliseconds');
監視すべき主要メトリクス
| カテゴリ | メトリクス | 説明 |
|---|---|---|
| Lambda | Invocations | 関数呼び出し回数 |
| Lambda | Duration | 実行時間 |
| Lambda | Errors | 失敗した呼び出し |
| Lambda | ConcurrentExecutions | 並列実行数 |
| API Gateway | Count | リクエスト数 |
| API Gateway | Latency | レスポンス時間 |
| API Gateway | 4XXError | クライアントエラー |
| API Gateway | 5XXError | サーバーエラー |
| DynamoDB | ConsumedReadCapacity | 読み取り使用量 |
| DynamoDB | ConsumedWriteCapacity | 書き込み使用量 |
| DynamoDB | ThrottledRequests | スロットルされた操作 |
CDKダッシュボード
ダッシュボードはデフォルトのCDKテンプレートに含まれていません。CloudWatchダッシュボードを作成するには、CDKスタックに以下を追加してください。
CloudWatchダッシュボードを作成:
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
const dashboard = new cloudwatch.Dashboard(this, 'AppDashboard', {
dashboardName: `${props.appName}-${props.envName}`,
});
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Lambda Invocations',
left: [handler.metricInvocations()],
}),
new cloudwatch.GraphWidget({
title: 'Lambda Duration',
left: [handler.metricDuration()],
}),
new cloudwatch.GraphWidget({
title: 'Lambda Errors',
left: [handler.metricErrors()],
}),
new cloudwatch.GraphWidget({
title: 'API Gateway Requests',
left: [
api.metricCount(),
api.metricClientError(),
api.metricServerError(),
],
}),
);
CloudWatchアラーム
アラームはデフォルトのCDKテンプレートに含まれていません。本番監視用にCDKスタックに以下を追加してください。
アラームの作成
重要なメトリクスにアラームを設定:
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as actions from 'aws-cdk-lib/aws-cloudwatch-actions';
// 通知用SNSトピック
const alertTopic = new sns.Topic(this, 'AlertTopic');
// Lambdaエラーアラーム
const errorAlarm = new cloudwatch.Alarm(this, 'LambdaErrorAlarm', {
metric: handler.metricErrors(),
threshold: 5,
evaluationPeriods: 1,
alarmDescription: 'Lambda function errors exceed threshold',
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
errorAlarm.addAlarmAction(new actions.SnsAction(alertTopic));
// 高レイテンシーアラーム
const latencyAlarm = new cloudwatch.Alarm(this, 'LatencyAlarm', {
metric: handler.metricDuration({
statistic: 'p99',
}),
threshold: 5000, // 5 seconds
evaluationPeriods: 3,
alarmDescription: 'P99 latency exceeds 5 seconds',
});
latencyAlarm.addAlarmAction(new actions.SnsAction(alertTopic));
// DynamoDBスロットリングアラーム
const throttleAlarm = new cloudwatch.Alarm(this, 'ThrottleAlarm', {
metric: table.metricThrottledRequests(),
threshold: 1,
evaluationPeriods: 1,
alarmDescription: 'DynamoDB throttling detected',
});
throttleAlarm.addAlarmAction(new actions.SnsAction(alertTopic));
推奨アラーム
| アラーム | メトリクス | しきい値 | 説明 |
|---|---|---|---|
| 高エラー率 | Lambda Errors | 1分あたり5件超 | アプリケーションの問題を示す |
| 高レイテンシー | Lambda Duration p99 | 5秒超 | レスポンス時間が遅い |
| DynamoDBスロットリング | ThrottledRequests | 0より大きい | 容量の問題 |
| 5XXエラー | API Gateway 5XXError | 1%超 | サーバーエラー |
| デッドレターキュー | SQS ApproximateNumberOfMessages | 0より大きい | 失敗したメッセージ |
AWS X-Rayトレーシング
X-RayトレーシングはMBC CQRS Serverless CDKテンプレートでLambda関数とStep Functionsに対してデフォルトで有効になっています。
X-Rayの有効化
Lambda と Step Functions のトレーシングはデフォルトの CDK テンプレートで有効化済みです。API Gateway ステージのトレーシングはデフォルトテンプレートには含まれていないため、必要な場合は以下のように追加してください:
const handler = new lambda.Function(this, 'Handler', {
// ... other config
tracing: lambda.Tracing.ACTIVE,
});
const api = new apigateway.HttpApi(this, 'Api', {
// ... other config
});
// API GatewayのX-Rayを有効化
const stage = api.defaultStage?.node.defaultChild as apigateway.CfnStage;
stage.addPropertyOverride('TracingEnabled', true);
アプリケーションのインストルメント
AWS SDK呼び出しとHTTPリクエストのより深いトレーシングのために、オプションでaws-xray-sdkパッケージをアプリケーションに追加できます。
import * as AWSXRay from 'aws-xray-sdk-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// AWS SDK v3クライアントをインストルメント
const dynamoClient = AWSXRay.captureAWSv3Client(new DynamoDBClient({}));
// HTTPコールをインストルメント
AWSXRay.captureHTTPsGlobal(require('http'));
AWSXRay.captureHTTPsGlobal(require('https'));
// カスタムアノテーションを追加
const segment = AWSXRay.getSegment();
const subsegment = segment?.addNewSubsegment('CustomOperation');
subsegment?.addAnnotation('userId', userId);
subsegment?.addMetadata('request', requestData);
// ... perform operation
subsegment?.close();
集中ロギング
集中ロギングはデフォルトテンプレートに含まれていません。以下のパターンは、複雑なアプリケーションでログを集約する方法を示しています。
ログ集約パターン
複雑なアプリケーションでは、ログを集約:
// 集中ロググループを作成
const centralLogGroup = new logs.LogGroup(this, 'CentralLogs', {
logGroupName: `/app/${props.appName}/central`,
retention: logs.RetentionDays.THREE_MONTHS,
});
// LambdaログをサブスクライB
new logs.SubscriptionFilter(this, 'LambdaLogSubscription', {
logGroup: lambdaLogGroup,
destination: new destinations.LambdaDestination(logProcessorFunction),
filterPattern: logs.FilterPattern.allEvents(),
});
Log Insightsクエリ
便利なCloudWatch Logs Insightsクエリ:
# Error analysis
fields @timestamp, @message
| filter @message like /ERROR/
| stats count(*) by bin(1h)
# Slow requests
fields @timestamp, @duration, @requestId
| filter @duration > 3000
| sort @duration desc
| limit 20
# Request volume by endpoint
fields @timestamp
| filter @message like /HTTP/
| parse @message /(?<method>\w+) (?<path>\/\S+)/
| stats count(*) by path, method
| sort count desc
# Cold starts
fields @timestamp, @message
| filter @message like /Init Duration/
| parse @message /Init Duration: (?<initDuration>[\d.]+) ms/
| stats avg(initDuration), max(initDuration), count(*)