はじめに
「チャットボットの会話ログをAPIで受け取り、AWSに蓄積したい」——そんな要件を実現する構成が API Gateway + Lambda + CloudWatch Logs の組み合わせです。
この記事では、AWSコンソールのみを使って、チャットボットの会話ログを記録・取得するREST APIをゼロから構築するハンズオンを紹介します。
クライアント
↓ POST /logs(会話ログを送信)
API Gateway(REST API)
↓ Lambda プロキシ統合
Lambda(ChatLogFunction / Python 3.12)
↓ boto3 logs.put_log_events()
CloudWatch Log Group(/chatbot/chatbot-logs)
↓ Metric Filter(level = "error" のログを検出)
CloudWatch カスタムメトリクス(ChatErrorCount)このハンズオンで体験できること:
- API Gateway REST API のリソース・メソッド作成と Lambda プロキシ統合
- CloudWatch Logs カスタムロググループへのログ書き込み(
put_log_events) - Metric Filter によるエラーログの自動メトリクス変換
- CloudWatch Logs Insights でのログクエリ実行
この記事は SAM版ハンズオン の比較記事です。
コンソール操作でAPI Gateway・Lambda・CloudWatch Logsの連携を視覚的に学び、SAM と何が違うのかを比較したい方向けです。
キーワード解説
| 用語 | 意味 |
|---|---|
| CloudWatch Log Group | ログの論理的なまとまり。今回はチャットログ専用グループを作成する |
| Log Stream | Log Group の中の個別のログストリーム。今回はセッションIDごとに作成する |
| put_log_events | boto3 でカスタムロググループにログを書き込む API |
| Metric Filter | ロググループに届いたログを JSON フィルターでマッチさせ、カスタムメトリクスを自動生成する仕組み |
| カスタムメトリクス | CloudWatch に独自のメトリクスを記録する機能。アラーム設定やダッシュボードに使える |
| Lambda プロキシ統合 | API Gateway が HTTP リクエスト全体を Lambda に渡す最もシンプルな統合方式 |
使用するAWSサービス
| サービス | 役割 | 料金 |
|---|---|---|
| API Gateway | REST APIのエンドポイント(POST /logs, GET /logs) | 月100万リクエストまで無料 |
| Lambda | ログの書き込み・取得処理(Python 3.12) | 月100万リクエスト・400,000 GB-秒まで無料 |
| CloudWatch Logs | チャットログの保存・クエリ | 月5GBまで無料 |
| CloudWatch メトリクス | エラーログのカスタムメトリクス化 | 月10件まで無料 |
| IAM | Lambda の実行権限管理 | 無料 |
全体の作業順序
① CloudWatch Log Group を作成する
↓
② CloudWatch Metric Filter を作成する(error ログ → メトリクス)
↓
③ Lambda 関数を作成する(コード・環境変数・タイムアウト設定)
↓
④ Lambda の実行ロールに権限を追加する
(CloudWatch Logs: 書き込み・読み取り / CloudWatch: PutMetricData)
↓
⑤ API Gateway REST API を作成する(POST /logs, GET /logs)
↓
⑥ 動作テスト(curl でログ送信 → コンソールで確認)
↓
⑦ リソースの削除Log Group を先に作る理由:
Metric Filter の作成にはロググループが必要なため、最初に作成します。
① CloudWatch Log Group を作成する
AWSコンソール → CloudWatch → ロググループ → 「ロググループの作成」
| 設定項目 | 値 |
|---|---|
| ロググループ名 | /chatbot/chatbot-logs |
| 保持期間 | 7日間 |
「作成」をクリック。
控えておく情報:
- ロググループ名:
/chatbot/chatbot-logs
② CloudWatch Metric Filter を作成する
エラーログを自動的にカスタムメトリクスに変換するフィルターを設定します。
CloudWatch → ロググループ → /chatbot/chatbot-logs → 「メトリクスフィルター」タブ → 「メトリクスフィルターを作成」
フィルターパターンの設定
| 設定項目 | 値 |
|---|---|
| フィルターパターン | { $.level = "error" } |
「パターンのテスト」でテストデータを入力して確認できます(任意)。「次へ」をクリック。
メトリクスの設定
| 設定項目 | 値 |
|---|---|
| フィルター名 | ChatErrorFilter |
| メトリクスの名前空間 | ChatBot/chatbot-logs |
| メトリクス名 | ChatErrorCount |
| メトリクス値 | 1 |
| デフォルト値 | 0 |
「次へ」→「メトリクスフィルターを作成」。
Metric Filter の仕組み:
CloudWatch Logs に届いたログが{ $.level = "error" }にマッチするたびに、ChatBot/chatbot-logs名前空間のChatErrorCountに1が加算されます。
ログに{"level": "error", ...}という JSON が含まれていればマッチします。
③ Lambda 関数を作成する
AWSコンソール → Lambda → 「関数の作成」
| 設定項目 | 値 |
|---|---|
| 作成方法 | 一から作成 |
| 関数名 | ChatLogFunction |
| ランタイム | Python 3.12 |
| アーキテクチャ | x86_64 |
| 実行ロール | 「基本的な Lambda アクセス権限で新しいロールを作成」(デフォルト) |
「関数の作成」をクリック。
タイムアウトの設定
「設定」タブ → 「一般設定」→「編集」
| 設定項目 | 値 |
|---|---|
| タイムアウト | 10 秒 |
「保存」をクリック。
環境変数の設定
「設定」タブ → 「環境変数」→「編集」→「環境変数を追加」
| キー | 値 |
|---|---|
LOG_GROUP_NAME | /chatbot/chatbot-logs |
「保存」をクリック。
コードの入力
「コード」タブ → lambda_function.py を開いて既存の内容を全て置き換えます。
import json
import os
import time
import uuid
import boto3
from botocore.exceptions import ClientError
logs_client = boto3.client("logs")
cloudwatch = boto3.client("cloudwatch")
LOG_GROUP_NAME = os.environ["LOG_GROUP_NAME"]
METRIC_NAMESPACE = "ChatBot/" + LOG_GROUP_NAME.split("/")[-1] # "ChatBot/chatbot-logs"
def lambda_handler(event, context):
http_method = event.get("httpMethod", "")
resource = event.get("resource", "/")
if http_method == "POST" and resource == "/logs":
return post_log(event)
elif http_method == "GET" and resource == "/logs":
return get_logs(event)
else:
return _response(404, {"error": "Not Found"})
def post_log(event):
try:
body = json.loads(event.get("body") or "{}")
except json.JSONDecodeError:
return _response(400, {"error": "Invalid JSON"})
session_id = body.get("session_id") or str(uuid.uuid4())
user_id = body.get("user_id", "anonymous")
message = body.get("message", "")
response_text = body.get("response", "")
level = body.get("level", "info")
log_entry = {
"session_id": session_id,
"user_id": user_id,
"message": message,
"response": response_text,
"level": level,
}
log_stream_name = f"session/{session_id}"
_ensure_log_stream(log_stream_name) # ストリームを作成(既存なら無視)
logs_client.put_log_events(
logGroupName=LOG_GROUP_NAME,
logStreamName=log_stream_name,
logEvents=[
{
"timestamp": int(time.time() * 1000), # ミリ秒単位の Unix 時間
"message": json.dumps(log_entry, ensure_ascii=False),
}
],
)
cloudwatch.put_metric_data(
Namespace=METRIC_NAMESPACE,
MetricData=[
{
"MetricName": "MessageCount",
"Dimensions": [{"Name": "Level", "Value": level}],
"Value": 1,
"Unit": "Count",
}
],
)
return _response(200, {
"session_id": session_id,
"log_stream": log_stream_name,
"message": "ログを記録した",
})
def get_logs(event):
params = event.get("queryStringParameters") or {}
session_id = params.get("session_id")
limit = min(int(params.get("limit", "20")), 100)
if not session_id:
return _response(400, {"error": "session_id クエリパラメーターは必須"})
log_stream_name = f"session/{session_id}"
try:
resp = logs_client.get_log_events(
logGroupName=LOG_GROUP_NAME,
logStreamName=log_stream_name,
limit=limit,
startFromHead=False, # 最新のログから取得
)
events = []
for e in resp["events"]:
try:
msg = json.loads(e["message"])
except json.JSONDecodeError:
msg = e["message"]
events.append({"timestamp_ms": e["timestamp"], "log": msg})
except ClientError as e:
if e.response["Error"]["Code"] == "ResourceNotFoundException":
events = [] # ログストリームが存在しない場合は空リストを返す
else:
raise
return _response(200, {
"session_id": session_id,
"log_count": len(events),
"logs": events,
})
def _ensure_log_stream(log_stream_name: str) -> None:
try:
logs_client.create_log_stream(
logGroupName=LOG_GROUP_NAME,
logStreamName=log_stream_name,
)
except ClientError as e:
if e.response["Error"]["Code"] != "ResourceAlreadyExistsException":
raise # 既存ストリームは無視、それ以外のエラーは再送出
def _response(status_code: int, body: dict) -> dict:
return {
"statusCode": status_code,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body, ensure_ascii=False),
}「Deploy」ボタンをクリックしてコードを保存します。
④ Lambda の実行ロールに権限を追加する
デフォルトの実行ロールは CloudWatch Logs への書き込み(Lambdaの実行ログ出力)のみです。
今回はカスタムロググループへの書き込み・読み取りと、カスタムメトリクスへの書き込み権限を追加します。
Lambda → ChatLogFunction → 「設定」タブ → 「アクセス権限」→ 実行ロール名のリンクをクリック
(例: ChatLogFunction-role-XXXX)→ IAM コンソールのロール画面が開きます。
CloudWatch Logs と CloudWatch の権限をまとめて追加する
「許可を追加」→「インラインポリシーを作成」→「JSON」タブを選択して以下を入力します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:GetLogEvents",
"logs:DescribeLogStreams"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:YOUR_ACCOUNT_ID:log-group:/chatbot/chatbot-logs",
"arn:aws:logs:ap-northeast-1:YOUR_ACCOUNT_ID:log-group:/chatbot/chatbot-logs:*"
]
},
{
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*"
}
]
}
YOUR_ACCOUNT_IDを自分の AWS アカウント ID(12桁)に置き換えてください。
アカウント ID はaws sts get-caller-identityで確認できます。
「次へ」→ ポリシー名: ChatLogPolicy → 「ポリシーを作成」。
なぜ 2 種類の権限が必要か:
logs:PutLogEvents—put_log_events()でカスタムロググループにログを書き込むlogs:GetLogEvents—get_log_events()でログを読み取るcloudwatch:PutMetricData—put_metric_data()でカスタムメトリクスを記録する
⑤ API Gateway REST API を作成する
5-1. API の作成
AWSコンソール → API Gateway → 「APIを作成」→ 「REST API」→「構築」
| 設定項目 | 値 |
|---|---|
| API タイプ | REST API(WebSocket ではない方) |
| API 名 | ChatLogAPI |
| エンドポイントタイプ | リージョン |
「API を作成」をクリック。
5-2. /logs リソースを作成する
「リソース」→「リソースを作成」
| 設定項目 | 値 |
|---|---|
| リソース名 | logs |
| リソースパス | /logs |
「リソースを作成」をクリック。
5-3. POST メソッドを作成する(ログ書き込み)
/logs リソースを選択した状態で「メソッドを作成」。
| 設定項目 | 値 |
|---|---|
| メソッドタイプ | POST |
| 統合タイプ | Lambda 関数 |
| Lambda プロキシ統合 | 有効(チェックを入れる) |
| Lambda 関数 | ChatLogFunction |
「メソッドを作成」をクリック。
5-4. GET メソッドを作成する(ログ取得)
同様に GET メソッドを追加します。設定は POST と同じ(Lambda 関数: ChatLogFunction)。
5-5. API をデプロイする
「API をデプロイ」→
| 設定項目 | 値 |
|---|---|
| ステージ | 「新しいステージ」 |
| ステージ名 | Prod |
「デプロイ」をクリック。
デプロイ後に表示される URL を控えておきます:
https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod⑥ 動作テスト
事前準備: API URL を変数に設定する
set API_URL=https://XXXXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/Prod
XXXXXXXXXXを ⑤ でデプロイ後に表示された URL の値に置き換えてください。
テスト 1: ログを記録する(POST /logs)
curl -X POST "%API_URL%/logs" ^
-H "Content-Type: application/json" ^
-d "{\"session_id\": \"session-001\", \"user_id\": \"user001\", \"message\": \"今日の天気は?\", \"response\": \"東京は晴れです\", \"level\": \"info\"}"期待するレスポンス:
{
"session_id": "session-001",
"log_stream": "session/session-001",
"message": "ログを記録した"
}テスト 2: エラーログを記録する(Metric Filter のテスト)
curl -X POST "%API_URL%/logs" ^
-H "Content-Type: application/json" ^
-d "{\"session_id\": \"session-001\", \"user_id\": \"user001\", \"message\": \"注文履歴を見せて\", \"response\": \"エラーが発生しました\", \"level\": \"error\"}"このリクエスト後、CloudWatch メトリクス
ChatBot/chatbot-logs > ChatErrorCountが +1 されます。
反映まで数分かかることがあります。
テスト 3: ログを取得する(GET /logs)
curl "%API_URL%/logs?session_id=session-001"期待するレスポンス:
{
"session_id": "session-001",
"log_count": 2,
"logs": [
{
"timestamp_ms": 1700000000000,
"log": {
"session_id": "session-001",
"user_id": "user001",
"message": "今日の天気は?",
"response": "東京は晴れです",
"level": "info"
}
}
]
}CloudWatch でログを確認する
CloudWatch → ロググループ → /chatbot/chatbot-logs → ログストリーム
session/session-001ストリームをクリック- 送信した会話ログが JSON 形式で記録されていることを確認します
CloudWatch Logs Insights でクエリを実行する(任意)
CloudWatch → Logs Insights
ロググループに /chatbot/chatbot-logs を選択し、時間範囲を「直近1時間」など幅のある範囲に設定してから以下のクエリを実行します。
注意: 開始時刻と終了時刻が同じだとエラー
Query's end date and time is either before the log groups creation time...が出ます。カレンダーアイコンで時間幅を確保してください。
fields @timestamp, session_id, user_id, level, message
| filter level = "error"
| sort @timestamp desc
| limit 20エラーログだけを抽出して確認できます。
カスタムメトリクスを確認する(任意)
CloudWatch → メトリクス → すべてのメトリクス → カスタム名前空間 → ChatBot/chatbot-logs
| メトリクス名 | 説明 | 発生元 |
|---|---|---|
ChatErrorCount | error ログが書き込まれるたびに +1 | Metric Filter(自動) |
MessageCount | POST /logs が呼ばれるたびに +1 | Lambda の put_metric_data |
⑦ リソースの削除
課金を止めるために、ハンズオン完了後は必ず削除してください。
1. API Gateway を削除する
API Gateway → API → ChatLogAPI → 「削除」→ API 名を入力 → 「削除」
2. Lambda 関数を削除する
Lambda → 関数 → ChatLogFunction → 「アクション」→「削除」
3. CloudWatch Log Group を削除する
CloudWatch → ロググループ → /chatbot/chatbot-logs → 「アクション」→「ロググループを削除」
Metric Filter はロググループを削除すると自動的に削除されます。
4. IAM ロールを削除する(任意)
IAM → ロール → ChatLogFunction-role-XXXX を削除
5. CloudWatch Lambda ロググループを削除する(任意)
CloudWatch → ロগগループ → /aws/lambda/ChatLogFunction → 削除
SAM との対比
| SAMの記述 | コンソールでやること |
|---|---|
AWS::Logs::LogGroup | CloudWatch → ロググループを作成 |
AWS::Logs::MetricFilter | ロググループ → メトリクスフィルターを作成 |
Statement: logs:PutLogEvents | IAM → ロールにインラインポリシーを追加 |
Statement: cloudwatch:PutMetricData | 同上(同じポリシーに含める) |
Events: Type: Api(POST + GET) | API Gateway → リソース・メソッドを作成してデプロイ |
コンソール版のつまずきポイント:
IAM のインラインポリシーにYOUR_ACCOUNT_IDを自分のアカウントIDに書き換えるのを忘れるとAccessDeniedExceptionが発生します。
SAM版では!GetAtt ChatLogGroup.Arnで自動解決されるため、この手作業が不要です。
トラブルシューティング
| 症状 | 原因 | 対処 |
|---|---|---|
AccessDeniedException: logs:PutLogEvents | Lambda ロールに権限がない | ④ の手順でインラインポリシーを追加する |
ResourceNotFoundException (Log Group) | 環境変数のロググループ名が誤っている | LOG_GROUP_NAME 環境変数と ① で作成したグループ名を確認する |
| POST で 500 エラー | Lambda 実行エラー | Lambda → 「モニタリング」→「CloudWatch Logs を表示」でエラー詳細を確認する |
GET で {"log_count": 0} | ログがない or session_id が異なる | POST で使った session_id と一致しているか確認する |
| Metric Filter が反映されない | 数分かかる | 5〜10 分後に CloudWatch メトリクスを再確認する |
InvalidParameterException (put_log_events) | タイムスタンプが古すぎる or 未来すぎる | int(time.time() * 1000) でミリ秒タイムスタンプを生成しているか確認する |
| API Gateway から 403 が返る | ステージのデプロイ忘れ | API Gateway → 「API をデプロイ」を再実行する |
まとめ
今回のハンズオンで体験できたこと:
| 確認項目 | 内容 |
|---|---|
| API Gateway + Lambda 連携 | Lambda プロキシ統合で REST API の POST / GET を1関数で処理 |
| カスタムロググループへの書き込み | put_log_events() でセッション単位のログストリームに記録 |
| Metric Filter | { $.level = "error" } でエラーログを自動的にメトリクスへ変換 |
| CloudWatch Logs Insights | クエリでエラーログだけを抽出・確認 |
コンソール版で実感できたポイント
- API Gateway のリソース・メソッド・デプロイという3段階の設定が視覚的に理解できる
- IAM ポリシーのリソース ARN を手動で記述することで、権限スコープの意味が身につく
- Metric Filter の仕組みを GUI で設定することで、JSON フィルター構文の動作が理解できる
コンソール版と SAM 版を比較してみる
コンソールで API Gateway + Lambda + CloudWatch Logs の連携を理解したら、SAM で同じ構成をコードで定義することで「SAM が何を自動化しているか」が明確になります。IAM ポリシーの ARN 解決や Log Group の保持期間設定など、コンソールでの手動操作がコードに対応しています。
コメント