⏱️

Cloud Runの実行ログを読めるMCPサーバーを自作して不具合対応を爆速にしたい

に公開

はじめに

ここ最近様々なツールがMCPサーバーとしてリリースされています。
弊社Organizationでも先日haruさんのAPI仕様書を読み取れるMCPサーバーを自作したら開発が爆速になったが公開されて大きな反響がありました。(そこでタイトルのノリをパクってみた)

cloud-run-logs-mcpを作ってみた

https://github.com/maito1201/cloudrun-logs-mcp

NOT A HOTELではクラウドインフラとしてGoogle Cloudを採用しています。
そこで、Cloud Runのログを取得するMCPサーバーを作りました。
これで劇的に業務を改善してみせるぞ、という強い思いがあるわけではなく、
MCPサーバーで何か作ってみたいけど良いネタないかな、という思考で作ったものです。

Cloud Runの実行ログを取得するGoのコードをmcp-goを用いてMCPサーバー化します。
mcp-goについてはujiさんのGoのModel Context Protocol (MCP)の開発フレームワークmcp-goを使ってみるを読んでみてください。

エラーの発生しないアプリケーションを作るのはただでさえ難しいですが、私たちの仕事は現実世界での様々なイレギュラーに備える必要があります。
何かトラブルや問い合わせがあったときはエンジニアが即対応することが求められます。
これを作ったらアプリケーションのエラーログを雑な命令で検索できて、要約できて、面白いんじゃないか、と思いつき、8割型バイブコーディングしたのち、そこそこ手直しして完成しました。

使ってみた

このツールでClineにログ取得を頼んでみます

期待通りエラーログの内容を要約してくれました。(開発環境の当たり障りのないものを出しています)
記事中ではとりあえず動かしてみただけですが、障害発生時など、この時間にこういうエラーがあるんじゃないか、という仮説検証を高速に行うことができるようになる事が期待できます。

なおClineに長文を与えるとコンテキスト長に絶えきれず息絶えてしまうため、
ログ取得件数のlimit条件や時間の指定は細かめにやってあげる必要があります。

解説

// GetCloudRunLogs は指定されたフィルターオプションに基づいてCloud Runのログを取得します
func GetCloudRunLogs(ctx context.Context, opts FilterOptions) ([]LogEntry, error) {
	// Logging Adminクライアントを作成
	client, err := logadmin.NewClient(ctx, opts.ProjectID, option.WithScopes("https://www.googleapis.com/auth/logging.read"))
	if err != nil {
		return nil, fmt.Errorf("logadmin.NewClient: %v", err)
	}
	defer client.Close()

	// フィルター文字列を構築
	filter := buildFilter(opts)

	// ログエントリを取得
	entries := []LogEntry{}
	iter := client.Entries(ctx, logadmin.Filter(filter))

	// 結果を処理
	count := 0
	for {
		entry, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("iter.Next: %v", err)
		}

		// LogEntryに変換
		logEntry := LogEntry{
			Timestamp: entry.Timestamp,
			Severity:  entry.Severity.String(),
			Labels:    entry.Labels,
		}

		// メッセージを取得(ペイロードの種類によって処理が異なる)
		switch p := entry.Payload.(type) {
		case string:
			logEntry.Message = p
		case map[string]interface{}:
			if msg, ok := p["message"].(string); ok {
				logEntry.Message = msg
			} else {
				// JSONとしてフォーマット
				logEntry.Message = fmt.Sprintf("%v", p)
			}
		default:
			logEntry.Message = fmt.Sprintf("%v", p)
		}

		entries = append(entries, logEntry)

		count++
		if opts.Limit > 0 && count >= opts.Limit {
			break
		}
	}

	return entries, nil
}

// buildFilter はフィルターオプションからフィルター文字列を構築します
func buildFilter(opts FilterOptions) string {
	filter := fmt.Sprintf("resource.type=\"cloud_run_revision\"")

	// プロジェクトIDは既にクライアント作成時に指定されているため、フィルターには含めない

	// サービス名が指定されている場合
	if opts.ServiceName != "" {
		filter += fmt.Sprintf(" AND resource.labels.service_name=\"%s\"", opts.ServiceName)
	}

	// 時間範囲が指定されている場合
	if !opts.StartTime.IsZero() {
		filter += fmt.Sprintf(" AND timestamp>=\"%s\"", opts.StartTime.Format(time.RFC3339))
	}
	if !opts.EndTime.IsZero() {
		filter += fmt.Sprintf(" AND timestamp<=\"%s\"", opts.EndTime.Format(time.RFC3339))
	}

	// ログレベルが指定されている場合
	if opts.LogLevel != "" {
		filter += fmt.Sprintf(" AND severity>=\"%s\"", opts.LogLevel)
	}

	// キーワードが指定されている場合
	for _, keyword := range opts.Keywords {
		if keyword != "" {
			filter += fmt.Sprintf(" AND textPayload:\"%s\"", keyword)
		}
	}

	return filter
}

さほど解説する程のものでもありません。
欲しい機能(ここではログ取得)を独立した関数として再利用可能になるよう意識して実装させ、CLIツールとして動作検証可能なツールとして実装します。
その段階で一度git commitし、コードをmcpサーバーの作法でラッピングする処理を実装させれば完成です。
mcp-goのような新しい、ないしマイナーなライブラリの使い方は適宜教育する必要があるので、
実際にリポジトリを参照させたり、プロンプトに書いたり、人の手で手直ししてあげる必要があります。

隙間家具MCP

GitHub公式play-wright-mcpMCPサーバーとして機能するツールは公式から次から次へとリリースされます。
セキュリティの観点で検索やGitHubリポジトリへのアクセスなど主要な機能はツールは公式のものを使うのが良いでしょう。
一方、簡単すぎる機能はlinuxコマンドの実行などで事足ります。

隙間家具OSSのように痒い所に手が届く絶妙なサイズ感のツールを、隙間家具OSSならぬ隙間家具MCPとして思いつくのが個人開発としては面白いのではと考えています。
(隙間家具 OSS というのは ecspresso の作者である @fujiwara さんが使われている言葉です。)

Cloud Runのログ取得はGoogle Cloud CLI に log-streaming コンポーネントをインストールする事で実行できるようになるようですが、その準備の手間からギリギリmcpサーバーにする意義があったかなと思います。
こういったツールを思いついたら形にしていき、いつの日かSlackでAIに丸投げするだけで障害対応や一次調査ができるようになったら面白いのでは、と考えています。

NOT A HOTEL

Discussion