Log Drainsを使用してVercelのログをAmazon CloudWatchに転送する方法

Log Drainsを使用してVercelのログをAmazon CloudWatchに転送する方法

2025.10.29

リテールアプリ共創部のるおんです。

Vercel上でホスティングしているNext.js v15で作成したアプリケーションのログを、Log Drains 機能を使用してAmazon CloudWatch Logsに転送する仕組みを構築したので、実装手順をまとめました。

https://vercel.com/docs/drains

前提・やりたいこと

  • Next.js v15で作成したアプリケーションをVercel上にデプロイしている
  • Vercelのログを長期保存したい
  • Amazon CloudWatch Logsで一元管理し、メトリクスやアラートを設定したい

今回のブログでは、Vercel上のログをAmazon CloudWatch Logsに転送するところまでを解説しますが、他のAWSサービスと組み合わせると以下のようなことができるようになります。

https://x.com/ruonp24/status/1938602487105396794

先に結論

  • Vercelからのログを処理するAWS Lambdaを作成し、VercelのダッシュボードからLog Drainsを使用してLambda関数と連携。
  • Vercelのダッシュボードから検証リクエストを送信し、Lambda関数が正常に動作することを確認
  • アプリケーション上のログをLog Drains経由でLambda関数で受信し、CloudWatch Logsに転送されることを確認

これにより、Vercelのログ保持期間の制約を超えて、長期間のログ保存とCloudWatch上での高度な分析・アラートが可能になります。

構成図

スクリーンショット 2025-10-29 17.09.21

Vercelのログ保持期間の制約

Vercelのログ保持期間はプランによって以下のように制限されています。

Plan Retention time
Hobby 1 hour of logs
Pro 1 day of logs
Pro with Observability Plus 30 days of logs
Enterprise 3 days of logs
Enterprise with Observability Plus 30 days of logs

https://vercel.com/docs/logs/runtime#limits

Proプランでは1日、Enterpriseプランでも3日しかログが保存されず、Observability Plusを追加しても30日までです。
法令対応や長期的な分析のために60日以上ログを保持したい場合、Log Drains機能を使って外部サービスにログを転送する必要があります。

Log Drainsとは

https://vercel.com/docs/drains/using-drains

Vercelが提供する、観測データ(ログ 、トレース、Speed Insights、Web Analyticsなど)をカスタムHTTPエンドポイントやネイティブ統合に転送する機能です。
Pro/Enterpriseプランのみで利用可能で、API Routesなどの Vercel Functions やEdge Functions、静的ファイルやビルドログ、その他複数のデータソースに対応しています。また複数のデータフォーマットにも対応しています。

今回は Functionsログ データを NDJSON 形式でログを受信し、AWS Lambda経由でCloudWatch Logsに転送します。

やってみた

以下、簡単な手順です。

  1. AWS Lambda関数の準備
  2. Vercelダッシュボード上でのLog Draisの設定
  3. 動作確認

① AWS Lambda関数の準備

まず、ログを処理するLambda関数を作成します。マネジメントコンソール上の Lambda サービスにおいて、以下の内容で関数を作成します。
一から作成 を選択し、適当な関数名を入力します。今回はvercel-log-drainとして作成しました。また、適切なNode.jsのバージョンを指定してください。

スクリーンショット 2025-10-29 14.38.47

次に、その下にある「 その他の構成 」欄から 関数URLをEnable にします。また、今回はVercelからリクエストを送信したいのでIAMでの認証ではなく、Lambda関数内でカスタムヘッダーを使った独自認証を実装(後ほど解説)するので、認証タイプをNone に設定し、Corsも有効 にします。

スクリーンショット 2025-10-29 14.48.42

次に、実際のVercelからのリクエストを処理するLambda関数内のコードを実装します。コードソース内index.mjsに以下のコードを貼り付けてください。

index.mjs
export const handler = async (event) => {
	// Check if authorization header is present
	if (
		!event.headers.authorization ||
		event.headers.authorization !== `Bearer ${process.env.AUTH_TOKEN}`
	) {
		console.log("🔴 Unauthorized request");
		return {
			statusCode: 401,
		};
	}

	if (event.body === undefined) {
		// No body i.e. check request
		console.log("Verify request");
		return {
			statusCode: 200,
			headers: {
				"x-vercel-verify": process.env.INTEGRATION_SECRET,
			},
		};
	}

	/**
	 * We convert the request body from Base64,
	 * then split the resulting string by new lines.
	 * (The NDJSON format sends line-delimited JSON logs.)
	 */
	try {
		const logs = Buffer.from(event.body, "base64")
			.toString("utf-8")
			.split("\n");

		// We create a log entry for each item received.
		for (const log of logs) {
			/**
			 * The split function will leave an empty item
			 * at the end of the array. We don't want to log this.
			 */
			if (log !== "") {
				// Parse the log to determine the appropriate log level
				try {
					const logData = JSON.parse(log);
					const logLevel = logData.level || logData.logLevel;

					// Output with appropriate console method based on log level
					switch (logLevel) {
						case "error":
							console.error(logData);
							break;
						case "warn":
							console.warn(logData);
							break;
						case "debug":
							console.debug(logData);
							break;
						case "info":
							console.info(logData);
							break;
						default:
							console.log(logData);
							break;
					}
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
				} catch (parseError) {
					console.log(log);
				}
			}
		}
	} catch (e) {
		console.log("🔴 Error decoding body", e);
		// Continue to return 200 to avoid retries
	}

	return {
		statusCode: 200,
		headers: {
			"x-vercel-verify": process.env.INTEGRATION_SECRET,
		},
	};
};

上記コードを貼り付けたら Deploy ボタンを実行します。

実装のポイント

  • 認証チェック(3~11行目): カスタムヘッダーauthenticationでBearer認証を実装。これによりVercelからのリクエストであることを担保します。この後Vercel側でヘッダーの設定をします。
  • 検証リクエスト対応(13~22行目): bodyが空の場合はVercelからの検証リクエストなのでx-vercel-verifyヘッダーを返却
  • Base64デコード(30~32行目): Vercelから送られてくるbodyはBase64エンコードされているためデコード
  • ログレベル別出力: CloudWatch側でフィルタリングしやすいようにconsole.errorconsole.warnなどを使い分け

以上で、AWS Lambdaの準備は完了です。この後環境変数などを追加する設定もまだありますので、タブを閉じずに進めることをお勧めします。

ちなみに、ここまでの設定をAWS CDKを用いて構築するとこんな感じ
aws/lib/log-drain-stack.ts
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import * as aws_logs from "aws-cdk-lib/aws-logs";
import type { Construct } from "constructs";

export class LogDrainStack extends cdk.Stack {
	constructor(scope: Construct, id: string, props?: cdk.StackProps) {
		super(scope, id, props);

		// CloudWatch Log Group
		const functionLogGroup = new aws_logs.LogGroup(this, "LogGroup", {
			removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
			retention: aws_logs.RetentionDays.SIX_MONTHS, // 6ヶ月保持
			logGroupName: "/aws/lambda/vercel-log-drain",
		});

		// Lambda function
		const logDrainFunction = new nodejs.NodejsFunction(
			this,
			"LogDrainFunction",
			{
				functionName: "vercel-log-drain",
				entry: "lambda/log-drain-handler/index.js",
				handler: "handler",
				runtime: lambda.Runtime.NODEJS_LATEST,
				timeout: cdk.Duration.seconds(10),
				memorySize: 1769,
				environment: {
					AUTH_TOKEN: process.env.AUTH_TOKEN || "",
					INTEGRATION_SECRET: process.env.INTEGRATION_SECRET || "",
				},
				logGroup: functionLogGroup,
			},
		);

		// Function URL
		logDrainFunction.addFunctionUrl({
			authType: lambda.FunctionUrlAuthType.NONE,
			cors: {
				allowedOrigins: ["*"],
				allowedMethods: [lambda.HttpMethod.ALL],
				allowedHeaders: ["*"],
			},
		});
	}
}

CDKでのポイント

  • logGroupで明示的にロググループを指定し、保持期間を設定
  • addFunctionUrlでFunction URLを追加
  • 環境変数としてAUTH_TOKENINTEGRATION_SECRETを設定

② Vercelダッシュボード上でのLog Drainsの設定

Log Drainsのセットアップをします。
Vercelのダッシュボードにアクセスし、「Settings」→「Drains」タブに移動します。「Add Drain」から今回設定するLog Drainsを設定します。

「Add Drain」を押すと、画像のような設定画面が表示されます。まずは観測データを選択します。
今回は「Logs」を選択します

スクリーンショット 2025-10-29 15.43.10

次に、対象プロジェクトを選択し、データのソースを設定します。今回はAPI Routesなどのログが出力される「Functions」を選択します。EnvironmentにはProductionを選択しました。

スクリーンショット 2025-10-29 15.48.47

次に、Lambdaの 関数URL をマネジメントコンソールから取得して、取得した関数URLをLog Drainsのエンドポイントとして設定します。データフォーマットは、NDJSON であることに注意してください

スクリーンショット 2025-10-29 15.51.09

スクリーンショット 2025-10-29 16.02.16

最後に、Custom Headersのトグルをオンにして、インプット欄に以下の文字列を入力します。

Authorization: Bearer sample-random-key-hogehoge

パスワード生成ツール等で 安全なランダム文字列 を生成し、「sample-random-key-hogehoge」の部分は置き換えてください

スクリーンショット 2025-10-29 16.05.41

そして、今設定した安全なランダム文字列を「AUTH_TOKEN」として、そしてその上に表示されているSignature verification secretを「INTEGRATION_SECRET」としてLambdaの環境変数に設定します。

スクリーンショット 2025-10-29 16.11.55

こうすることで、先ほど作成したLambda関数の以下の部分で環境変数から値を取得し、VercelのLog Drains経由のリクエストだと判定することができます。

Vercelからのリクエストであることを確認

index.mjs
	// Check if authorization header is present
	if (
		!event.headers.authorization ||
		event.headers.authorization !== `Bearer ${process.env.AUTH_TOKEN}`
	) {
		console.log("🔴 Unauthorized request");
		return {
			statusCode: 401,
		};
	}

検証リクストをPassするためのコード(検証の場合bodyがnullで入ってくる)

index.mjs
	if (event.body === undefined) {
		// No body i.e. check request
		console.log("Verify request");
		return {
			statusCode: 200,
			headers: {
				"x-vercel-verify": process.env.INTEGRATION_SECRET,
			},
		};
	}

Create Drain」を押して、問題なくテストが通ったら成功です。

③ 動作確認

Amazon CloudWatch Logsに移動し、/vercel-log-drain ロググループが見つかるはずなので、そこにログが転送されていることを確認します。
Next.jsのアプリケーションのappディレクトリ配下にtestディレクトリとpage.tsxを作成し、以下のコードを追加しました。

src/app/test/page.tsx
export const dynamic = "force-dynamic";

export default function TestPage() {
	console.info("🚀Log Drain Test!!");
	return <div>TestPage</div>;
}

アプリのurlで/testにアクセスするとサーバーコンポーネント上で実行されるコンソールのログがVercelのログに出力され、さらにそれがAmazon CloudWatchのログストリームに転送されていることが確認できました。
また、Vercel上のメタデータもログとして付与されていることがわかります。

Vercel Logs
スクリーンショット 2025-10-29 16.40.45

Amazon CloudWatch Logs
スクリーンショット 2025-10-29 16.43.10

終わりに

Vercel Log DrainsとAmazon CloudWatchを使用すると、Vercelのログを長期保存することができ、AWS上で高度な分析やアラート設定が可能になります。
特に、ログレベルに応じた適切な出力方法を採用することで、CloudWatch側でのフィルタリングやメトリクス化がスムーズになります。

今回のブログではログの転送の解説に留まりましたが、他のサービスを組み合わせることでエラーアラートをSlackへ連携して通知を送信することなどが可能です。詳しくは以下のブログなどが参考になります。

https://dev.classmethod.jp/articles/send-cloudwatch-alarm-to-slack/

自分自身4ヶ月ほど前に本機能を実装しましたが、やり方を残しておきたいなと思っていたので今回ブログにしました。
以上、どなたかの参考になれば幸いです。

参考

https://vercel.com/docs/drains/using-drains

https://dev.to/mannieschumpert/setting-up-a-vercel-log-drain-with-aws-4a9c

この記事をシェアする

FacebookHatena blogX

関連記事