Spring Sleuth was removed in Spring Boot version 3 in favor of Micrometer and I recently migrated many services and want to share a bit of the learning and things that we need to change to make everything work with Micrometer as they were working before.
We were using mostly automatic instrumentation and in some places, we were using @NewSpan
or even injecting the Tracer and starting a scoped span to track some specific operations. We had distributed tracing in HTTP requests, RabbitMQ messages, Scheduled Jobs, and Async executions.
Under the hood, our Spring Sleuth was using Brave as the implementation and we were exporting the context in the Zipkin B3 format with the multiple headers options (X-B3-TraceId
, X-B3-ParentSpanId
, and X-B3-SpanId
).
I am not going to go over all the things that require micrometer work from scratch, as there are already very good guides on how to do it, I will mostly focus on what we need to change from Sleuth to Micrometer.
Let’s start with the dependencies.
Dependencies
After upgrading the Spring Boot version to something like 3.2.4 (the latest when this article was written), we have to remove the sleuth dependencies, in our case we had two:
implementation "org.springframework.cloud:spring-cloud-sleuth-zipkin"
implementation "org.springframework.cloud:spring-cloud-starter-sleuth"
And we need to add the new ones.
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
These are the basic ones, which should allow you to trace and export to Zipkin.
HTTP request
By default, Micrometer uses W3C propagation type. In the Migration Page they recommend setting Sleuth & Boot 2.x application
spring.sleuth.propagation.type=w3c,b3
so it exports both and you can use the default in the newly migrated application. But we cannot do this as we don’t only have Spring Services, we also have Node.JS which receives expects the B3 format for now.
To continue being compatible with the whole system, we just changed the default propagation type in Micrometer to b3_multi
by adding the following property.
management.tracing.propagation.type=b3_multi
Sampling
Learned this the really hard way (by having it in production for a few days) but by default, (not sure why they thought that it was a good idea), but it only exports 10% of the traces (source) and you have to change a property to send all.
management.tracing.sampling.probability=1
I was really pissed about this, it should always export everything and IF YOU WANT to change, you can, but not the other way around. Also, the naming is very confusing as setting it to 100% probability of sampling, means that it sends all and not discard all.
@NewSpan
To enable support for the “old” annotations like @NewSpan
, we needed to add the following property (source)
management.observations.annotations.enabled=true
You also have to make sure that you have Spring AOP in the classpath, as it is required by what I understood of the documentation.
implementation "org.springframework.boot:spring-boot-starter-aop"
RabbitMQ
For RabbitMQ, we were injecting
private final SpringRabbitTracing springRabbitTracing;
and setting the decorateSimpleRabbitListenerContainerFactory
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
springRabbitTracing.decorateSimpleRabbitListenerContainerFactory(factory);
With our container factory which was used to create our consumer factory.
As we don’t have the SpringRabbitTracing
anymore in Micrometer, what we did was to remove these lines and simply enable tracing in the RabbitTemplate.
rabbitTemplate.setObservationEnabled(true);
We also have to set the enabled in the ConnectionFactory
based on this to enable the tracing for the consumers
factory.setObservationEnabled(true);
Database
For databases, we had P6Spy via
implementation "com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.1"
but we were not really using all the functionalities of P6Spy and only the JDBC tracing so we changed the library to use the
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.3'`
in which instruments the Database with Micrometer. Maybe com.github.gavlyukovskiy
still works, I haven’t fully checked that as I wanted to get rid of P6Spy logging as we use the span duration to detect slow queries now.
One adjustment I had to make was to rename the service name in the spans for the Database spans. Without doing anything, it was using the Hikari Pool name (as our pool didn’t start with HikariPool-
) but otherwise, it would use the schema name (as resolved by DefaultDataSourceNameResolver. I wanted to change it to the service name, so I created a custom DataSourceNameResolver
that returned the service name from the application properties.
@Configuration
public class ObservabilityConfig
{
@Value("${spring.application.name}")
private String serviceName;
@Bean
public DataSourceNameResolver dataSourceNameResolver()
{
return (beanName, dataSource) -> serviceName;
}
}
Async
For our async configuration, we were using LazyTraceExecutor
in something like this
@EnableAsync
@Configuration
public class AsyncConfiguration implements AsyncConfigurer
{
@Inject
private BeanFactory beanFactory;
@Override
public Executor getAsyncExecutor()
{
return new LazyTraceExecutor(beanFactory, new OurCusomThreadPool("async-task-"));
}
}
And we changed it to the code below based on this StackOverflow answer
@EnableAsync
@Configuration
public class AsyncConfiguration implements AsyncConfigurer
{
@Override
public Executor getAsyncExecutor()
{
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(5);
threadPoolTaskExecutor.setThreadNamePrefix("async-task-");
threadPoolTaskExecutor.initialize();
return ContextExecutorService.wrap(Executors.newCachedThreadPool(threadPoolTaskExecutor), ContextSnapshot::captureAll);
}
}
Scheduled
For the scheduled tasks, we did something a bit different, in the configuration that implemented SchedulingConfigurer
, we injected the ObservationRegistry
and set it in the ScheduledTaskRegistrar
based on the docs
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT05M")
@RequiredArgsConstructor
@Configuration
public class SchedulingConfiguration implements SchedulingConfigurer
{
private final ObservationRegistry observationRegistry;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
{
taskRegistrar.setTaskScheduler(new OurTaskScheduler("scheduled-task-"));
taskRegistrar.setObservationRegistry(observationRegistry);
}
}
Differences
I notice some differences in the tracing and tags. These are not all the changes, but just the ones that I have noticed.
HTTP spans
In Sleuth, we have the following tags http.method
, http.path
, http.status_code
, mvc.controller.class
, mvc.controller.method
and Client Address
.
In micrometer, http.method
became method
, http.path
became http.url
and http.status_code
became status
and we have a new uri
which shows the URL without interpolation (a nice add).
Also, a good improvement is that database spans (select, insert, result-set) are not duplicated anymore. In Sleuth, it generated two for each operation, one for the server and one client, with the same data. In my opinion that was not necessary and only polluted (making us have to add sampling). Now we have only one span.
These were some of the changes that we made. I hope this helps you in some way.
Cheers.