Alex
Alex Passionate about inclusivity, sustainability, cycling and health. Software problems solver, aspiring writer. My opinions are my own. He/him

Practical OpenTelemetry part 3: Java

Practical OpenTelemetry part 3: Java

A grocery store isn’t much of a store if there’s no way to buy anything. After visiting Python in part 1 and Go in part 2, it’s time to visit Java in part of the practical OpenTelemetry series. In this post we’ll add the beginnings of an order processing system to our grocery store. This new service, the checkout will receive a POST request over HTTP and place an order job in a Redis message queue, which will be picked up by a future service. Using a Docker container, let’s start by setting up redis locally, the same way we launched the Jaeger backend in part 1. Run the following command in a terminal:

1
docker run --name redis --rm -p 6379:6379 redis:6.2

Note that Redis is accessed on port 6379. If things went smoothly, the following message should appear:

1
2
3
...
1:M 18 Mar 2021 17:08:44.224 # Server initialized
1:M 18 Mar 2021 17:08:44.224 * Ready to accept connections

Let’s test the redis port and make sure we can connect to it. I like using netcat to test whether ports are accessible or not. The following command sends the redis command hkeys * to the port via nc:

1
2
3
echo 'hkeys *' | nc -v localhost 6379
Connection to localhost port 6379 [tcp/*] succeeded!
*0

The succeded message tells us we successfully connected to the port, and *0 is redis’ response to the command we passed in. So far so good.

architecture-diagram

Checkout service

Not having touched Java in some time, I decided to use Spring for the web application and the Spring Initializer to get started. I updated my app’s name, added the Spring Web and Spring Data Reactive Redis dependencies and clicked the Generate button.

spring-initializer

The source code was then downloaded and added to the codeboten/practical-otel repo. In order to receive a request to our app, let’s add a RestController that will accept POST requests to the following URL /orders:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.checkout;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CheckoutController {

    @Autowired
    private ApplicationContext context;

    String serializeOrder(Order order) {
        Jackson2JsonRedisSerializer<Order> serializer = new Jackson2JsonRedisSerializer<>(Order.class);
        return new String(serializer.serialize(order));
    }

    void sendMessage(String message) {
        StringRedisTemplate template = context.getBean(StringRedisTemplate.class);
        template.convertAndSend("orders", message);
    }

    @PostMapping("/orders")
    Order newOrder(@RequestBody Order newOrder) {
        Order order = new Order();
        order.setItems(newOrder.getItems());
        String serializedOrder = serializeOrder(order);
        sendMessage(serializedOrder);
        return order;
    }
}

You can see that newOrder receives an Order from the RequestBody, serializes it to a JSON string which it then sends to Redis. We’re keeping the models pretty minimal as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Order.java
package com.example.checkout;

import java.util.List;
import java.util.UUID;

public class Order {
    public String id;
    public List<LineItem> items;

    public Order() {
        id = UUID.randomUUID().toString();
    }

    public List<LineItem> getItems() {
        return items;
    }

    public void setItems(List<LineItem> items) {
        this.items = items;
    }
}

// LineItem.java
package com.example.checkout;

public class LineItem {
    public String id;
    public int quantity;
}

Once that’s all done, let’s build our JAR by running the following command to compile the application:

1
2
git clone https://github.com/codeboten/practical-otel.git && cd ./practical-otel/part3/Checkout
./mvnw clean package spring-boot:repackage

Auto Instrumentation

The Java instrumentation community provides a mechanism that automatically instruments various libraries by leveraging a single JAR. For the first part of our example, let’s use this to get some traces. First let’s download the opentelemetry-javaagent-all.jar:

1
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.0.0/opentelemetry-javaagent-all.jar

You can find the latest releases on the community’s repository in GitHub. The Java agent supports configuring a variety of useful options, like the exporter and resource attributes via application properties which can be set via the command line. Note I’m setting a service.name as well as the jaeger exporter this way, pretty convenient! By default Spring will run on port 8080, but we already have the inventory service running on that port, so we’ll use 8083 instead:

1
2
3
4
5
6
7
8
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.0.0/opentelemetry-javaagent-all.jar

java -javaagent:./opentelemetry-javaagent-all.jar \
     -Dserver.port=8083 \
     -Dotel.resource.attributes=service.name=checkout \
     -Dotel.traces.exporter=jaeger \
     -Dotel.metrics.exporter=none \
     -jar ./target/Checkout-0.0.1-SNAPSHOT.jar

If all that went well, we should see Spring starting and the application is ready to go:

1
2
2021-03-18 10:38:56.211  INFO 88326 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8083 (http) with context path ''
2021-03-18 10:38:56.221  INFO 88326 --- [           main] c.example.Checkout.CheckoutApplication   : Started CheckoutApplication in 3.271 seconds (JVM running for 4.91)

Now let’s update grocery_store.py to provide a new endpoint that sends a set order to the checkout service. We’ll add a front end in a future Practical OpenTelemetry article, so for now we just hardcode the order in the request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# grocery_store_server.py
# ...
@app.route("/checkout")
def checkout():
    checkout_service = os.environ.get(
        "CHECKOUT_SERVICE_URL", "http://localhost:8083/orders"
    )
    order = {
        "items": [
            {"id": "1", "quantity": 10},
            {"id": "2", "quantity": 20},
        ],
    }
    res = requests.post(checkout_service, json=order)
    return res.text

We’ll also add another request to the bottom of shopper.py to call the new checkout functionality:

1
2
3
4
# shopper.py
# ...
    res = requests.get("http://localhost:5000/checkout")
    print(res.text)

Next time we run the shopper and look at the traces, we should now see a new service in the trace:

1
python ./part2/shopper.py

traces-with-four-services

Manual instrumentation

Auto instrumentation is great, but it’s really just a starting point. Luckily, we can combine manual instrumentation with the automatic instrumentation to get even more details about what our application is doing. Let’s add some manual traces! Let’s update pom.xml to add a dependency for the OpenTelemetry api:

1
2
3
4
5
6
7
8
9
10
		<dependency>
		<groupId>io.opentelemetry</groupId>
			<artifactId>opentelemetry-api</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>io.opentelemetry</groupId>
			<artifactId>opentelemetry-extension-annotations</artifactId>
			<version>1.0.0</version>
		</dependency>

In the code, we can use the WithSpan annotation on the methods we want to add spans for. In our example app, it could be useful to have a span for the serialization and redis operations, in case something goes wrong, so let’s add them now:

1
2
3
4
5
6
7
8
9
10
// CheckoutController.java
...
import io.opentelemetry.extension.annotations.WithSpan;
...
    @WithSpan
    String serializeOrder(Order order) {
...
    @WithSpan
    void sendMessage(String message) {
...

The last of code we’ll add is an attribute to our span to track the order-id, this could be helpful if a particular order didn’t go through or if a customer calls to ask what happened. We can use the OpenTelemetry API to do this:

1
2
3
4
5
6
7
8
9
// CheckoutController.java
...
import io.opentelemetry.api.trace.Span;
...
    @WithSpan
    String serializeOrder(Order order) {
				Span span = Span.current();
        span.setAttribute("order-id", order.id);
...

Recompile the code and let’s see what this looks like once we put a new request through:

traces-with-manual-instrumentation

Looking through the new trace, there’s a new span for each method we add instrumentation for and now there’s even an order-id attribute on the serializeOrder span! We managed to build a sample Spring app, instrument it automatically with Java auto instrumentation and now we can augment the tracing data to better suit our specific needs! As always, the full example is available in the Practical OpenTelemetry repo.


Photo by Pawel Nolbert on Unsplash