Blog

Getting Started with Java & OpenTelemetry

 

It’s easy to get started with Java and Honeycomb using OpenTelemetry. With Honeycomb being a big supporter of the OpenTelemetry initiative, all it takes is a few parameters to get your data in. In this post, I will walk through setting up a demo app with the OpenTelemetry Java agent and show how I was able to get rich details with little work by combining automatic instrumentation from the agent with custom instrumentation in the code.

Java Auto-Instrumentation

OpenTelemetry has a robust Java agent for automatically instrumenting your apps. This is the “easy button” for sending trace data to your provider of choice (hopefully Honeycomb). Honeycomb accepts OTLP data directly making quick insights a breeze! You can check out all the supported libraries here

Java Application

I chose the Spring PetClinic demo app for instrumentation. It has a variety of insights we can get from it that the OpenTelemetry Java agent will automatically pick up.

To begin, I first got the code on my machine.

git clone https://github.com/spring-projects/spring-petclinic.git

Next, I hopped into the folder and skipped the testing while building the project using Maven. I was just so excited to get going that I didn’t want to wait for the tests to be done.

cd spring-petclinic
mvn install -DskipTests

I started up the app with the following command and I was ready to go.

java -jar target/*.jar

I was able to navigate to http://localhost:8080/ to verify that everything was working correctly before I added the OpenTelemetry agent.

Home page of the brand-new Spring PetClinic application

OpenTelemetry Instrumentation Agent

For the next part, I downloaded the OpenTelemetry agent and started it up with the Java app. As of the time of this writing, v0.16.0 was the latest version to download.

curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v0.16.0/opentelemetry-javaagent-all.jar > otel.jar

After downloading,  I attached the agent to the jar of the PetClinic demo application. However, by default, the agent assumes it will be headed to an OpenTelemetry collector on the same machine. There are still a few properties to set on startup in order to get data into Honeycomb. I found the names of the attributes to configure in the OpenTelemetry Java Instrumentation README.

export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
export OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=$APIKEY,x-honeycomb-dataset=petclinic
export OTEL_RESOURCE_ATTRIBUTES=service.name=petclinic-app
java -javaagent:otel.jar -jar target/*.jar

Here is what each property means in a bit more detail:

OTEL_TRACES_EXPORTER=otlp,OTEL_METRICS_EXPORTER=none

Set the export for traces as the OTLP protocol and spans only.

OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io

The endpoint is where to export our data. In this case, we’re sending it to Honeycomb’s API directly.

OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=$APIKEY,x-honeycomb-dataset=petclinic

Honeycomb takes in two headers to receive data. The x-honeycomb-team is the Honeycomb API key. The x-honeycomb-dataset is the Honeycomb dataset I am sending to. I found the API key by going to my datasets and clicking the New Dataset button. $APIKEY is replaced with the Honeycomb API Key for the environment.

new dataset view showing a redacted API key

OTEL_RESOURCE_ATTRIBUTES=service.name=petclinic-app

This attribute is for setting service name directly. It allowed me to distinguish my services in my own terms rather than using the default.

After running the above command, I went back to http://localhost:8080/. As I made requests to the demo app, telemetry was generated automatically and sent to Honeycomb.

query results in Honeycomb showing the heatmap visualization of the distribution of latency across a handful of requests in the Pet Clinic app, broken down by HTTP URL

trace visualization for the owner search request, showing the sidebar field with search term Sickles

Right away, I was getting some good insights on my traces, such as the ability to see specific database statements and which database the information comes from. I was able to follow the code in my Java controllers to see what took place as I did various actions in the app. Getting code insights allowed me to quickly find long-running actions in the demo application. However, Honeycomb really shines if I can add highly detailed data. In the case of my PetClinic demo, I was interested in fields like owner, pet, and visit information. I then used manual instrumentation (details below) to add this high cardinality data that the auto-instrumentation did not pick up.

Custom Instrumentation

When considering manual instrumentation, I wondered, “How would that work with the Java agent?” Well, the great thing I found is that manual and auto-instrumentation are compatible with each other as long as they are on the same version. What this means is that I was able to add these custom fields to the spans I’m already generating with auto-instrumentation. 

To do this, I added the OpenTelemetry dependencies, imported them in the code, and used a static method on the Span class.

Since I was using Maven, I added the dependencies to the `pom.xml`. I made sure to use 0.16.0 since that is the version of the instrumentation jar I downloaded.

<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-api</artifactId>
  <version>0.16.0</version>
</dependency>
 <dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-extension-annotations</artifactId>
  <version>0.16.0</version>
</dependency>

Next, I imported the class I needed to modify spans in my src/main/java/owner/OwnerController.java file. I figured this was a good place to get owner details.

import io.opentelemetry.api.trace.Span;

I then found places to set my attributes. I found an easy and quick test on a search method for finding owners—knowing what a user searched for is good highly detailed information! 

@GetMapping("/owners")
 public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
     Span span = Span.current(); // Get the current context in the auto instrumentation
     // allow parameterless GET request for /owners to return all records
     if (owner.getLastName() == null) {
         owner.setLastName(""); // empty string signifies broadest possible search
     }
 
     span.setAttribute("search_term", owner.getLastName()); //set our attribute
 
     // find owners by last name
     Collection<Owner> results = this.owners.findByLastName(owner.getLastName());
     if (results.isEmpty()) {
         // no owners found
         result.rejectValue("lastName", "notFound", "not found");
         return "owners/findOwners";
     }
     else if (results.size() == 1) {
         // 1 owner found
         owner = results.iterator().next();
         return "redirect:/owners/" + owner.getId();
     }
     else {
         // multiple owners found
         model.put("selections", results);
         return "owners/ownersList";
     }
 }

Two lines were all that was needed. One line was to get the current span context from the auto instrumentation, and the second was to set the attribute I wanted visibility on. I rebuilt my code and re-ran it.

mvn spring-javaformat:apply
mvn install -DskipTests
java -javaagent:otel.jar -jar target/*.jar

I went to the owner’s search form and put in a few searches. After refreshing my Honeycomb window, I was able to see the new field in the schema.

red arrow pointing to schema box

I then looked at a trace to see the detailed information in my span.

trace visualization for the owner search request, showing the sidebar field with search term Sickles

I saw that I searched for my name! I then went back and added more attributes. I knew getting the owner would be something I did multiple times, so I built a function to reuse. Here is a method I added to get my owner info into Honeycomb that I reused throughout the codebase.

private void saveOwnerToSpan(Owner owner) {
    Span span = Span.current();
    span.setAttribute("ownerLastName", owner.getLastName());
    span.setAttribute("ownerFirstName", owner.getFirstName());
    span.setAttribute("ownerCity", owner.getCity());
    span.setAttribute("ownerAddress", owner.getAddress());
    if (!owner.isNew()) {
        span.setAttribute("ownerIsNew", false);
        span.setAttribute("ownerId", owner.getId());
    }
    else {
        span.setAttribute("ownerIsNew", true);
    }
}

For this app, owner was something I felt was useful. For other apps, it could be any variable in code like a userID, geographical region, error code, and so on.

Test It Out With Honeycomb

With a few lines of code, I downloaded an example app, auto-instrumented the app, and then added custom instrumentation. By combining automatic and manual instrumentation, I was able to get rich details with little work. Curious to see if it’ll work for you? Try out Honeycomb today and find out!