Let’s start by defining a JAX-RS endpoint that also acts as a stateless EJB. This is a common trick
to get JAX-RS endpoints to be managed as EJBs, so that they can be invoked via JMX or get
monitoring features. Or, in our case, to get traced via EJB interceptors.
This endpoint is where we get our HTTP requests from and where our transaction starts, from
the tracing perspective. Once we receive an HTTP request, we call the
AccountService#sendNotification
method and then the OrderService#processOrderPlacement
.
Note that we annotate the class with @Interceptors(OpenTracingInterceptor.class)
, which means
that all methods on this class are to be traced.
src/main/java/io/opentracing/contrib/ejb/demoexample/Endpoint.java
:
package io.opentracing.contrib.ejb.demoexample;
import io.opentracing.contrib.ejb.OpenTracingInterceptor;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import java.util.logging.Logger;
/**
* This is a regular JAX-RS endpoint with EJB capabilities. We use the EJB capability to specify an interceptor,
* so that every method on this class is wrapped on its own span. If the OpenTracing JAX-RS integration is being used,
* it would be a good idea to not have the interceptor at this level, to avoid having too much "noise".
*
* @author Juraci Paixão Kröhling
*/
@Path("/order")
@Stateless
@Interceptors(OpenTracingInterceptor.class)
public class Endpoint {
private static final Logger log = Logger.getLogger(Endpoint.class.getName());
@Inject
AccountService accountService;
@Inject
OrderService orderService;
@POST
@Path("/")
public String placeOrder() {
log.info("Request received to place an order");
accountService.sendNotification();
orderService.processOrderPlacement();
return "Order placed";
}
}
Our AccountService
is a simple stateless EJB, responsible for sending a notification about the new order
to the owner of the account. Here, we could call another service, or send an email, SMS or any other form
of message.
As this is a regular EJB, we are able to automatically join the span context from the JAX-RS endpoint, making
this call a child span of the main transaction. This is all transparent to you as developer.
Note again that we annotate the bean with @Interceptors(OpenTracingInterceptor.class)
. As our interceptor
is just like any other EJB interceptor, you could use a ejb-jar.xml
to automatically use this inteceptor on
all available beans. Whether or not to trace all beans is a per-deployment decision, so, no ejb-jar.xml
is
provided by the integration.
src/main/java/io/opentracing/contrib/ejb/demoexample/AccountService.java
:
package io.opentracing.contrib.ejb.demoexample;
import io.opentracing.contrib.ejb.OpenTracingInterceptor;
import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import java.util.logging.Logger;
/**
* This is a simple synchronous EJB, without any knowledge about span context or other OpenTracing semantics. All it
* does is specify an interceptor and it's shown as the child of a parent span.
*
* @author Juraci Paixão Kröhling
*/
@Stateless
@Interceptors(OpenTracingInterceptor.class)
public class AccountService {
private static final Logger log = Logger.getLogger(AccountService.class.getName());
public void sendNotification() {
log.info("Notifying the account owner about a new order");
}
}
Our OrderService
is responsible for actually placing the order: it’s where the business knowledge
resides. We’ll later look into details at the InventoryService
, but for now, we need to know that
this service requires a SpanContext
to be explicitly passed. We can get this context from the EJBContext
,
stored under a context data entry that can be retrieved with the constant
io.opentracing.contrib.ejb.OpenTracingInterceptor.SPAN_CONTEXT
.
src/main/java/io/opentracing/contrib/ejb/demoexample/OrderService.java
:
package io.opentracing.contrib.ejb.demoexample;
import io.opentracing.SpanContext;
import io.opentracing.contrib.ejb.OpenTracingInterceptor;
import javax.annotation.Resource;
import javax.ejb.EJBContext;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import java.util.logging.Logger;
import static io.opentracing.contrib.ejb.OpenTracingInterceptor.SPAN_CONTEXT;
/**
* This is a regular synchronous stateless EJB. It demonstrates how to get the span context for the span it's wrapped
* on. This can be used to pass down the call chain, create child spans or add baggage items.
*
* @author Juraci Paixão Kröhling
*/
@Stateless
@Interceptors(OpenTracingInterceptor.class)
public class OrderService {
private static final Logger log = Logger.getLogger(OrderService.class.getName());
@Resource
EJBContext ctx;
@Inject
InventoryService inventoryService;
public void processOrderPlacement() {
log.info("Placing order");
Object ctxParentSpan = ctx.getContextData().get(SPAN_CONTEXT);
if (ctxParentSpan instanceof SpanContext) {
inventoryService.changeInventory((SpanContext) ctxParentSpan);
return;
}
inventoryService.changeInventory(null);
}
}
Our InventoryService
is responsible for interfacing with backend systems dealing with inventory control.
We don’t want to block the parent transaction while interacting with those systems, so, we make this an
asynchronous EJB. When dealing with asynchronous objects, it’s a good idea to be explicit about the span
context, as there are potential concurrency issues when sharing a context between a synchronous and an
asynchronous bean.
The OpenTracing EJB integration is able to intercept the method call and detect if there is a span context
among the parameters, which is the case of the changeInventory(SpanContext)
method. In this situation,
the following happens behind the scenes:
-
The caller makes a method call, passing the SpanContext
-
The interceptor is activated, creating a new child span using the SpanContext
as the parent
-
The interceptor replaces the original SpanContext
with this new child span on the method call
-
The intercepted method is finally invoked, wrapped by the new child span.
Note that the SpanContext
passed by the OrderService
is not the same as the one received by InventoryService
.
While this might cause some confusion, we believe this is the right semantic for this use case, as it allows
for a complete tracing picture, without any explicit tracing code, apart from passing the context around.
src/main/java/io/opentracing/contrib/ejb/demoexample/InventoryService.java
package io.opentracing.contrib.ejb.demoexample;
import io.opentracing.SpanContext;
import io.opentracing.contrib.ejb.OpenTracingInterceptor;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import java.util.logging.Logger;
/**
* This is an asynchronous stateless EJB with spans created automatically by the interceptor. Note that the span context
* that this method sees is <b>not</b> the same as the span context sent by the caller: the interceptor wraps this
* method call on its own span, and replaces the span context by the context of this new span. This is done so that this
* span context can be passed along to the next service "as is".
*
* @author Juraci Paixão Kröhling
*/
@Asynchronous
@Stateless
@Interceptors({OpenTracingInterceptor.class})
public class InventoryService {
private static final Logger log = Logger.getLogger(InventoryService.class.getName());
@Inject
InventoryNotificationService inventoryNotificationService;
public void changeInventory(SpanContext context) {
log.info("Changing the inventory");
inventoryNotificationService.sendNotification(context);
}
}
And finally, our last service, InventoryNotificationService
: in this case, we notify another set of backend systems
that a new order has been placed. Again, this is an asynchronous EJB and works like the one above, but additionally,
we wanted to manually create a "business span", called sendNotification
. This method could send several notifications,
wrapping each one into a span of its own. As we manually started it, we manually finish it as well.
src/main/java/io/opentracing/contrib/ejb/demoexample/InventoryNotificationService.java
package io.opentracing.contrib.ejb.demoexample;
import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.util.GlobalTracer;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import java.util.logging.Logger;
/**
* This is the final call in the chain. This is an asynchronous stateless EJB, which obtains the span context
* via a method parameter. This bean is not intercepted in any way by us, so, the span context received is exactly
* the same as what was sent by the caller.
*
* @author Juraci Paixão Kröhling
*/
@Stateless
@Asynchronous
public class InventoryNotificationService {
private static final Logger log = Logger.getLogger(InventoryNotificationService.class.getName());
public void sendNotification(SpanContext context) {
Span span = GlobalTracer.get().buildSpan("sendNotification").asChildOf(context).startManual();
log.info("Sending an inventory change notification");
span.finish();
}
}
Now, let’s do a final sanity check and see if everything is in the right place:
mvn wildfly-swarm:run
. As before, the final message should be WildFly Swarm is Ready
. Hit Ctrl+C
and let’s
setup our tracing backend.