Elastic APM Java for async Spring events

Hi all,

I hope someone can point me in the right direction here. I am using Spring Boot 3 in combination with Spring Modulith in my application. I get APM data in Kibana, so the configuration is fine. But, when I use an async event to communicate with the next module, APM does not collect data in the other modules.

Module 1 sending code (simplified):

@Service
public class Modul1Service {
private ApplicationEventPublisher events;

    @Scheduled(fixedRate = 5000)
    @Transactional
    public void poll() {
      events.publishEvent(new JobCreatedEvent());
   }
}

Module 2 receiving code (simplified):

@Service
public class Modul2Service {
    @CaptureSpan
    @ApplicationModuleListener
    void on(JobCreatedEvent event) {
      //process
   }
}

When enabling debug logs for the agent, I find the following log repeatedly:

Not creating span for Modul2Service#on because there is no currently active span.

As this is related to async processing, I checked your docs about asynchronous frameworks. As you support ExecutorService I tried to force Spring to use this next:

	@Bean
	@Primary
	ExecutorService asyncExecutor() {
		return Executors.newVirtualThreadPerTaskExecutor();
	}

	/**
	 * required to use the custom async executor with virtual threads
	 * 
	 * @return
	 */
        @Bean
	ApplicationEventMulticaster applicationEventMulticaster() {
		SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();

		eventMulticaster.setTaskExecutor(asyncExecutor());
		return eventMulticaster;
	}

Still, APM does not collect spans for the other modules. Any ideas how I can fix this?

Kibana version: 8.13.3

Elasticsearch version: 8.13

APM Server version: 8.13.3

APM Agent language and version: Java 1.49

I think the problem here is that ExecutorService.submit is likely not invoked synchronously when you call ApplicationEventPublisher.publish. Therefore the trace context is already lost before the built-in support of the elastic apm agent for executors can take effect.

One solution here would be to manually propagate the trace context, ideally via the OpenTelemtry-API:

When you call ApplicationEventPublisher.publishEvent, you'll need to somehow attach the current trace context to your event:

Then, in your @EventListener, you'll need to extract the stored context from the event and activate it (Context.makeCurrent()). When you now start a span while the context is activated, it will continue the previous trace.

You could make that more ergonomic using spring-aop if you want to.
If you don't want to use the OpentelemetryApi you could also use our public API (e.g. Span.current()), though OpenTelemetry is the more futue-proof way.

EDIT:
You could alternatively also use a custom ApplicationEventMulticaster implementation which is responsible for activating the trace context. I think that would be the cleanest and simplest solution instead of trying to enhance the @EventListener callsites.

1 Like

Hello Jonas,

Thank you so much! It works like a charm - it seems that I was thinking too complicated.

I am sure that OpenTelemetry is the future proof way, but I am still missing some functionality there (like getting the current root span - Elastic allows easy access to the Transaction.