NewRelic java agent の JAR 管理を Gradle に任せる

# 環境
- java: 17
- Gradle: 8.1.1
- NewRelic java agent: 8.6.0

# モチベーション
NewRelic java agent の公式のインストール手順では、JAR ファイルをダウンロードすることになっています。

https://docs.newrelic.com/jp/install/java/

今どき JAR ファイルを手作業でダウンロードというのは前時代的ですし、依存性を追跡できないのでアップデートも億劫になりがちです。
また、CI の過程で JAR を都度ダウンロードさせる場合は、(CI が頻発することで) 公式のホスティングサイトに多大なトラフィックが流れていかないよう、ダウンロード結果をキャッシュしておくのがマナーですが、この実装も地味に面倒です。
やはり maven や gradle の dependency として管理したいところです。


:::note info
newrelic.jar デカすぎ問題
そうは言っても、JAR なんてたまにダウンロードして commit しておくだけじゃんという反論もあると思います。
まあそうなんですが、 newrelic.jar ファイルって結構デカいんです。
(執筆時点で 22MB)

私の環境では、デプロイに AWS CodePipeline を使っているのですが、ソースリポジトリに newrelic.jar が含まれている状態だと Source artifact のサイズ制限に引っかかってビルドステージがエラーになってしまいました。
https://docs.aws.amazon.com/codepipeline/latest/userguide/limits.html

なので、CI/CD 中にダウンロードせざるを得ない状況でした。
:::


# そもそも JAR は Maven central に登録されているのか？
NewRelic java agent のリポジトリ ([newrelic/newrelic-java-agent](https://github.com/newrelic/newrelic-java-agent)) の README を見ると、以下のようにあります。

> Java agent releases follow semantic versioning conventions. See Java agent release notes for full details on releases and downloads. Java agent artifacts can also be found on Maven.

リンクを辿ると `com.newrelic.agent.java` という Group ID で複数のアーティファクトが登録されているのがわかります。

https://central.sonatype.com/search?q=com.newrelic.agent.java&smo=true

これらのアーティファクトのうち、 `newrelic-agent` が javaagent 本体、 `newrelic-api` が NewRelic APM の API のようです。

ということで、Maven central には JAR ファイルが登録されているようです。

# プロジェクト設定
maven / gradle の依存性管理に任せられる条件は整っていることを確認できたので、
プロジェクトの設定をしていきます。

## ディレクトリ構成
ディレクトリ構成は、以下の通りとします。
(gradle wrapper 等のファイルは省略してます)

```
${PROJECT_ROOT}
  |
  +-- build.gradle
  +-- settings.gradle
  +-- newrelic/
  |     |
  |     +-- newrelic.yml
  |     +-- extensions/
  |           |
  |           +-- extension-xxx.xml
  |
  +-- src/
```

ポイントは、 `newrelic` ディレクトリの下に NewRelic の設定ファイルが置いてある点です。
公式の newrelic.zip を解凍して設定ファイルだけ残したような状態です。
(NewRelic の用語的には、これが `newrelic.home` ディレクトリに相当)
それ以外は普通の gradle プロジェクトです。

## build.gradle
以下のように設定しました。
```gradle
buildscript {
  ext {
    newrelicVersion = "8.6.0"
  }
}

configurations {
  runtimeAgent
}

dependencies {
  implementation "com.newrelic.agent.java:newrelic-api:${newrelicVersion}"
  runtimeAgent "com.newrelic.agent.java:newrelic-agent:${newrelicVersion}"
}

bootRun {
  jvmArgs = [
    "-javaagent:${configurations.runtimeAgent.singleFile}",
    '-Xshare:off'
  ]
  // newrelic.yml が newrelic-agent.jar と違うディレクトリにあるので指定が必要
  systemProperty 'newrelic.home', 'newrelic'
}

//
// 以下は NewRelic の Custom instrumentation 設定ファイル (extensions/*.xml) の
// validation をしたい場合のみ必要。
//
task newrelicValidate() {
  ext {
    fileLocation = 'newrelic/extensions'
    targets = fileTree(fileLocation).include('**/*.xml')
  }
  description "Validate NewRelic instrument file (${fileLocation})"
  targets.each { xml ->
    doLast {
      javaexec {
        classpath = sourceSets.main.runtimeClasspath + configurations.runtimeAgent
        mainClass = 'com.newrelic.bootstrap.BootstrapAgent'
        args = ['instrument', '-file', xml.absolutePath, '-debug', 'true']
      }
    }
  }
  dependsOn tasks.named('compileJava')
  onlyIf { !targets.isEmpty() }
}

//
// 以下は Jib でコンテナをビルドする場合のみ必要。
//
task jibPrepare(type: Copy) {
  ext {
    destDir = "${project.buildDir}/newrelic"
  }
  doFirst {
    mkdir destDir
  }
  from 'newrelic'
  from "${configurations.runtimeAgent.singleFile}"
  into destDir
}

jib {
  // ...snip...
  container {
    jvmFlags = [
      "-javaagent:./newrelic/${configurations.runtimeAgent.singleFile.name}",
      '-Xshare:off'
    ]
    appRoot = '/app'
    workingDirectory = '/app'
    extraDirectories {
      paths {
        path {
          from = "${project.buildDir}/newrelic"
          into = '/app/newrelic'
        }
      }
    }
  }
}

tasks.jibDockerBuild.dependsOn jibPrepare
```

# 動作確認
設定できたので、ビルドして動かしてみます。

## bootRun
bootRun でサーバを起動してみます。
```
$ NEW_RELIC_LICENSE_KEY=xxxxxxxxx ./gradlew bootrun

> Task :bootRun
2023-10-25T19:14:22,922+0900 [58328 1] com.newrelic INFO: New Relic Agent: Loading configuration file "newrelic/./newrelic.yml"

(...snip...)

2023-10-25T19:14:26,319+0900 [58328 1] com.newrelic INFO: New Relic Agent v8.6.0 has started
2023-10-25 19:14:33.535 INFO Application - Started Application in 0.984 seconds
```
ログから Agent がきちんと起動していることがわかります。

## image の内容
私は [Jib](https://github.com/GoogleContainerTools/jib) でコンテナイメージをビルドしているので、作成されたイメージの中に JAR が含まれているか確認します。

```
$ ./gradlew jibDockerBuild
(...snip...)

$ docker run --entrypoint '' --rm -it xxx:xxx ls -l /app/newrelic 
total 37176
drwxr-xr-x    2 root     root          4096 Jan  1  1970 extensions
-rw-r--r--    1 root     root      38040677 Jan  1  1970 newrelic-agent-8.6.0.jar
-rw-r--r--    1 root     root         19784 Jan  1  1970 newrelic.yml
```
所定の位置にファイルが配置されています。

# まとめ
NewRelic java agent の JAR ファイルを Gradle で管理する方法を紹介しました。
試してませんが、同じ方法で Datadog や OpenTelemetry の javaagent も設定できるんじゃないかと思います。

別の方法として、 javaagent を簡単に設定するための Gradle プラグインを開発している方もいるようです。

https://github.com/ryandens/javaagent-gradle-plugin

まだ知名度がそんなに高くなさそうなので、 version 1.0 になったら採用を検討しようと思います。