service mesh

Red Hat OpenShift Service Mesh brings tracing and monitoring to your system with little effort. However, the information is basic. In this article, we will create three applications in the Quarkus, Node.Js, and Java languages to collect more detailed statistics with Jaeger, collect custom application specific metrics with Prometheus, and create new dashboards with Grafana.

Jaeger

Jaeger, installed by default in OpenShift Service Mesh, allows developers to configure their services to gather runtime statistics about their performance.

It is important to understand the following terms:

  • Span: Represents a logical unit of work, which has a unique name, a start time, and the duration of execution.
  • Trace: This is an execution path of services in OpenShift Service Mesh. In other words, a trace is comprised of one or more spans (Figure 1).
Spans Traces
Figure 1: Architecture

Enabling tracing in Quarkus applications

To enable custom metrics for a Quarkus application, add the quarkus-smallrye-opentracing extension in the pom.xml file.

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-opentracing</artifactId>
</dependency>

To enable custom metrics for a Quarkus application, add the related properties in the application.properties file.

opentracing.jaeger.service-name=app_name
opentracing.jaeger.const-sampler.decision=true
opentracing.jaeger.sampler.param=1
opentracing.jaeger.log-spans=false
opentracing.jaeger.http-sender.url=http://jaeger-collector.<namespace>.svc.cluster.local:14268/api/traces
opentracing.jaeger.enable-b3-propagation=true

Enabling tracing in Node.js applications

To enable custom metrics for a Node.js application, add the jaeger-client and opentracing package.

npm install --save jaeger-client opentracing

This package has all the standard Jaeger metric types that you can use to instrument your custom tracing.

const Jaeger = require("jaeger-client");
const Opentracing = require("opentracing");
const logger = Pino();
module.exports = { create };

Configure and initialize the Jaeger tracer.

function create() {
  const config = {
    serviceName: "app_name",
    sampler: { type: "const", param: 1 },
    reporter: { logSpans: true, collectorEndpoint: "http://jaeger-collector.<namespace>.svc.cluster.local:14268/api/traces",
    }
  };
  const options = { logger };
  const tracer = Jaeger.initTracer(config, options);
  const codec = new Jaeger.ZipkinB3TextMapCodec({ urlEncoding: true });
  tracer.registerInjector(Opentracing.FORMAT_HTTP_HEADERS, codec);
  tracer.registerExtractor(Opentracing.FORMAT_HTTP_HEADERS, codec);
  return tracer;
}

This package has all the standard Jaeger metric types that you can use to instrument your custom tracing.

const Fastify = require("fastify");
const Opentracing = require("opentracing");
const logger = Pino();
module.exports = { create };

Start a new span.

function create(tracer) {
  const server = Fastify({ logger });
  server.addHook("onRequest", traceRequest);
  server.addHook("onResponse", traceResponse);
  return server;


  async function traceRequest(request) {
    const { method, originalUrl } = request.raw;
    const name = method + ":app_name";
    const span = tracer.startSpan(name);
    span.setTag(Opentracing.Tags.HTTP_URL, originalUrl);
    span.setTag(Opentracing.Tags.HTTP_METHOD, method);
    request.rootSpan = span;
  }
  async function traceResponse(request, reply) {
    request.rootSpan.setTag(
      Opentracing.Tags.HTTP_STATUS_CODE,
      reply.res.statusCode
    );
    request.rootSpan.finish();
  }
}

Enabling tracing in Java applications

To enable custom metrics for a Java application, add the opentracing-spring-jaeger-cloud-starter extension in the pom.xml file.

<dependency>
  <groupId>io.opentracing.contrib</groupId>
  <artifactId>opentracing-spring-jaeger-cloud-starter</artifactId>
</dependency>

To enable custom metrics for a Java application, add the related properties in the application.properties file.

opentracing.jaeger.service-name=app_name
opentracing.jaeger.const-sampler.decision=true
opentracing.jaeger.sampler.param=1.0
opentracing.jaeger.log-spans=false
opentracing.jaeger.http-sender.url=http://jaeger-collector.<namespace>.svc.cluster.local:14268/api/traces
opentracing.jaeger.enable-b3-propagation=true

Visualizing traces and spans using the Jaeger web console

In the first console, you can see only the service and the route that was consumed, as shown in Figure 2.

istio_1
Figure 2: Default tracing

In the last console, the method that was consumed and the logs are shown in Figure 3.

istio_2
Figure 3: Trace using the library

Prometheus and Grafana

Prometheus is an open source systems monitoring and alerting toolkit which includes a time series database for storing metrics. OpenShift Service Mesh provides a default Prometheus instance which gathers metrics data from the Envoy proxies, the services in the service mesh, and the components in the control plane.

Grafana is an open source graphical visualization tool used for creating operational dashboards. OpenShift Service Mesh provides a default Grafana instance with ready made dashboards for viewing data from the Envoy proxies, the services in the service mesh, and the components in the control plane.

Collecting custom application metrics

OpenShift Service Mesh collects several useful metrics high level understanding at the performance of the service mesh and the applications deployed on it. However, sometimes you might need to gather custom application specific metrics and display them in a Grafana dashboard. You must include the Prometheus client libraries in your application, and then create these custom metrics.

Prometheus supports four types of metrics:

  • Counter: A cumulative metric whose value can only increase or reset to zero on restart. For example, you can use a counter to represent the number of requests served, errors, and tasks completed.
  • Gauge: A metric that can be incremented or decremented. For example, you can use a gauge to represent the number of processes or the number of concurrent users.
  • Histogram: Track the number and the sum of the values, allowing you to calculate the average of the values.
  • Summary: Similar to a histogram, but a summary also calculates configurable values over a sliding time window.

Creating custom metrics for Quarkus applications

To enable custom metrics for a Quarkus application, add the quarkus-smallrye-metrics extension in the pom.xml file.

<dependency>
  <groupId>io.quarkus</groupId>
   <artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>

Import the required metrics classes in your application from the org.eclipse.microprofile.metrics.* package.

This package has all the standard Prometheus metric types that you can use to instrument your custom metrics.

import org.eclipse.microprofile.metrics.MetricUnits;
import org.eclipse.microprofile.metrics.annotation.Counted;
import org.eclipse.microprofile.metrics.annotation.Gauge;
import org.eclipse.microprofile.metrics.annotation.SimplyTimed;
import org.eclipse.microprofile.metrics.annotation.Metered;

Timed can be created by adding the @SimplyTimed annotation to methods:

@SimplyTimed(
  name = "app_name:request_process_time",
  description = "A measure of how long it takes to process a request",
  unit = MetricUnits.MILLISECONDS
)

Counter can be created by adding the @Counted annotation to methods:

@Counted(
  name = "app_name:request_placed",
  description = "Count of request placed"
)

Metered can be created by adding the @Metered annotation to methods:

@Metered(
  name = "app_name:request_processed_rate",
  description = "Rate at which requests are placed",
  unit = MetricUnits.MINUTES, 
  absolute = true
)

Gauge can be created by adding the @Gauge annotation to methods:

@Gauge(
  name = "app_name:order_process_rating",
  description = "Overall customer rating for the order process",
  unit = MetricUnits.NONE 
)

Creating custom metrics for Node.js applications

To enable custom metrics for a Node.js application, add the prom-client package.

npm install --save prom-client

Import the prometheus-client package and initialize it. Then add a unique prefix to identify the standard metrics.

const express = require('express');
var prometheus = require('prom-client');
const prefix = 'app_name_';
prometheus.collectDefaultMetrics({ prefix });

Counter can be created by adding the new prometheus.Counter annotation to methods:

const requestPlaced = new prometheus.Counter({
  name: app_name:request_placed,
  help: 'Count of request placed'
});

Gauge can be created by adding the new prometheus.Gauge annotation to methods:

const processRating = new prometheus.Gauge({
  name: app_name:order_process_rating,
  help: 'Overall customer rating for the order process'
});

Update the method to add the new constant variables.

app.get('/method-name, async function (req, res) {  
  processRating.inc(); //You can then increment the counter
  requestPlaced.setToCurrentTime();   //You can know the overall rating
  const end = requestPlaced.startTimer();
  end();
  res.send('OK');
});

Add code to create a new HTTP GET endpoint called /metrics which will be used by Prometheus to collect metrics for this microservice.

app.get('/metrics', function (req, res) {
  res.set('Content-Type', prometheus.register.contentType);
  res.send(prometheus.register.metrics());
});

Creating custom metrics for Java applications

To enable custom metrics for a Java application,  add the micrometer-registry-prometheus extension in the pom.xml file.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
  <scope>runtime</scope>
</dependency>

To enable custom metrics for a Java application, add the related properties in the application.properties file.

management.endpoints.enabled-by-default=true
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details: always

Import the required metrics classes in your application from the io.micrometer.core.* package.

This package has all the standard Prometheus metric types that you can use to instrument your custom metrics.

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.annotation.Timed;

Gauge can be created by adding the Gauge.builder method to constructor:

public class OrderController {
  private List<Order> orderList;
  public Supplier<Number> orderCount() { return ()->orderList.size(); }
  public OrderController(MeterRegistry registry) {
    Gauge.builder("app_name:order_process_rating", orderCount()).     
      description("Overall customer rating for the order process").
      register(registry);
  }
}

Timed can be created by adding the @Timed annotation to methods:

@Timed(
  value = "app_name:request_process_time",
  description = "A measure of how long it takes to process a request",
  percentiles = {0.5,0.9}
)

Enabling Prometheus metrics

OpenShift Service Mesh does not collect metrics from applications by default. To enable Prometheus to collect metrics from your application, add the following annotations:

spec:
 template:
   metadata:
     annotations:
       sidecar.istio.io/inject: "true"
       prometheus.io/scrape: "true"
       prometheus.io/port: "8080"
       prometheus.io/scheme: "http"

Querying OpenShift Service Mesh metrics using Prometheus

Figure 4 illustrates the newly created metric app_name:request_placed in Prometheus.

prometheus_1
Figure 4: First custom metric in Prometheus

Figure 5 shows the newly created metric app_name:order_process_rating in Prometheus.

prometheus_2
Figure 5: Second custom metric in Prometheus

Visualizing OpenShift Service Mesh metrics using Grafana

Figure 6 illustrates the basic metrics in Grafana.

graphana_1
Figure 6: Metrics in Grafana by default

Create a new dashboard to add the newly created metric app_name:request_placed in Grafana (Figure 7).

graphana_2
Figure 7: First custom metric in Grafana

Create a new dashboard to add the newly created metric app_name:order_process_rating in Grafana (Figure 8).

graphana_3
Figure 8: Second custom metric in Grafana

You can view the new dashboard created with the two metrics created (Figure 9).

graphana_4
Figure 9: New dashboard with custom metrics

Summary

Tracing helps you identify performance issues in microservices based applications. That's why you should add your applications Jaeger and OpenTracing libraries to enable tracing. Also, you can use Prometheus libraries to generate custom application metrics and Grafana to generate custom dashboards for your application metrics.