こんにちは!開発現場の裏側で、日夜ビルドの高速化やCI/CDパイプラインの最適化に頭を悩ませているあなたへ。
JavaやKotlinの開発現場で、いまやなくてはならない存在である「Gradle」。あなたも毎日のように `gradlew build` や `gradlew test` を叩いていることでしょう。
でも、ふと思ったことはありませんか?
「うちのプロジェクト、最近ビルド遅い気がするけど、どのタスクがどれくらい時間を食っているんだっけ?」
「CI環境で、誰のどのブランチがビルド時間を悪化させているのか、定量的にデータで追いたいな……」
ネットを検索すると「Gradleの並列ビルドを有効にしよう」「キャッシュを使おう」といったお決くのチューニング記事がたくさん出てきます。しかし、それらはあくまで「対症療法」に過ぎません。真に開発効率を極限まで高めたいなら、「自チームのビルドメトリクス(KPI)を継続的に計測し、データドリブンでボトルネックを叩く仕組み」を自分たちの手で作る必要があります。
今回は、Gradleのモダンな拡張機能である `BuildService` と `BuildEventListenerRegistry` を使って、ビルド中のタスク実行時間や成功・失敗のステータスをリアルタイムにキャッチし、外部のダッシュボードへ自動送信する「ビルドイベント・リスナー」の作り方を解説します。
これをマスターすれば、あなたのチームのビルドパフォーマンス改善は「感覚」から「科学」へと劇的に進化しますよ。さあ、一緒に深掘りしていきましょう!
—
なぜ従来のビルド計測ではダメなのか?(アーキテクチャの思想)
まず、私たちがこれから作る仕組みの全体像と、なぜそれが優れているのかという背景を少しだけお話しさせてください。
これまで、ビルド時間を計測しようとすると、以下のようなアプローチが取られがちでした。
1. CIのログ(GitHub ActionsやJenkinsのコンソール)を正規表現でパースする。
2. `–profile` オプションを付けて出力されるHTMLレポートを毎度人力で確認する。
しかし、これには大きな欠点があります。
- ログのパースは、フォーマットが変わった瞬間に壊れる脆弱な仕組みになる。
- 開発者のローカル環境(PC)でのビルド傾向がブラックボックスのままになる。
Gradleには、ビルドのライフサイクル(いつどのタスクが始まり、いつ終わり、成功したのか失敗したのか)を安全かつ正確にフックするための公式APIが用意されています。それが `BuildEventListenerRegistry` です。
さらに、Gradle 6.1以降で導入された `BuildService` を使うことで、ビルド内で並列実行されるタスクの間で安全に状態(データを溜め込むバッファやHTTPクライアントなど)を共有できます。これらを組み合わせることで、「ビルドの全貌をリアルタイムに捉え、非同期で安全に外部へ飛ばすスケーラブルなリスナー」を構築できるのです。
—
基礎セットアップ:イベントリスナーの骨組みを作る
百聞は一見に如かず。実際に手を動かして、ビルドの終了時に「どのタスクが何秒かかったか」をコンソールに出力する最小限のプラグイン(リスナー)を作ってみましょう。
今回は、プロジェクト直下の `build.gradle.kts`(Kotlin DSL)に直接ロジックを書いていきますが、大規模なプロジェクトではこれを独自 Gradle プラグインとして切り出すのがベストプラクティスです。
1. BuildServiceの定義
まずは、ビルドイベントを受け取り、データを保持・処理するための「サービス」を定義します。このクラスは、Gradleのビルド並列実行(Configuration CacheやParallel Execution)の安全性を担保するために `BuildService` インターフェースを実装します。
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.tooling.events.OperationCompletionListener
import org.gradle.tooling.events.FinishEvent
import org.gradle.tooling.events.task.TaskFinishEvent
import java.util.concurrent.ConcurrentHashMap
import java.time.Duration
// 1. BuildServiceで共有するパラメータの定義(今回はパラメータなしなのでParameters.Noneを使用)
abstract class MetricsCollectorService : BuildService
// 複数タスクから並列でアクセスされるため、スレッドセーフなConcurrentHashMapを使用
private val taskExecutionTimes = ConcurrentHashMap
// タスクが完了するたびにGradleから非同期で呼び出されるコールバック
override fun onFinish(event: FinishEvent) {
// イベントが「タスクの完了」である場合のみ処理を絞り込む
if (event is TaskFinishEvent) {
val taskPath = event.descriptor.taskPath
val startTime = event.result.startTime
val endTime = event.result.endTime
val duration = endTime – startTime
// マップにタスク名と実行時間を保存(ここでDBや外部APIへの送信バッファに詰める)
taskExecutionTimes[taskPath] = duration
val status = if (event.result.succeeded) “SUCCESS” else “FAILED”
println(“[MetricsService] タスク ‘$taskPath’ がステータス [$status] で完了しました (所要時間: ${duration}ms)”)
}
}
// ビルドが完全に終了し、Gradleがシャットダウンする直前に呼ばれる
override fun close() {
println(“=== [MetricsService] ビルド統計データの送信処理を開始します ===”)
// 実務では、ここでOkHttpなどのHTTPクライアントを使い、
// DatadogやElasticsearch、社内の独自ダッシュボードAPIへ一括送信(JSON化)します。
println(“合計 ${taskExecutionTimes.size} 個のタスクメグトリクスを収集しました。”)
println(“=== [MetricsService] 送信完了 ===”)
}
}
2. EventListenerRegistryへの登録
次に、作成した `MetricsCollectorService` をGradleのイベントレジストリに登録します。これにより、Gradleはビルド中のあらゆるイベントをこのサービスに通知するようになります。
import org.gradle.tooling.events.task.TaskOperationDescriptor
// Gradleのプロジェクトライフサイクルにフックしてサービスを登録する
val metricsServiceRegistration = gradle.sharedServices.registerIfAbsent(
“metricsCollector”, // サービスを一意に識別する名前
MetricsCollectorService::class.java
) {
// 必要に応じてここで外部設定(APIのエンドポイントなど)をパラメータとして渡せます
}
// BuildEventListenerRegistryを取得し、タスクの完了イベントを購読させる
val eventListenerRegistry = the
eventListenerRegistry.onTaskCompletion(metricsServiceRegistration)
—
精度高い動作確認:HelloWorldを動かしてみよう
上記のコードを、プロジェクトのルートにある `build.gradle.kts` の最下部に丸ごと貼り付けてみてください。
そして、ターミナルで適当なタスク(例えば `help` や `classes`)を実行してみましょう。
$ ./gradlew classes –no-configuration-cache
実行ログのイメージ
ビルドが走ると、標準出力に次のようなログがリアルタイムで流れてくるはずです。
> Task :compileJava NO-SOURCE
[MetricsService] タスク ‘:compileJava’ がステータス [SUCCESS] で完了しました (所要時間: 12ms)
> Task :processResources NO-SOURCE
[MetricsService] タスク ‘:processResources’ がステータス [SUCCESS] で完了しました (所要時間: 2ms)
> Task :classes UP-TO-DATE
[MetricsService] タスク ‘:classes’ がステータス [SUCCESS] で完了しました (所要時間: 0ms)
BUILD SUCCESSFUL in 1s
4 actionable tasks: 2 up-to-date, 2 no-source
=== [MetricsService] ビルド統計データの送信処理を開始します ===
合計 3 個のタスクメグトリクスを収集しました。
=== [MetricsService] 送信完了 ===
どうですか?狙い通り、タスクごとの実行時間がミリ秒単位でキャッチされ、ビルドの終了(`close()` メソッドのタイミング)に集計処理が走っているのが確認できたはずです。
—
実務への応用:独自ダッシュボード自動送信アーキテクチャ案
「コンソールに出るだけじゃ、CIの分析に使えないよ」と思いましたよね?
ここからがアーキテクトの腕の見せ所です。この仕組みを実務のCI/CDパイプライン(GitHub Actionsなど)で強力に活かすためのアーキテクチャ案を提示します。
[Gradleビルド実行 (CI)]
│
▼ (BuildServiceでデータ収集)
[MetricsCollectorService.close()]
│
▼ (JSONにシリアライズしてHTTP POST)
[社内BI / Prometheus / Elasticsearch / 独自ダッシュボードAPI]
│
▼ (Grafanaなどで可視化)
「今週、誰がどのモジュールでビルド時間をロスしているか」が一目で分かるダッシュボード
実務で実装する際のポイント
1. APIキーやエンドポイントの秘匿:
ハードコードせず、システムプロパティや環境変数(例: `System.getenv(“METRICS_API_URL”)`)経由で `BuildService` にパラメータとして渡します。
2. 非同期・ノンブロッキング送信:
`close()` メソッド内でのHTTPリクエストが失敗しても、「ビルド自体の成否(成功・失敗)」を落とさないようにtry-catchで例外をハンドリングし、CIを無駄に止めない配慮がプロの技です。
3. メタデータの付与:
タスクの実行時間だけでなく、以下のメタデータを一緒に送信ペイロードに含めると、ダッシュボードでの分析精度が跳ね上がります。
- Gitのブランチ名(`git rev-parse –abbrev-ref HEAD` の結果など)
- コミットハッシュ
- CI環境のランナーID(GitHub Actionsの `GITHUB_RUN_ID` など)
- 開発者のマシン名(ローカル計測の場合)
—
まとめ
今回は、Gradleの `BuildService` と `BuildEventListenerRegistry` を使って、ビルドパフォーマンスを独自に計測・収集する基盤の作り方を解説しました。
- BuildService を使えば、並列処理されるタスク間でも安全に状態を共有できる。
- BuildEventListenerRegistry を使えば、タスクの成功・失敗や実行時間を正確かつエレガントにフックできる。
- これを外部のダッシュボードAPIへ飛ばすことで、チーム全体のビルド最適化をデータドリブンに進められる。
「なんとなくビルドが遅い気がする」というモヤモヤからチームを解放し、客観的なメトリクスに基づいた高速化を実現できれば、毎日のコーディング体験は驚くほど快適になりますよ。
ぜひ、あなたのプロジェクトの `build.gradle.kts` に組み込んで、社内ダッシュボードへの第一歩を踏み出してみてください。応援しています!