So, I just spent this Friday afternoon at $DAYJOB figuring out how to instrument a Java application (of which we have no source code available) with NewRelic.
I thought I’d share my findings here, should I ever need to do this again.
The Problem
So, at $DAYJOB, we run a Java application in a Docker container (actually, on a Kubernetes cluster, but anyway). We’re trying to figure out some performance issues in our whole stack, and we already use NewRelic for other monitoring tasks, so why not?
I’ll also point out that the vendor decided to not provide any OpenTelemetry instrumentation in their code (which we would have preferred).
The Solution
So, here is what I did:
I created a new
Dockerfile. It looks like this:ARG ORIGINAL_TAG="latest" FROM curlimages/curl:latest AS downloader WORKDIR /home/curl_user RUN curl -O https://download.newrelic.com/newrelic/java-agent/newrelic-agent/current/newrelic-java.zip RUN unzip newrelic-java.zip ADD ./newrelic.yml /home/curl_user/newrelic.yml FROM --platform=amd64 <IMAGE>:${ORIGINAL_TAG} AS original COPY --from=downloader /home/curl_user/newrelic/newrelic.jar /app COPY --from=downloader /home/curl_user/newrelic.yml /app WORKDIR /app USER root RUN mkdir -p /app/logs && chown appuser -R /app/logs USER appuser ENV APP_JAVA_OPS="-javaagent:/app/newrelic.jar" ENTRYPOINT ["/bin/bash", "/app/runserver.sh"]Basically, it uses
curlto download the NewRelic Java agent and copies it into the final image, along with the NewRelic configuration file (which is stored in the Git repository alongside theDockerfile). As you can see, I just added the-javaagent:/app/newrelic.jarto theAPP_JAVA_OPSenvironment variable. (Your mileage may vary here, depending on how your application is started.)
Some things to note:- The
/app/runserver.shscript is the entrypoint for the container and starts the Java application. I don’t need or want to know what’s in there. - I injected the
ORIGINAL_TAGargument to theDockerfile, and then I rebuilt the image with the same tag as the original image. - I used the
--platform=amd64flag, because I also need to build this image for ARM64 and the vendor does not provide anarm64version of their image. (In practice, I have anotherDockerfilefor ARM64 that looks almost the same, but also uses an ARM64 Java base image.)1
- The
I created a
newrelic.ymlfile that looks like this:common: &default_settings license_key: "NEWRELIC_LICENSE_KEY" app_name: "APP_NAME" # lots of other things here, check the NewRelic documentation if you're interestedI used this excellent GitHub action to replace the placeholders with the actual values, stored in GitHub secrets, before building the Docker image.
The GitHub action step looks like this:- name: Update newrelic.yml uses: fjogeleit/yaml-update-action@main with: valueFile: 'newrelic.yml' changes: | { "common.license_key": "${{ secrets.NEWRELIC_LICENSE_KEY }}" "common.app_name": "${{ secrets.APP_NAME }}" } commitChange: falseThere is no step 3. That’s it. The Java application is now instrumented with NewRelic.
Thanks for reading. I hope this helps someone else in the future.