Instrumenting with OpenCensus

In my last post, I talked about the OpenCensus Service. In this post, I would like to discuss how to instrument your application with the OpenCensus client libraries. Read on to learn more!

Before We Begin

Before you start adding instrumentation you should answer some questions about your library and application. For this post, I will cover an example in Java (the answers I will be using are in bold below).

  • Which context propagation format do you use (B3, W3C, other)?
  • How do you add dependencies today (Bazle, Gradle, Maven)?
  • What framework and what versions (DropWizard, Spring Boot, Spring Sleuth)?
  • What RPCs do you use (REST, gRPC, Thrift, etc)?
  • Do you run HTTP on Java and if so which ones (Jetty, Servlet, Tomcat)?
  • Do you make DB calls and if so which DBs (Cassandra, MongoDB, MySQL)

Goal: Create a span (either root or child) for a given service

Java Application

Here is my example Java application:

public final class MyClassWithTracing {

    public static void main(String[] args) {    
        doWork();
        doMoreWork();
    }

    private static void doWork() {
        // do work
    }

    private static void doMoreWork() {
        doSomeOtherWork();
    }

    private static void doSomeOtherWork() {
        doSomeOtherWork();
    }

}

Creating a Root Span

Here is what my code looks like after I add instrumentation to create a root span:

import io.opencensus.common.Span;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.config.TraceConfig;
import io.opencensus.trace.samplers.Samplers;
import io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter;

public final class MyClassWithTracing {
    private static final Tracer tracer = Tracing.getTracer();

    public static void main(String[] args) {
        // TODO: update endpoint and service name
        ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");

        TraceConfig traceConfig = Tracing.getTraceConfig();
        traceConfig.updateActiveTraceParams(traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
        
        doWork();
        doMoreWork();

        // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
        sleep(5100);
    }

    private static void doWork() {
        // Create root span
        Span rootSpan = tracer.spanBuilder("MyRootSpan").startSpan();
        // do work
        rootSpan.end();
    }

    private static void doMoreWork() {
        doSomeOtherWork();
    }

    private static void doSomeOtherWork() {
        // do some other work
    }

}

OK, let’s walk through these changes. First, we import what we need for OpenCensus:

import io.opencensus.common.Span;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.config.TraceConfig;
import io.opencensus.trace.samplers.Samplers;
import io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter;

Then we instantiate our tracer:

private static final Tracer tracer = Tracing.getTracer();

Then we register our exporter:

// TODO: update endpoint and service name
ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");

Then we adjust our sampling policy. By default, OpenCensus samples 1:10,000 spans. We will change this to sample everything:

TraceConfig traceConfig = Tracing.getTraceConfig();
traceConfig.updateActiveTraceParams(traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());

Then we will add a sleep. By default, OpenCensus exports traces every five seconds, so we need to be sure to wait at least that long:

// Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
sleep(5100);

With OpenCensus configured, we can now create our root span:

// Create root span
Span rootSpan = tracer.spanBuilder("MyRootSpan").startSpan();
// do work
rootSpan.end();

Creating A Child Span Of A Function Call

Next, let’s add child spans of function calls:

import io.opencensus.common.Scope;
import io.opencensus.common.Span;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.config.TraceConfig;
import io.opencensus.trace.samplers.Samplers;
import io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter;

public final class MyClassWithTracing {
    private static final Tracer tracer = Tracing.getTracer();

    public static void main(String[] args) {
        // TODO: update endpoint and service name
        ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");

        TraceConfig traceConfig = Tracing.getTraceConfig();
        traceConfig.updateActiveTraceParams(traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
        
        doWork();
        doMoreWork();

        // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
        sleep(5100);
    }

    private static void doWork() {
        // Create root span
        Span rootSpan = tracer.spanBuilder("MyRootSpan").startSpan();
        // do work
        rootSpan.end();
    }

    private static void doMoreWork() {
        // Option 1
        Span childSpan = tracer.spanBuilder("MyChildSpan").startSpan();
        doSomeOtherWork();
        childSpan.end();
    }

    private static void doSomeOtherWork() {
        // Option 2 - requires additional import above
        try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
            // do some other work
        }
    }

}

There are actually a couple ways to create spans so for this example I demonstrate the two most common. The first one is the same as the root, but this time creates a child span:

// Option 1
Span childSpan = tracer.spanBuilder("MyChildSpan").startSpan();
doSomeOtherWork();
childSpan.end();

The second option makes use of a scoped span and requires an additional import:

import io.opencensus.common.Scope;

Now we can wrap our calls instead of explicitly ending them. This works whether the wrapped calls are successful or not.

// Option 2 - requires additional import above
try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
    // do some other work
}

Creating Child Spans in Spring

If you are using Spring then adding spans is even easier. We start by adding the beans:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:aspectj-autoproxy/>

  <!-- global tracer -->
  <bean id="tracer" class="io.opencensus.trace.Tracing" factory-method="getTracer"/>

  <!-- traces explicit calls to Traced -->
  <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
    <constructor-arg ref="tracer"/>
  </bean>

  <!-- traces all SQL calls -->
  <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
    <constructor-arg ref="tracer"/>
  </bean>
</beans>

Then to create spans for functions we can do:

import io.opencensus.common.Span;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.config.TraceConfig;
import io.opencensus.trace.samplers.Samplers;
import io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter;

public final class MyClassWithTracing {
    private static final Tracer tracer = Tracing.getTracer();

    public static void main(String[] args) {
        // TODO: update endpoint and service name
        ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");

        TraceConfig traceConfig = Tracing.getTraceConfig();
        traceConfig.updateActiveTraceParams(traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
        
        doWork();
        doMoreWork();

        // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
        sleep(5100);
    }

    private static void doWork() {
        // Create root span
        Span rootSpan = tracer.spanBuilder("MyRootSpan").startSpan();
        // do work
        rootSpan.end();
    }

    @Traced()
    private static void doMoreWork() {
        doSomeOtherWork();
    }

    @Traced()
    private static void doSomeOtherWork() {
        // do some other work
    }

}

As you can see, adding spans is as easy as starting each function with:

@Traced()

Adding Span Metadata

Once you have spans for service-to-service communication calls you will next want to add metadata to enrich your spans. OpenCensus provides two facilities for this:

  • Attributes: key/value pairs
  • Annotations: strings (e.g. log message)

Let’s enrich our example application:

import io.opencensus.common.Scope;
import io.opencensus.common.Span;
import io.opencensus.trace.AttributeValue;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.config.TraceConfig;
import io.opencensus.trace.samplers.Samplers;
import io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter;

public final class MyClassWithTracing {
    private static final Tracer tracer = Tracing.getTracer();

    public static void main(String[] args) {
        // TODO: update endpoint and service name
        ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");

        TraceConfig traceConfig = Tracing.getTraceConfig();
        traceConfig.updateActiveTraceParams(traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
        
        doWork();
        doMoreWork();

        // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
        sleep(5100);
    }

    private static void doWork() {
        // Create root span
        Span rootSpan = tracer.spanBuilder("MyRootSpan").startSpan();
        // do work
        rootSpan.end();
    }

    private static void doMoreWork() {
        // Option 1
        Span childSpan = tracer.spanBuilder("MyChildSpan").startSpan();
        tracer.getCurrentSpan().putAttributes("Host.name", AttributeValue.stringAttributeValue(ip.getHostName()));
        doSomeOtherWork();
        childSpan.end();
    }

    private static void doSomeOtherWork() {
        // Option 2 - requires additional import above
        try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
            // do some other work
            tracer.getCurrentSpan().addAnnotation("Important message or log.");
        }
    }

}

OK, so what changed? This time, we added a host.name key on the child span of the doMoreWork function call. To do this, we added the following import:

import io.opencensus.trace.AttributeValue;

Then we added the attribute as follows:

tracer.getCurrentSpan().putAttributes("Host.name", AttributeValue.stringAttributeValue(ip.getHostName()));

In addition, we added a string to the child span of the doSomeOtherWork function call:

tracer.getCurrentSpan().addAnnotation("Important message or log.");

Summary

As you can see, it is straightforward to get started with the OpenCensus Client Libraries. You provide some initial configuration based on your requirements, including context propagation format, exporter, and sampling policy. Then, you create root and child spans. Finally, you enrich spans with metadata. This basic flow is the same no matter what language you are instrumenting.

© 2019 – 2021, Steve Flanders. All rights reserved.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top