Spring-Boot-3 e Micrometer-Tracing. Ainda usando Spring-Cloud-Sleuth?

Lírio
Devspoint
Published in
12 min readOct 24, 2023

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.

Trace ID propagado entre serviços

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.

Customer → Kafka Topico → Addresses

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.:

https://micrometer.io/docs/tracing

--

--