gradle runの実行結果を別コマンドにパイプして出力する

2021/02/23に公開

2021-02-23 22:02更新: 実行環境を明確にして処理を簡略化しました。

./gradlew run で起動したアプリケーションの標準出力を別のコマンドにパイプして出力したいという欲求がありました。具体的には、Webアプリケーションから標準出力にJSON形式で出力されたログを、ローカル実行時だけ pino-pretty コマンドを使って読みやすく整形して出力したいというものです。

検討したけど採用しなかった案

CLIで実行するだけであれば、次のようにパイプすれば良いですが、IntelliJ IDEAからアプリケーションを起動した場合にもログを整形したかったので採用しませんでした。IntelliJ IDEAから起動したアプリケーションに対して、パイプするプロセスを指定する方法は見つけられませんでした。

./gradlew run | pino-pretty

また、IntelliJ IDEAからShell Scriptで上記のコマンドを起動するという案もありましたが、アプリケーションをデバッグ実行できないため見送りました。

pino-pretty にそこまでこだわりはないので、IntelliJ IDEAでJSON形式のログを整形して表示するプラグインとかあるかなーと思って探してみましたが、特に見つかりませんでした。

アプリケーション内で開発時だけ整形して出力するという案は、Twelve Factor AppのDev/prod parityの観点からあまり好ましくないと考えているので採用しませんでした。

実装

最終的に build.gradle.kts で次のように実装することで、 ./gradlew run を実行するだけで pino-pretty コマンドを使って整形できるようになりました。IntelliJで起動した場合も同様の出力が得られます。

run タスクの実行前にProcessBuilderを使って pino-pretty を起動し、 run で起動するアプリケーションの標準出力を pino-pretty の標準入力にパイプするというものです。

import kotlin.concurrent.thread

tasks.named<JavaExec>("run") {
    doFirst {
        val process = ProcessBuilder("pino-pretty", "--colorize").start()
         // 起動するアプリケーションの標準出力をpino-prettyの標準入力にパイプする。
        standardOutput = process.outputStream

        thread {
            // pino-prettyの標準出力を現在の標準出力に転送する。
            process.inputStream.transferTo(System.out)
        }
    }
}

※上記のコードでは省略しましたが、実際には -PnoPretty=1 をつけた場合はdoFirst自体を実行しないという処理を入れて、pino-pretty コマンド無しでも起動できるようにしています。

実行環境は次の通りです。

  • Gradle: 6.6.1
  • JVM: AdoptOpenJDK 11.0.10+9

実行イメージ

IntelliJ IDEAでの実行イメージは次の通りです。

わかってないこと

ProcessBuilderについて調べると、 .redirectOutput(ProcessBuilder.Redirect.INHERIT) とすることで現在の標準出力と同じになるとありましたが、これを指定して pino-pretty を起動しても標準出力には何も表示されませんでした。仕方ないので、別スレッドで pino-pretty の標準出力を読み取っては標準出力に転送する処理を実行しています。

ps で見る限りプロセス自体は起動しているので、Gradleが標準出力を捨ててるのかなーと推察していますが、同じ箇所で println した文字列は出力されており、いまいちよくわかっていません。

最後に

ニッチな欲求かと思いますが、調べてもすぐに見つからなかったので、書いておきました。上記のわかってないことを含め、もう少し良い方法がありそうなので、もしご存知の方は教えていただけると幸いです。

参考

Discussion