- Helidon SE Tracing Guide
This guide describes how to create a sample Helidon SE project that can be used to run some basic examples using tracing with a Helidon SE application.
What You Need
For this 30 minute tutorial, you will need the following:
| A Helidon SE Application | You can use your own application or use the Helidon SE Quickstart to create a sample application. |
| Java SE 17 (Open JDK 17) | Helidon requires Java 17+. |
| Maven 3.6.1+ | Helidon requires Maven 3.6.1+. |
| Docker 18.09+ | You need Docker if you want to build and deploy Docker containers. |
| Kubectl 1.16.5+ | If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop. |
java -version
mvn --version
docker --version
kubectl version# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 17`
# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-17Introduction
Distributed tracing is a critical feature of micro-service based applications, since it traces workflow both within a service and across multiple services. This provides insight to sequence and timing data for specific blocks of work, which helps you identify performance and operational issues. Helidon SE includes support for distributed tracing through the OpenTracing API. Tracing is integrated with WebServer, gRPC Server, and Security using either the Zipkin or Jaeger tracers.
Tracing Concepts
This section explains a few concepts that you need to understand before you get started with tracing. In the context of this document, a service is synonymous with an application. A span is the basic unit of work done within a single service, on a single host. Every span has a name, starting timestamp, and duration. For example, the work done by a REST endpoint is a span. A span is associated to a single service, but its descendants can belong to different services and hosts. A trace contains a collection of spans from one or more services, running on one or more hosts. For example, if you trace a service endpoint that calls another service, then the trace would contain spans from both services. Within a trace, spans are organized as a directed acyclic graph (DAG) and can belong to multiple services, running on multiple hosts. The OpenTracing Data Model describes the details at The OpenTracing Semantic Specification. Spans are automatically created by Helidon as needed during execution of the REST request.
Getting Started with Tracing
The examples in this guide demonstrate how to integrate tracing with Helidon, how to view traces, how to trace across multiple services, and how to integrate with tracing with Kubernetes. All examples use Zipkin and traces will be viewed using both the Zipkin API and UI.
Create a Sample Helidon SE Project
Use the Helidon SE Maven archetype to create a simple project that can be used for the examples in this guide.
mvn -U archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=3.2.16 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-quickstart-se \
-Dpackage=io.helidon.examples.quickstart.sehelidon-quickstart-se directory:cd helidon-quickstart-seSet up Zipkin
First, you need to run the Zipkin tracer. Helidon will communicate with this tracer at runtime.
docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin - Run the Zipkin docker image named
openzipkin/zipkin.
curl http://localhost:9411/health - Invoke the Zipkin REST API to check the Zipkin server health.
{
"status": "UP",
"zipkin": {
"status": "UP",
"details": {
"InMemoryStorage{}": {
"status": "UP"
}
}
}
}- All
statusfields should beUP.
Enable Tracing in the Helidon Application
Update the pom.xml file and add the following Zipkin dependency to the <dependencies> section (not <dependencyManagement>). This will enable Helidon to use Zipkin at the default host and port, localhost:9411.
pom.xml:<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-zipkin</artifactId>
</dependency>All spans sent by Helidon to Zipkin need to be associated with a service. Specify the service name below.
resources/application.yaml:tracing:
service: helidon-se-1Main class; Add Tracer to the WebServer builderimport io.helidon.tracing.TracerBuilder;
...
WebServer server = WebServer.builder(createRouting(config))
.config(config.get("server"))
.tracer(TracerBuilder.create(config.get("tracing")).build())
.addMediaSupport(JsonpSupport.create())
.build();- Add a new import statement.
- Build and register a
Tracerobject using the tracing configuration.
GreetService class; Replace the getDefaultMessageHandler method: private void getDefaultMessageHandler(ServerRequest request,
ServerResponse response) {
var span = request.tracer()
.spanBuilder("getDefaultMessageHandler")
.update(spanBuilder -> request.spanContext().ifPresent(spanBuilder::parent))
.start();
try {
sendResponse(response, "World");
span.end();
} catch (Throwable t) {
span.end(t);
}
}- Get the
Tracerobject from the request. - Build a new span named
getDefaultMessageHandler. - Make the new span a child of the request’s span if it has one.
- Start the span. The current timestamp is used as the starting time for the span.
- End the span successfully. The current timestamp is used as the ending time for the span.
- End the span in the error case.
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se.jarcurl http://localhost:8080/greet
...
{
"message": "Hello World!"
}Viewing Tracing Using Zipkin REST API
Because you had tracing enabled, the previous /greet endpoint invocation resulted in a new trace being created. Let’s get the trace data that was generated using the Zipkin API. First, get the service information.
curl http://localhost:9411/api/v2/services
...
["helidon-se-1"] - This is the tracing service name specified in
resources/application.yaml.
Each span used by a service has a name, which is unique within a trace. If you invoke the /greet endpoint multiple times, you will still get the same set of names.
curl -X GET "http://localhost:9411/api/v2/spans?serviceName=helidon-se-1" -H "accept: application/json"
...
[
"content-write",
"getdefaultmessagehandler",
"http request"
]- Get the span names for the
helidon-se-1service. - These are the span names. If you invoke the
/greetendpoint again, then invoke the/spansendpoint, you will get the same response.
Next, get the spans in the trace as shown below.
curl -X GET "http://localhost:9411/api/v2/traces?serviceName=helidon-se-1&limit=1" -H "accept: application/json"
...
[
[
{
"traceId": "f193adb3f2bab3b3",
"parentId": "f193adb3f2bab3b3",
"id": "1536021daf3845e1",
"kind": "SERVER",
"name": "content-write",
"timestamp": 1568245972222815,
"duration": 527,
"localEndpoint": {
"serviceName": "helidon-se-1",
"ipv4": "192.168.1.115"
},
"tags": {
"response.type": "org.glassfish.json.JsonObjectBuilderImpl$JsonObjectImpl"
}
},
...
(truncated)
]- Get the newest trace only, using the
limit=1query param. There are other query params that let you restrict results to a specific time window. - The request will return 3 spans, one for each name.
- Each span has a
parentIdfield, except thehttp requestspan, which is the root.
Viewing Tracing Using Zipkin UI
The tracing output data is verbose and can be difficult to interpret using the REST API, especially since it represents a structure of spans. Zipkin provides a web-based UI at http://localhost:9411/zipkin, where you can see a visual representation of the same data and the relationship between spans within a trace.
Click on the UI refresh button (the search icon) as shown in the image below. Notice that you can change the look-back time to restrict the trace list.

The image below shows the trace summary, including start time and duration of each trace. There are two traces, each one generated in response to a curl http://localhost:8080/greet invocation. The oldest trace will have a much longer duration since there is one-time initialization that occurs.

Click on a trace and you will see the trace detail page where the spans are listed. You can clearly see the root span and the relationship among all the spans in the trace, along with timing information.

A parent span might not depend on the result of the child. This is called a FollowsFrom reference, see Open Tracing Semantic Spec.
You can examine span details by clicking on the span row. Refer to the image below, which shows the span details, including timing information. You can see times for each space relative to the root span. These rows are annotated with Server Start and Server Finish, as shown in the third column.

Tracing Across Services
Helidon automatically traces across services, providing that the services use the same tracer, for example, the same instance of Zipkin. This means a single trace can include spans from multiple services and hosts. OpenTracing uses a SpanContext to propagate tracing information across process boundaries. When you make client API calls, Helidon will internally call OpenTracing APIs to propagate the SpanContext. There is nothing you need to do in your application to make this work.
To demonstrate distributed tracing, you will need to create a second project, where the server listens on port 8081. Create a new root directory to hold this new project, then do the following steps, similar to what you did at the start of this guide:
Create the Second Service
mvn -U archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=io.helidon.archetypes \
-DarchetypeArtifactId=helidon-quickstart-se \
-DarchetypeVersion=3.2.16 \
-DgroupId=io.helidon.examples \
-DartifactId=helidon-quickstart-se-2 \
-Dpackage=io.helidon.examples.quickstart.sehelidon-quickstart-se directory:cd helidon-quickstart-se-2pom.xml:<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-zipkin</artifactId>
</dependency>resources/application.yaml with the following:app:
greeting: "Hello From SE-2"
tracing:
service: "helidon-se-2"
server:
port: 8081
host: 0.0.0.0Main class; Add Tracer to the WebServer builderimport io.helidon.tracing.TracerBuilder;
...
WebServer server = WebServer.builder(createRouting(config))
.config(config.get("server"))
.tracer(TracerBuilder.create(config.get("tracing")).build())
.addMediaSupport(JsonpSupport.create())
.build();GreetService class; Replace the getDefaultMessageHandler method: private void getDefaultMessageHandler(ServerRequest request,
ServerResponse response) {
var span = request.tracer()
.spanBuilder("getDefaultMessageHandler")
.update(spanBuilder -> request.spanContext().ifPresent(spanBuilder::parent))
.start();
try {
sendResponse(response, "World");
span.end();
} catch (Throwable t) {
span.end(t);
}
}- Get the
Tracerobject from the request. - Build a new span named
getDefaultMessageHandler. - Make the new span a child of the request’s span if it has one.
- Start the span. The current timestamp is used as the starting time for the span.
- End the span successfully. The current timestamp is used as the ending time for the span.
- End the span in the error case.
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se-2.jarcurl http://localhost:8081/greet
...
{
"message": "Hello From SE-2 World!"
}Modify the First Service
Once you have validated that the second service is running correctly, you need to modify the original application to call it.
pom.xml:<dependency>
<groupId>io.helidon.security.integration</groupId>
<artifactId>helidon-security-integration-jersey</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.tracing</groupId>
<artifactId>helidon-tracing-jersey-client</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>GreetService class with the following code:package io.helidon.examples.quickstart.se;
import io.helidon.common.http.Http;
import io.helidon.config.Config;
import io.helidon.tracing.jersey.client.ClientTracingFilter;
import io.helidon.webserver.Routing;
import io.helidon.webserver.ServerRequest;
import io.helidon.webserver.ServerResponse;
import io.helidon.webserver.Service;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Invocation;
import jakarta.ws.rs.client.WebTarget;
public class GreetService implements Service {
private final AtomicReference<String> greeting = new AtomicReference<>();
private WebTarget webTarget;
private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());
GreetService(Config config) {
greeting.set(config.get("app.greeting").asString().orElse("Ciao"));
Client jaxRsClient = ClientBuilder.newBuilder().build();
webTarget = jaxRsClient.target("http://localhost:8081/greet");
}
@Override
public void update(Routing.Rules rules) {
rules
.get("/", this::getDefaultMessageHandler)
.get("/outbound", this::outboundMessageHandler)
.put("/greeting", this::updateGreetingHandler);
}
private void getDefaultMessageHandler(ServerRequest request,
ServerResponse response) {
var span = request.tracer()
.spanBuilder("getDefaultMessageHandler")
.update(spanBuilder -> request.spanContext().ifPresent(spanBuilder::parent))
.start();
try {
sendResponse(response, "World");
span.end();
} catch (Throwable t) {
span.end(t);
}
}
private void sendResponse(ServerResponse response, String name) {
String msg = String.format("%s %s!", greeting.get(), name);
JsonObject returnObject = JSON.createObjectBuilder().add("message", msg).build();
response.send(returnObject);
}
private void updateGreetingFromJson(JsonObject jo, ServerResponse response) {
if (!jo.containsKey("greeting")) {
JsonObject jsonErrorObject =
JSON.createObjectBuilder().add("error", "No greeting provided").build();
response.status(Http.Status.BAD_REQUEST_400).send(jsonErrorObject);
return;
}
greeting.set(jo.getString("greeting"));
response.status(Http.Status.NO_CONTENT_204).send();
}
private void outboundMessageHandler(ServerRequest request, ServerResponse response) {
Invocation.Builder requestBuilder = webTarget.request();
var span = request.tracer()
.spanBuilder("outboundMessageHandler")
.update(spanBuilder -> request.spanContext().ifPresent(spanBuilder::parent))
.start();
try {
requestBuilder.property(
ClientTracingFilter.CURRENT_SPAN_CONTEXT_PROPERTY_NAME, request.spanContext());
requestBuilder
.rx()
.get(String.class)
.thenAccept(str -> {
response.send(str);
span.end();
})
.exceptionally(
throwable -> {
// process exception
response.status(Http.Status.INTERNAL_SERVER_ERROR_500);
response.send("Failed with: " + throwable);
span.end(throwable);
return null;
});
} catch (Throwable t) {
span.end(t);
}
}
private void updateGreetingHandler(ServerRequest request, ServerResponse response) {
request.content().as(JsonObject.class).thenAccept(jo -> updateGreetingFromJson(jo, response));
}
}- Add
outboundMessageHandlerto the routing rules. - Create and start a span that is a child of the current span.
- Set a property with the
SpanContext. - Invoke the second service.
- End the span.
curl -i http://localhost:8080/greet/outbound
...
{
"message": "Hello From SE-2 World!"
}- The request went to the service on
8080, which then invoked the service at8081to get the greeting. - Notice the greeting came from the second service.
Refresh the Zipkin UI trace listing page and notice that there is a trace across two services.

Click on the trace with two services to see the detail view.

In the image above, you can see that the trace includes spans from two services. You will notice there is a gap before the sixth span, which is a get operation. This is a one-time client initialization delay. Run the /outbound curl command again and look at the new trace to see that the delay no longer exists.
You can now stop your second service, it is not longer used in this guide.
Integration with Kubernetes
The following example demonstrate how to use Zipkin from a Helidon application running in Kubernetes.
resources/application.yaml with the following:
tracing:
service: helidon-se-1
host: zipkin- Helidon service
helidon-se-1will connect to the Zipkin server at host namezipkin.
docker build -t helidon-tracing-se .Deploy Zipkin into Kubernetes
zipkin.yaml, with the following contents:apiVersion: v1
kind: Service
metadata:
name: zipkin
spec:
ports:
- port: 9411
protocol: TCP
selector:
app: zipkin
---
kind: Pod
apiVersion: v1
metadata:
name: zipkin
labels:
app: zipkin
spec:
containers:
- name: zipkin
image: openzipkin/zipkin
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9411kubectl apply -f ./zipkin.yamlkubectl expose pod zipkin --name=zipkin-external --port=9412 --target-port=9411 --type=LoadBalancerNavigate to http://localhost:9412/zipkin to validate that you can access Zipkin running in Kubernetes. It may take a few seconds before it is ready.
Deploy Your Helidon Application into Kubernetes
tracing.yaml, with the following contents:kind: Service
apiVersion: v1
metadata:
name: helidon-tracing
labels:
app: helidon-tracing
spec:
type: NodePort
selector:
app: helidon-tracing
ports:
- port: 8080
targetPort: 8080
name: http
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: helidon-tracing
spec:
replicas: 1
selector:
matchLabels:
app: helidon-tracing
template:
metadata:
labels:
app: helidon-tracing
version: v1
spec:
containers:
- name: helidon-tracing
image: helidon-tracing-se
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080- A service of type
NodePortthat serves the default routes on port8080. - A deployment with one replica of a pod.
kubectl apply -f ./tracing.yamlAccess Your Application and the Zipkin Trace
kubectl get service/helidon-tracingNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
helidon-tracing NodePort 10.99.159.2 <none> 8080:31143/TCP 8s - A service of type
NodePortthat serves the default routes on port31143.
31143, your port will likely be different:curl http://localhost:31143/greet
...
{
"message": "Hello World!"
}Access the Zipkin UI at http://localhost:9412/zipkin and click on the refresh icon to see the trace that was just created.
Cleanup
You can now delete the Kubernetes resources that were just created during this example.
kubectl delete -f ./zipkin.yaml
kubectl delete -f ./tracing.yaml
kubectl delete service zipkin-external
docker rm -f zipkinSummary
This guide has demonstrated how to use the Helidon SE tracing feature with Zipkin. You have learned to do the following:
Enable tracing within a service
Use tracing with JAX-RS
Use the Zipkin REST API and UI
Use tracing across multiple services
Integrate tracing with Kubernetes
Refer to the following references for additional information: