Spring-Boot-3 e Micrometer-Tracing. Ainda usando Spring-Cloud-Sleuth?
Nesse artigo, vamos abordar UM dos TRÊS pilares da observabilidade. O Trace Distribuído! Em especifico com Spring-Boot-3 que passou a usar o Micrometer-Tracing como padrão deixando o Sleuth de lado!
Spring-Boot-3 e Micrometer-Tracing
Automaticamente quando migramos de Spring Boot 2 para o 3, precisamos acompanhar suas atualizações e o que está sendo utilizado como padrão. Numa dessas atualizações, o Spring deixa de ter o Spring-Cloud-Sleuth como padrão para Trace Distribuído, e passa a utilizar o Micrometer-Tracing.
O Time do Spring percebeu que Tracing poderia ser um projeto separado do Spring-Cloud e criou o projeto Micrometer-Tracing. Uma cópia indêntica do Spring-Cloud-Sleuth. Micrometer-Tracing foi lançado em novembro de 2022.
Hands-on
Para ilustrar vamos criar duas aplicações que se integrarão via HTTP (síncrono) e Kafka (assíncrono). A primeira aplicação irá gerenciar o cadastro de clientes, e a outra cadastro de endereços.
Arquitetura Síncrona
Vamos criar a aplicação customers, teremos como dependência → web, webflux, distributed-tracing, actuator, h2, e spring-data-jpa.
Trace-Distribuído → io.micrometer:micrometer-tracing-bridge-brave
Vamos desenvolver nossa aplicação com as camadas Controller/Service/Repository e na nossa classe CustomerEntity inicilamente teremos 3 atributos apenas.
@Entity
@Table(name = "customers")
public class CustomerEntity {
@Id @GeneratedValue private Long id;
private String name;
private LocalDate bornAt;
public Long getId() { return id; }
public String getName() { return name; }
public LocalDate getBornAt() { return bornAt; }
public void setId(Long id) { this.id = id; }
public void setName(String name) { this.name = name; }
public void setBornAt(LocalDate bornAt) { this.bornAt = bornAt; }
}
Repository
@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, Long> {}
Service
@Service
public class CustomerService {
private static final Logger log = LoggerFactory.getLogger(CustomerService.class);
private final CustomerRepository customerRepository;
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public CustomerEntity getById(Long id) {
log.info("method=getById, id={}", id);
return customerRepository.findById(id).get();
}
}
Controller
@RestController
@RequestMapping("/customers")
public class CustomerController {
private static final Logger log = LoggerFactory.getLogger(CustomerController.class);
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/{id}")
public CustomerEntity getById(@PathVariable("id") Long id) {
log.info("method=getById, step=starting, id={}", id);
var customer = customerService.getById(id);
log.info("method=getById, step=finished, id={}, customer={}", id, customer);
return customer;
}
@ExceptionHandler({NoSuchElementException.class})
public ResponseEntity<Object> notFoundHandleException(Exception e) {
var map = new HashMap<String, String>();
map.put("message", "Customer Not Found");
log.warn("method=notFoundHandleException, step=not_found, e={}", e.getMessage());
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(map);
}
}
Por último vamos configurar a Spring-de-conexão do nosso banco H2, para isso, vamos adicionar o código abaixo no application.yml
spring:
datasource:
url: jdbc:h2:mem:demodb
username: sa
password:
driver-class-name: org.h2.Driver
h2.console.enabled: true
Ao executar um HTTP-Request temos o log abaixo, podemos ver que passou pelo Controller, Service, não encontrou o recurso no Banco e devolvemos um StatusCode 404.
curl http://localhost:8080/customers/1
> {"message":"Customer Not Found"}
Log….
INFO 94481 --- [nio-8080-exec-1] c.d.s.controller.CustomerController : method=getById, step=starting, id=1
INFO 94481 --- [nio-8080-exec-1] c.d.s.service.CustomerService : method=getById, id=1
WARN 94481 --- [nio-8080-exec-1] c.d.s.controller.CustomerController : method=notFoundHandleException, step=not_found, e=No value present
Trace ID e Span ID
Para adicionar o traceId e spanId, nos logs vamos precisar colocar a configuração abaixo no application.yml.
logging:
pattern:
level: '%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]'
O logging.pattern.level possui basicamente 3 infos, nome da aplicação, traceId e spanId. O traceId é um ID único que é gerado para rastrear uma única solicitação ou transação à medida que ela se move através de vários serviços em um ambiente distribuído.
Após adicionar a configuração acima e executar novamente o Request teremos o Log abaixo:
2023-10-19T16:13:56.505-03:00 INFO [customers,65317ff4457e9241e3a1f722b22da378,e3a1f722b22da378] 94853 --- [nio-8080-exec-1] c.d.s.controller.CustomerController : method=getById, step=starting, id=1
2023-10-19T16:13:56.505-03:00 INFO [customers,65317ff4457e9241e3a1f722b22da378,e3a1f722b22da378] 94853 --- [nio-8080-exec-1] c.d.s.service.CustomerService : method=getById, id=1
2023-10-19T16:13:56.541-03:00 WARN [customers,65317ff4457e9241e3a1f722b22da378,e3a1f722b22da378] 94853 --- [nio-8080-exec-1] c.d.s.controller.CustomerController : method=notFoundHandleException, step=not_found, e=No value present
Entre o LOG_LEVEL e o Id do Log temos as infos no qual configuramos:
Adicionando o endpoint para criação um novo cliente.
@PostMapping
public ResponseEntity<?> create(
@RequestBody CustomerEntity customerEntity,
UriComponentsBuilder ucb
) {
log.info("method=create, step=starting, customerEntity={}", customerEntity);
var customer = customerService.create(customerEntity);
log.info("method=create, step=finished, customerId={}", customer.getId());
return ResponseEntity
.created(ucb.path("/customers/{id}").buildAndExpand(customer.getId()).toUri())
.build();
}
Service
public CustomerEntity create(CustomerEntity customerEntity) {
log.info("method=create, customerEntity={}", customerEntity);
return customerRepository.save(customerEntity);
}
Cadastrando um cliente…
curl -i -H 'Content-Type: application/json' \
-d '{ "name":"foo", "bornAt": "2000-05-03"}}' \
-X POST \
localhost:8080/customers
HTTP/1.1 201
Location: http://localhost:8080/customers/1
Content-Length: 0
Date: Mon, 23 Oct 2023 17:43:52 GMT
Buscando o mesmo…
# Buscando Cliente-1 cadastrado no request acima
curl http://localhost:8080/customers/1
> {"id":1,"name":"foo","bornAt":"2000-05-03"}
# Buscando cliente-2 - Nao encontrado
curl http://localhost:8080/customers/2
> {"message":"Customer Not Found"}
Executamos 3 requests para os 3 traces-Id diferente. As três primeiras linhas de logs são para criar o cliente, do quarto ao sexto para busca-lo pelo o ID (1), e as três últimas linhas, para buscar um cliente novamente.
INFO [customers,653183c551762ac564df73123307c804,64df73123307c804] 95655 - - [nio-8080-exec-1] c.d.s.controller.CustomerController : method=create, step=starting, customerEntity=com.devspoint.springboot3micrometertracingcustomers.entity.CustomerEntity@22309756
INFO [customers,653183c551762ac564df73123307c804,64df73123307c804] 95655 - - [nio-8080-exec-1] c.d.s.service.CustomerService : method=create, customerEntity=com.devspoint.springboot3micrometertracingcustomers.entity.CustomerEntity@22309756
INFO [customers,653183c551762ac564df73123307c804,64df73123307c804] 95655 - - [nio-8080-exec-1] c.d.s.controller.CustomerController : method=create, step=finished, customerId=1
INFO [customers,653184781bbfa459167e0316f60a8cc8,167e0316f60a8cc8] 95655 - - [nio-8080-exec-2] c.d.s.controller.CustomerController : method=getById, step=starting, id=1
INFO [customers,653184781bbfa459167e0316f60a8cc8,167e0316f60a8cc8] 95655 - - [nio-8080-exec-2] c.d.s.service.CustomerService : method=getById, id=1
INFO [customers,653184781bbfa459167e0316f60a8cc8,167e0316f60a8cc8] 95655 - - [nio-8080-exec-2] c.d.s.controller.CustomerController : method=getById, step=finished, id=1, customer=com.devspoint.springboot3micrometertracingcustomers.entity.CustomerEntity@4c8c4e52
INFO [customers,653184f52052ba6c59cc1f6e34bdcc1f,59cc1f6e34bdcc1f] 95655 --- [nio-8080-exec-4] c.d.s.controller.CustomerController : method=getById, step=starting, id=2
INFO [customers,653184f52052ba6c59cc1f6e34bdcc1f,59cc1f6e34bdcc1f] 95655 --- [nio-8080-exec-4] c.d.s.service.CustomerService : method=getById, id=2
WARN [customers,653184f52052ba6c59cc1f6e34bdcc1f,59cc1f6e34bdcc1f] 95655 --- [nio-8080-exec-4] c.d.s.controller.CustomerController : method=notFoundHandleException, step=not_found, e=No value present
Tracing Context Propagation
Existem alguns tipos de Progragação, um dos mais comuns é o B3 (Big-Brother-Bird). Foi desenvolvido pelo projeto Zipkin. O W3C é outro padrão de propagação de context de rastreamento em microserviços. O objetivo é padronizar como as informações de rastreamento são transmitidas para outro serviço.
Embora o B3 seja amplamente adotado em sistemas que usam o projeto Zipkin, o W3C Trace Context busca oferecer uma padronização mais ampla para rastreamento em toda a web. Uma das mudanças mais notáveis do Spring-Boot-3, é ter como padrão o W3C para progagação de contexto.
Vamos criar a segunda aplicação para seguir o desenho, customers→addresses. A aplicação addresses terá a mesma configuração da aplicação customers. Inicialmente vamos criar a AddressEntity, depois o controller e repository. Sim! Pulei o service para somente simplificar.
@Entity
@Table(name="addresses")
public class AddressEntity {
@Id @GeneratedValue private Long id;
private String street;
private Integer number;
private String district;
private String city;
private String state;
private Long customerId;
// getters and setters...
}
Controller
@RestController
@RequestMapping("/addresses")
public class AddressController {
private static final Logger log = LoggerFactory.getLogger(AddressController.class);
private final AddressRepository addressRepository;
public AddressController(AddressRepository addressRepository) {
this.addressRepository = addressRepository;
}
@GetMapping("/customers/{customerId}")
public List<AddressEntity> getAddressesByCustomerId(@PathVariable("customerId") Long customerId) {
log.info("method=getAddressesByCustomerId, step=starting, customerId={}", customerId);
return addressRepository.findByCustomerId(customerId);
}
}
Repository
@Repository
public interface AddressRepository extends CrudRepository<AddressEntity, Long> {
List<AddressEntity> findByCustomerId(Long customerId);
}
HTTP Client
Ao buscar um cliente existente na base de dados de customers, vamos buscar também os endereços do mesmo, então para isso vamos desenvolver uma chamada HTTP-Client.
RestTemplate
Vamos utilizar o RestTemplate, mas também vamos realizar exemplos com o WebClient do Webflux.
Voltando para a aplicação customers, vamos alterar o CustomerEntity que terá mais um atributo, List<Address> addresses. O AddressHttpClient.java ficará na nova camada httpclient, o service será alterado para buscar endereços do cliente no novo serviço addresses.
AddressEntity
public class AddressEntity {
private Long id;
private String street;
private Integer number;
private String district;
private String city;
private String state;
}
CustomerEntity
@Entity
@Table(name = "customers")
public class CustomerEntity {
@Id @GeneratedValue private Long id;
private String name;
private LocalDate bornAt;
@Transient private List<AddressEntity> addresses;
// ...
AddressHttpClient
@Component
public class AddressHttpClient {
private static final Logger log = LoggerFactory.getLogger(AddressHttpClient.class);
@Autowired
private RestTemplateBuilder restTemplateBuilder;
public List<AddressEntity> getAddressesByCustomerId(Long customerId) {
log.info("method=getAddressesByCustomerId, customerId={}", customerId);
return restTemplateBuilder
.build()
.getForEntity("http://localhost:8085/addresses/customers/" + customerId, List.class)
.getBody();
}
}
Aqui nesse ponto temos um detalhe muito importante para fazer o tracing funcionar. O Spring tem um RestTemplate já instanciado e adicionado no seu Container, o RestTemplateBuilder!
Ao injetarmos o mesmo, agora precisamos utilizar o metodo .build() para obter de fato o RestTemplate, e agora sim terminar o código como um RestTemplate tradicional.
CustomerService
@Service
public class CustomerService {
private static final Logger log = LoggerFactory.getLogger(CustomerService.class);
private final CustomerRepository customerRepository;
private final AddressHttpClient addressHttpClient; // NOVO
public CustomerService(CustomerRepository customerRepository, AddressHttpClient addressHttpClient) {
this.customerRepository = customerRepository;
this.addressHttpClient = addressHttpClient; // NOVO
}
public CustomerEntity getById(Long id) {
log.info("method=getById, id={}", id);
CustomerEntity customerEntity = customerRepository.findById(id).get();
// NOVO
List<AddressEntity> addresses = addressHttpClient.getAddressesByCustomerId(customerEntity.getId());
log.info("method=getById, id={}, addresses={}", id, addresses);
customerEntity.setAddresses(addresses);
return customerEntity;
}
// ....
Realizando os requests…
curl -i -H 'Content-Type: application/json' \
-d '{ "name":"foo", "bornAt": "2000-05-03"}}' \
-X POST \
localhost:8080/customers
HTTP/1.1 201
Location: http://localhost:8080/customers/1
Content-Length: 0
Date: Mon, 23 Oct 2023 17:53:34 GMT
curl http://localhost:8080/customers/1
> {"id":1,"name":"foo","bornAt":"2000-05-03", "addresses":[]}
Com 2 IntelliJ abertos vamos conseguir ver o mesmo traceId, que foi propagado.
WebClient
Para usar o WebClient do webflux, seria algo parecido com o RestTemplate.
@Autowired
private WebClient.Builder webClientBuilder;
//
webClientBuilder.baseUrl("http://localhost:8085/addresses").build();
Kafka e Arquitetura Assincrona
Muito comum arquiteturas de software adotar o Async-First. Nesse caso, para integração, passamos a ter outras opções além de um HTTP ou gRPC. Em arquiteturas Assíncronos uma das ferramentas mais utilizadas é o Kafka.
Temos o HTTP POST na aplicação customers para criar um novo cliente (já implementamos parte desse código), e após salvar o cliente, vamos produzir uma mensagem ao Tópico. A aplicação addresses irá consumir e gravar a mesagem do banco de dados.
Antes de alterarmos o código novamente, vamos subir um Kafka com docker. → docker-compose.yml
docker-compose up
Spring-Kafka
Adicionando a dependência do Kafka na aplicação customers.
implementation 'org.springframework.kafka:spring-kafka'
Primeiramente vamos criar um Bean do KafkaTemplate.
@Configuration
public class KafkaConfig {
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
configProps.put(ProducerConfig.CLIENT_ID_CONFIG, "oneClientId");
KafkaTemplate<String, Object> kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<>(configProps));
kafkaTemplate.setObservationEnabled(true); // <<<<
return kafkaTemplate;
}
}
Observe que temos um metodo no kafkaTemplate, setObservationEnabled(true). Para habilitar o Tracing via Micrometer.
Kafka Producer (customers)
@Component
public class CustomerUpdateKafkaProducer {
private static final Logger log = LoggerFactory.getLogger(CustomerUpdateKafkaProducer.class);
@Autowired
private KafkaTemplate<String, Object> kafkaTemplate;
public void send(CustomerEntity customerEntity) {
log.info("method=send, step=starting, customerEntity={}", customerEntity);
kafkaTemplate.send("topic.customer.updated", customerEntity).isDone();
log.info("method=send, step=finished, customerEntity={}", customerEntity);
}
}
Alterando o CustomerService#create. Após salvar o cliente no Banco, enviaremos para o Tópico para Notificar os serviços interessados nessa criação.
public CustomerEntity create(CustomerEntity customerEntity) {
log.info("method=create, customerEntity={}", customerEntity);
CustomerEntity saved = customerRepository.save(customerEntity);
producer.send(saved);
return saved;
}
Implementando o kafka-cosumer na aplicação addresses.
build.gradle (addresses)
implementation 'org.springframework.kafka:spring-kafka'
KafkaConfig.java (addresses)
@EnableKafka
@Configuration
public class KafkaConfig {
@Bean
public KafkaAdmin kafkaAdmin() {
Map<String, Object> configs = new HashMap<>();
configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
return new KafkaAdmin(configs);
}
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "addresses");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
ContainerProperties containerProperties = factory.getContainerProperties();
containerProperties.setMicrometerEnabled(true);
containerProperties.setLogContainerConfig(true);
containerProperties.setObservationEnabled(true);
containerProperties.setCommitLogLevel(LogIfLevelEnabled.Level.INFO);
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
Kafka Consumer (addresses)
@Component
public class KafkaCustomerConsumer {
private static final Logger log = LoggerFactory.getLogger(KafkaCustomerConsumer.class);
private final AddressRepository addressRepository;
public KafkaCustomerConsumer(AddressRepository addressRepository) {
this.addressRepository = addressRepository;
}
@KafkaListener(topics = "topic.customer.updated", groupId = "addresses")
public void listenGroupAddresses(String message) throws JsonProcessingException {
log.info("m=listenGroupAddresses, step=init, message={}", message);
CustomerDto customer = new ObjectMapper().readValue(message, CustomerDto.class);
if (customer.getAddresses() == null || customer.getAddresses().isEmpty()) {
log.warn("m=listenGroupAddresses, step=not_contains_address");
return;
}
List<AddressEntity> address = customer.getAddresses().stream().peek(it -> it.setCustomerId(customer.getId())).collect(Collectors.toList());
addressRepository.saveAll(address);
log.info("m=listenGroupAddresses, step=finished");
}
}
Vamos executar novamente o endpoint POST /customers.
Adicionando uma nova dependência, o zipkin-reporter-brave!
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
Latência entre os serviços, saídas para infraestrutura, como integração com banco de dados e http-client é considerado muito importante termos uma visão clara do que está acontecendo. Percebemos acima que para visualizar o Trace End-to-End não é das melhores abordagem, isso porque temos somente 2 serviços, em um cenário com 5, 10, 20 serviços, essa rastreabilidade fica mais dificil ainda. Então para isso temos uma ferramenta que nos ajuda nessa visualização, o Zipkin.
Ao adicionar a dependência nas duas aplicações, estamos coletando e enviando para o Zipkin toda a instrumentação e tracing do nosso código. Para isso devemos ter um Zipkin rodando, podemos adicionar o openzipkin/zipkin no mesmo docker-compose.yml que usamos para subir o Kafka.
Vale comentar que se estamos seguindo os padrões do OpenTelementry, podemos trocar de vendor com muita facilidade. Basta mudar a forma que exportamos os logs, tracing e métricas. Nesse caso, basta trocar a lib ‘zipkin-reporter-brave’ pela do Jaeger por exemplo!
Ao abrir a URL http://localhost:9411 no navegador vamos ver a seguinte tela.
Nossa aplicação enviará a instrumentação e o trace para esse endereço http://localhost:9411/api/v2/spans.
Também é necessário adicionar as properties abaixo no application.properties.
management.tracing.sampling.probability=1.0 # para enviar 100% da amostra
management.tracing.propagation.type=w3c
management.tracing.baggage.enabled=true
management.tracing.enabled=true
management.zipkin.tracing.endpoint=SEU_ZIPKIN/api/v2/spans # alterar o endereco do Zipkin
O propagation.type por padrão já é o w3c.
Subi as aplicações novamente e executei os 3 requests abaixo:
#1
curl localhost:8080/customers/1
> {"message":"Customer Not Found"}
#2
curl -H 'Content-Type: application/json' \
-d '{ "name":"foo", "bornAt": "2000-05-03", "addresses": [{ "street": "Av Paulista", "number": 1320, "district": "Bela Vista", "city": "Sao Paulo", "state": "SP" }]}}' \
-X POST \
localhost:8080/customers
HTTP/1.1 201
Location: http://localhost:8080/customers/1
Content-Length: 0
Date: Mon, 23 Oct 2023 18:02:32 GMT
#3
curl localhost:8080/customers/1
> {"id":1,"name":"foo","bornAt":"2000-05-03","addresses":[{"id":1,"street":"Av Paulista","number":1320,"district":"Bela Vista","city":"Sao Paulo","state":"SP"}]}
No Zipkin teremos essa visão:
Temos 3 registros, equivalente aos 3 requests. O primeiro, temos somente a aplicação customers label azul, que realizou uma busca no banco de dados e não encontrou o customer com o ID=1, lançou uma Exception e não executou o request para a aplicação addresses.
O Segundo, criamos um cliente, que nos retornou ID=1, produziu uma mensagem ao Kafka-Tópico, podemos ver o mesmo com o label amarelo escrito “apache kafka: ggec…”, e por último temos o addresses que consumiu a mensagem do tópico e persistiu o endereço-do-cliente no Banco de Dados.
A terceira requisição é a mesma da primeira, só que agora temos os dados no Banco, então foi adicionado o label addresses.
Ao clicar no botão Show da segunda requisição podemos analisar um pouco mais esse Trace.
Para cada Componente/Serviço podemos fazer uma análise individual. O tempo de resposta do customers foi 381ms, enquanto já recebemos a resposta, o trace continuou, o que seria normal numa arquitetura assíncrona. O tempo que a mensagem ficou no tópico foi de 58ms e o tempo de processamento do addresses foi de 76ms.
Observando parte especificas do código
Para observar partes especificas do nosso código, precisamos do Objeto ObservationRegistry, temos ele no Spring-Container basta injetar onde vamos usar.
Um novo SpanId, Logs e Temporizadores serão criados, abaixo ficará mais claro como isso acontece. No entanto, se quiser saber um pouco mais como o ciclo de vida do Obervation funciona, deixo esse artigo como referencia → observability-with-spring-boot-3.
Na nossa aplicação customers, na camada config, vamos criar uma nova classe de configuração, para que o Observation funcione precisaremos da dependência do spring-boot-starter-aop.
org.springframework.boot:spring-boot-starter-aop
@Configuration(proxyBeanMethods = false)
class ObservedAspectConfiguration {
@Bean
public ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
return new ObservedAspect(observationRegistry);
}
}
O Spring usa o Aspect para Observar parte específicas do nosso código, bastando usar a Annotation @Observed. Essa annotation pode ser usada tanto numa classe ou em um método específico, vamos adiciona-lá na classe CustomerService.
@Service
@Observed
public class CustomerService {
// ...
Após um request no endpoint /customers/{id}, vamos analisa-lo no Zipkin.
O primeiro ponto que podemos observar, temos o Label customer(2), esse 2 significa que agora temos 2 spansId, temos essa info também na Coluna Spans, temos o duration que agora está destacado de vermelho com o tempo do Request de 92ms, isso porque no método getById lançou uma exceção, manipulamos esse exceção no controller e retornamos um 404. Vamos clicar no botao Show e analisar os detalhes.
Podemos perceber que o tempo de processamento total da Requisição foi de 92ms, mas agora temos o trace e o tempo do customer-service#get-by-id, esse é o método da classe CustomerService com seu tempo total de processamento, 35ms do tempo total de 92ms.
Abaixo executei o POST /customers novamente. Tempo do customers foi de 347ms, sendo 295ms somente do customerService#create. Em seguida temos o trace do tópico e do addresses como vimos antes.
Conclusão
Se você já está usando ou pensa em usar o Spring-Boot-3, vale começar a entender um pouco mais essas mudanças. Se caso não esteja tanto familiarizado com o tema de Observabilidade, deixo as referências de alguns links abaixo sobre Micrometer, Spring-Boot-3 e Zipkin.
Valeu ;-)
Github:
https://gtihub.com/diegolirio/spring-boot-3-observability
Ref.: