In this blog series, we are covering application instrumentation steps for distributed tracing with OpenTelemetry standards across multiple languages. Earlier, we covered Golang Application Instrumentation for Distributed Traces and DotNet Application Instrumentation for Distributed Traces. Here we are going to cover the instrumentation for Java.
Initialize New Project
To begin, create a new Java project and add the below dependencies that are required for OpenTelemetry manual instrumentation.
Maven
<project>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>1.5.0-alpha</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.39.0</version>
</dependency>
</dependencies>
</project>
Gradle
dependencies {
implementation platform("io.opentelemetry:opentelemetry-bom:1.2.0")
implementation('io.opentelemetry:opentelemetry-api')
implementation('io.opentelemetry:opentelemetry-sdk')
implementation('io.opentelemetry:opentelemetry-exporter-otlp')
implementation('io.opentelemetry:opentelemetry-semconv:1.5.0-alpha')
implementation('io.grpc:grpc-netty-shaded:1.39.0')
}
It is recommended to use OpenTelemetry BOM to keep the version of the various components in sync.
If you are developing a library that is going to be used by some other final application, then your code will have dependency only on opentelemetry-api.
Create Resource Detectors
The resource describes the object that generated the Telemetry signals. Essentially, it must be the name of the service or application. OpenTelemetry has defined the standards to describe the service execution env, viz. hostname, hostType (cloud, container, serverless), namespace, cloud-resource-id, etc. These attributes are defined under Resource Semantic Conventions or semconv.
Here we will be creating a resource with some environmental attributes.
Attribute | Description | Required |
service.name | It is the logical name of the service. | Yes |
service.namespace | It is used to group the services.For example, you can use service.namespace to distinguish services across environments like QA,UAT,PROD. | No |
host.name | Name of the host where the service is running. | No |
//Create Resource
AttributesBuilder attrBuilders = Attributes.builder()
.put(ResourceAttributes.SERVICE_NAME, SERVICE_NAME)
.put(ResourceAttributes.SERVICE_NAMESPACE, "US-West-1")
.put(ResourceAttributes.HOST_NAME, "prodsvc.us-west-1.example.com");
Resource serviceResource = Resource
.create(attrBuilders.build());
Init Span Exporter
The exporter is the component in SDK responsible for exporting the Telemetry signal (trace) out of the application to a remote backend, log to a file, stream to stdout., etc.
In this example, we are creating a gRPC exporter to send out traces to an OTLP receiver backend running on localhost:55680. Possibly an OTEL Collector.
//Create Span Exporter
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:55680")
.build();
Construct TracerProvider and Configure SDK
Using TracerProvider you can access Tracer, which is used to create spans.
//Create SdkTracerProvider
SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter)
.setScheduleDelay(100, TimeUnit.MILLISECONDS).build())
.setResource(serviceResource)
.build();
//This Instance can be used to get tracer if it is not configured as global
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.buildAndRegisterGlobal();
You need to configure the SDK and create the tracer as a first step in your application.
Create Tracer
Tracer tracer= GlobalOpenTelemetry.getTracer("auth-Service-instrumentation");
//Tracer tracer= GlobalOpenTelemetry.getTracer("auth-Service-instrumentation","1.0.0");
//OR use the OpenTelemetry instance from previous step to get tracer
//openTelemetry.getTracer("auth-Service-instrumentation");
You can use GlobalOpenTelemetry only If your OpenTelemery instance is registered as global in the previous step or else you can use the OpenTelemetry instance returned by SDK builder.
The getTracer method requires an instrumentation library name as a parameter, which must not be null.
Create a Span and Define Span Attributes
The span is a single execution of an operation. It is identified by a set of attributes, which are sometimes referred to as span tags. Application owners are free to choose the attributes which can capture required information for the spans. There is no limit to the number of span attributes per span.
In this example, we are defining two-span attributes for our sample applications.
Span parentSpan = tracer.spanBuilder("doLogin").startSpan();
parentSpan.setAttribute("priority", "business.priority");
parentSpan.setAttribute("prodEnv", true);
Create a Child Span
You can use the setParent method to correlate spans manually.
Span childSpan = tracer.spanBuilder("child")
.setParent(Context.current().with(parentSpan))
.startSpan();
The OpenTelemetry API also offers an automated way to propagate the parent span on the current thread.
Use the makeCurrent method to automatically propagate the parent span on the current thread.
try (Scope scope = parentSpan.makeCurrent()) {
Thread.sleep(200);
boolean isValid=isValidAuth(username,password);
//Do login
} catch (Throwable t) {
parentSpan.setStatus(StatusCode.ERROR, "Change it to your error message");
} finally {
parentSpan
.end(); // closing the scope does not end the span, this has to be done manually
}
//Child Method
private boolean isValidAuth(String username,String password){
Span childSpan = tracer.spanBuilder("isValidAuth").startSpan();
// NOTE: setParent(...) is not required;
// `Span.current()` is automatically added as the parent
childSpan.setAttribute("Username", username)
.setAttribute("id", 101);
//Auth code goes here
try {
Thread.sleep(200);
childSpan.setStatus(StatusCode.OK);
} catch (InterruptedException e) {
childSpan.setStatus(StatusCode.ERROR, "Change it to your error message");
}finally {
childSpan.end();
}
return true;
}
Add Events/Logs to Spans
Spans can be enriched with some execution logs/events that happened during the execution of the span. This information will help provide contextual logs always tied up with the respective span.
Attributes eventAttributes = Attributes.builder().put("Username", username)
.put("id", 101).build();
childSpan.addEvent("User Logged In", eventAttributes);
Putting It Together
TestApplication.java
package com.logicmonitor.example;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.util.concurrent.TimeUnit;
public class TestApplication {
private static final String SERVICE_NAME = "Authentication-Service";
static {
//Create Resource
AttributesBuilder attrBuilders = Attributes.builder()
.put(ResourceAttributes.SERVICE_NAME, SERVICE_NAME)
.put(ResourceAttributes.SERVICE_NAMESPACE, "US-West-1")
.put(ResourceAttributes.HOST_NAME, "prodsvc.us-west-1.example.com");
Resource serviceResource = Resource
.create(attrBuilders.build());
//Create Span Exporter
OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:55680")
.build();
//Create SdkTracerProvider
SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter)
.setScheduleDelay(100, TimeUnit.MILLISECONDS).build())
.setResource(serviceResource)
.build();
//This Instance can be used to get tracer if it is not configured as global
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.buildAndRegisterGlobal();
}
public static void main(String[] args) throws InterruptedException {
Auth auth = new Auth();
auth.doLogin("testUserName", "testPassword");
Thread.sleep(1000);
}
}
Auth.Java
package com.logicmonitor.example;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
public class Auth {
Tracer tracer = GlobalOpenTelemetry.getTracer("auth-Service-instrumentation");
//Tracer tracer= GlobalOpenTelemetry.getTracer("auth-Service-instrumentation","1.0.0");
public void doLogin(String username, String password) {
Span parentSpan = tracer.spanBuilder("doLogin").startSpan();
parentSpan.setAttribute("priority", "business.priority");
parentSpan.setAttribute("prodEnv", true);
try (Scope scope = parentSpan.makeCurrent()) {
Thread.sleep(200);
boolean isValid = isValidAuth(username, password);
//Do login
} catch (Throwable t) {
parentSpan.setStatus(StatusCode.ERROR, "Change it to your error message");
} finally {
parentSpan
.end(); // closing the scope does not end the span, this has to be done manually
}
}
private boolean isValidAuth(String username, String password) {
Span childSpan = tracer.spanBuilder("isValidAuth").startSpan();
// NOTE: setParent(...) is not required;
// `Span.current()` is automatically added as the parent
//Auth code goes here
try {
Thread.sleep(200);
childSpan.setStatus(StatusCode.OK);
Attributes eventAttributes = Attributes.builder().put("Username", username)
.put("id", 101).build();
childSpan.addEvent("User Logged In", eventAttributes);
} catch (InterruptedException e) {
childSpan.setStatus(StatusCode.ERROR, "Change it to your error message");
} finally {
childSpan.end();
}
return true;
}
}
Run the Application
Run TestApplication.java.
Traces Received in the LogicMonitor Platform
Detailed View of the Trace
Parent Span:
Child Span:
Conclusion
Congratulations, you have just written a Java application emitting traces using the OpenTelemetry Protocol (OTLP) Specification. Feel free to use this code as a reference when you get started with instrumenting your business application with OTLP specifications. LogicMonitor APM specification is 100% OTLP compliant with no vendor lock-in. To receive and visualize traces of multiple services for troubleshooting with the LogicMonitor platform, sign up for a free trial account here. Check back for more blogs covering application instrumentation steps for distributed tracing with OpenTelemetry standards across multiple languages.
Subscribe to our blog
Get articles like this delivered straight to your inbox