///////////////////////////////////////////////////////////////////////////////

    Copyright (c) 2019, 2020 Oracle and/or its affiliates.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

///////////////////////////////////////////////////////////////////////////////

= Helidon SE Tracing Guide
:description: Helidon tracing
:keywords: helidon, tracing, microprofile, guide
:toc:

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

[width=50%,role="flex, sm7"]
|===
|About 30 minutes
|<<about/03_prerequisites.adoc,Helidon Prerequisites>>
|===

== Introduction

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 https://opentracing.io[OpenTracing API].  Tracing is integrated with WebServer, gRPC Server,
and Security using either the https://zipkin.io[Zipkin] or https://www.jaegertracing.io[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 https://opentracing.io/specification[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.

[source,bash,subs="attributes+"]
.Run the Maven archetype:
----
mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion={helidon-version} \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se \
    -Dpackage=io.helidon.examples.quickstart.se
----

[source,bash]
.The project will be built and run from the `helidon-quickstart-se` directory:
----
cd helidon-quickstart-se
----

=== Setup Zipkin

First, you need to run the Zipkin tracer.  Helidon will communicate with this tracer at runtime.

[source,bash]
.Run Zipkin within a docker container, then check the Zipkin server health:
----
docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin  // <1>
----
<1> Run the Zipkin docker image named `openzipkin/zipkin`.


[source,bash]
.Check the Zipkin server health:
----
curl http://localhost:9411/health // <1>
...
{
  "status": "UP", <2>
  "zipkin": {
    "status": "UP",
    "details": {
      "InMemoryStorage{}": {
        "status": "UP"
      }
    }
  }
}
----
<1> Invoke the Zipkin REST API to check the Zipkin server health.
<2> All `status` fields should be `UP`.

=== Enable Tracing in your 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`.

[source,xml]
.Add the following dependency to `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.

[source,bash]
.Add the following line to `resources/application.yaml`:
----
tracing:
  service: helidon-se-1
----

[source,java]
.Update the `Main` class; 1) Add a new import, 2) Change the code that initializes `serverConfig` to use the new `buildServerConfig` method., and 3) Add the `buildServerConfig` method:
----
import io.helidon.tracing.TracerBuilder; // <1>
...
    // Replace the existing line of code
    // ServerConfiguration serverConfig = ServerConfiguration.create(config.get("server"));
    // with ` ServerConfiguration serverConfig = buildServerConfig(config);`
    ServerConfiguration serverConfig = buildServerConfig(config); // <2>
...

  private static ServerConfiguration buildServerConfig(Config config) {  // <3>
    return ServerConfiguration.builder()
        .config(config.get("server"))
        .tracer(TracerBuilder.create(config.get("tracing")).buildAndRegister()) // <4>
        .build();
  }

----
<1> Add a new import statement.
<2> Call the new `buildServerConfig` method to build a `ServerConfiguration` object.
<3> Build the `ServerConfiguration` object.
<3> Build and register a `Tracer` object using the tracing configuration.

[source,java]
.Update the `GreetService` class; 1) Add a new import and 2) Replace the `getDefaultMessageHandler` method:
----
import io.opentracing.Span; // <1>
...
    private void getDefaultMessageHandler(ServerRequest request,
                                   ServerResponse response) {

        Span span = request.tracer()  // <2>
            .buildSpan("getDefaultMessageHandler")  // <3>
            .asChildOf(request.spanContext())  // <4>
            .start(); // <5>

        try {
            sendResponse(response, "World");
        } finally {
            span.finish();  // <6>
        }
    }
----
<1> Add new import statement.
<2> Get the `Tracer` object from the request.
<3> Build a new span named `getDefaultMessageHandler`.
<4> Make the a new span a child of the current span.
<5> Start the span.  The current timestamp is used as the starting time for the span.
<6> Finish the span.  The current timestamp is used as the ending time for the span.


[source,bash]
.Build the application, skipping unit tests, then run it:
----
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se.jar
----

[source,bash]
.Run the curl command in a new terminal window and check the response:
----
curl 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.

[source,bash]
.Run the curl command and check the response:
----
curl http://localhost:9411/api/v2/services
...
["helidon-se-1"] // <1>
----
<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.

[source,bash]
.Invoke the endpoint below and check the response:
----
// <1>
curl -X GET "http://localhost:9411/api/v2/spans?serviceName=helidon-se-1" -H "accept: application/json"
...
[ // <2>
  "content-write",
  "getdefaultmessagehandler",
  "http request"
]
----
<1> Get the span names for the `helidon-se-1` service.
<2> These are the span names.  If you invoke the `/greet` endpoint again, then
invoke the `/spans` endpoint, you will get the same response.


Next, get the spans in the trace as shown below.

[source,bash]
.Invoke the endpoint below and check the response:
----
// <1>
curl -X GET "http://localhost:9411/api/v2/traces?serviceName=helidon-se-1&limit=1" -H "accept: application/json"
...
[
  [ // <2>
    {
      "traceId": "f193adb3f2bab3b3",
      "parentId": "f193adb3f2bab3b3", // <3>
      "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)
]

----
<1> Get the newest trace only, using the `limit=1` query param.  There are other query params that let you restrict
results to a specific time window.
<2> The request will return 3 spans, one for each name.
<3> Each span has a `parentId` field, except the `http request` span, 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.

.Trace refresh
image::guides/12_tracing_se_refresh.png[Trace Refresh]

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.

.Tracing list view
image::guides/12_tracing_se_top.png[Traces]

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.

.Trace detail page
image::guides/12_tracing_se_detail.png[Trace Detail]

NOTE: A parent span might not depend on the result of the child. This is called a `FollowsFrom` reference, see
https://github.com/opentracing/specification/blob/master/specification.md[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.

.Span detail page
image::guides/12_tracing_se_span_detail.png[Span Details]


=== 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

[source,bash,subs="attributes+"]
.Run the Maven archetype:
----
mvn archetype:generate -DinteractiveMode=false \
    -DarchetypeGroupId=io.helidon.archetypes \
    -DarchetypeArtifactId=helidon-quickstart-se \
    -DarchetypeVersion={helidon-version} \
    -DgroupId=io.helidon.examples \
    -DartifactId=helidon-quickstart-se-2 \
    -Dpackage=io.helidon.examples.quickstart.se
----

[source,bash]
.The project will be built and run from the `helidon-quickstart-se` directory:
----
cd helidon-quickstart-se-2
----

[source,xml]
.Add the following dependency to `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>
----

[source,bash]
.Replace `resources/application.yaml` with the following:
----
app:
  greeting: "Hello From SE-2"

tracing:
  service: "helidon-se-2"

server:
  port: 8081
  host: 0.0.0.0
----

[source,java]
.Update the `Main` class; 1) Add a new import, 2) call the `buildServerConfig()` method, and 3) Add the `buildServerConfig` method:
----
import io.helidon.tracing.TracerBuilder;
...
    ServerConfiguration serverConfig = buildServerConfig(config);
...

  private static ServerConfiguration buildServerConfig(Config config) {
    return ServerConfiguration.builder()
        .config(config.get("server"))
        .tracer(TracerBuilder.create(config.get("tracing")).buildAndRegister())
        .build();
  }
----

[source,java]
.Update the `GreetService` class; 1) Add new import and 2) Replace the `getDefaultMessageHandler` method:
----
import io.opentracing.Span;
...
    private void getDefaultMessageHandler(ServerRequest request,
                                   ServerResponse response) {

        Span span = request.tracer()
            .buildSpan("getDefaultMessageHandler")
            .asChildOf(request.spanContext())
            .start();

        try {
            sendResponse(response, "World");
        } finally {
            span.finish();
        }
    }
----

[source,bash]
.Build the application, skipping unit tests, then run it:
----
mvn package -DskipTests=true
java -jar target/helidon-quickstart-se-2.jar
----

[source,bash]
.Run the curl command in a new terminal window and check the response (*notice the port is 8081*) :
----
curl 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.

[source,xml]
.Add the following dependency to `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>
----


[source,java]
.Replace the `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 io.opentracing.Span;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import javax.json.Json;
import javax.json.JsonBuilderFactory;
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.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) // <1>
        .put("/greeting", this::updateGreetingHandler);
  }

  private void getDefaultMessageHandler(ServerRequest request, ServerResponse response) {

    Span span =
        request
            .tracer()
            .buildSpan("getDefaultMessageHandler")
            .asChildOf(request.spanContext())
            .start();

    try {
      sendResponse(response, "World");
    } finally {
      span.finish();
    }
  }

  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();

    // <2>
    Span span =
        request
            .tracer()
            .buildSpan("outboundMessageHandler")
            .asChildOf(request.spanContext())
            .start();

    try {
      requestBuilder.property(
          ClientTracingFilter.CURRENT_SPAN_CONTEXT_PROPERTY_NAME, request.spanContext());  // <3>

      requestBuilder   // <4>
          .rx()
          .get(String.class)
          .thenAccept(response::send)
          .exceptionally(
              throwable -> {
                // process exception
                response.status(Http.Status.INTERNAL_SERVER_ERROR_500);
                response.send("Failed with: " + throwable);
                return null;
              });
    } finally {
      span.finish();   // <5>
    }
  }

  private void updateGreetingHandler(ServerRequest request, ServerResponse response) {
    request.content().as(JsonObject.class).thenAccept(jo -> updateGreetingFromJson(jo, response));
  }
}
----
<1> Add `outboundMessageHandler` to the routing rules.
<2> Create and start a span that is a child of the current span.
<3> Set a property with the `SpanContext`.
<4> Invoke the second service.
<5> Stop the span.

[source,bash]
.Build and run the application, then invoke the endpoint and check the response:
----
curl -i http://localhost:8080/greet/outbound // <1>
...
{
  "message": "Hello From SE-2 World!" // <2>
}
----
<1> The request went to the service on `8080`, which then invoked the service at `8081` to get the greeting.
<2> 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.

.Tracing multiple service list view
image::guides/12_tracing_se_top_2_services.png[Traces]

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

.Tracing across multiple services detail view
image::guides/12_tracing_se_detail_2_services.png[Traces]

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.

[source,bash]
.Replace the tracing configuration in `resources/application.yaml` with the following:
----
// <1>
tracing:
  service: helidon-se-1
  host: zipkin
----
<1> Helidon service `helidon-se-1` will connect to the Zipkin server at host name `zipkin`.

[source,bash]
.Stop the application and build the docker image for your application:
----
docker build -t helidon-tracing-se .
----

==== Deploy Zipkin into Kubernetes

[source,yaml]
.Create the Kubernetes YAML specification, named `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: 9411
----

[source,bash]
.Create the Zipkin pod and ClusterIP service:
----
kubectl apply -f ./zipkin.yaml
----

[source,bash]
.Create a Zipkin external server to view the UI and expose it on port 9142:
----
kubectl expose pod  zipkin --name=zipkin-external --port=9412 --target-port=9411 --type=LoadBalancer
----

Navigate 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

[source,yaml]
.Create the Kubernetes YAML specification, named `tracing.yaml`, with the following contents:
----
kind: Service
apiVersion: v1
metadata:
  name: helidon-tracing // <1>
  labels:
    app: helidon-tracing
spec:
  type: NodePort
  selector:
    app: helidon-tracing
  ports:
    - port: 8080
      targetPort: 8080
      name: http
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: helidon-tracing
spec:
  replicas: 1 // <2>
  template:
    metadata:
      labels:
        app: helidon-tracing
        version: v1
    spec:
      containers:
        - name: helidon-tracing
          image: helidon-tracing-se
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
----
<1> A service of type `NodePort` that serves the default routes on port `8080`.
<2> A deployment with one replica of a pod.

[source,bash]
.Create and deploy the application into Kubernetes:
----
kubectl apply -f ./tracing.yaml
----

==== Access your application and the Zipkin trace

[source,bash]
.Get the application service information:
----
kubectl get service/helidon-tracing
----

[source,bash]
----
NAME             TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
helidon-tracing   NodePort   10.99.159.2   <none>        8080:31143/TCP   8s // <1>
----
<1> A service of type `NodePort` that serves the default routes on port `31143`.

[source,bash]
.Verify the tracing endpoint using port `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.

[source,bash]
.Delete the Kubernetes resources:
----
kubectl delete -f ./zipkin.yaml
kubectl delete -f ./tracing.yaml
kubectl delete service zipkin-external
docker rm -f zipkin
----

== Summary

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:

- MicroProfile OpenTracing specification at https://github.com/eclipse/microprofile-opentracing/releases/tag/1.3
- Helidon Javadoc at https://helidon.io/docs/latest/apidocs/index.html?overview-summary.html




