Ejemplo de observabilidad en Spring Boot 3

Observabilidad en Spring Boot 3

Observabilidad en Spring Boot 3


Uno de los cambios que ha llevado Spring Boot 3 ha sido lo referente a la Observabilidad mediante la eliminación de la trazabilidad del proyecto Spring Cloud Sleuth y su incorporación con micrometer. En este artículo vamos a ver un ejemplo de Observabilidad en Spring Boot 3 haciendo uso de micrometer.

¿Qué es la Observabilidad?

Podríamos definir la Observabilidad como la interacción entre logging, métricas y monitorización de un sistema, que nos va a permitir explicar el estado de un sistema a través de estos parámetros. Para ver más sobre observabilidad en una arquitectura de microservicios puedes echar un ojo a un artículo anterior.

¿Qué es micrometer?

Micrometer es una fachada para recoger métricas de la JVM. Permite una integración fácil y sencilla con Spring Boot a través de starters y autoconfiguración.

Ejemplo de Observabilidad en Spring Boot 3

A continuación vamos a crear dos aplicaciones haciendo uso de Spring Boot 3, la primera aplicación será User y la segunda Accounts, en la que la aplicación User hará una llamada a Accounts para obtener las cuentas de un usuario.

Ejemplo Observabilidad en Spring Boot 3
Ejemplo Observabilidad en Spring Boot 3

Las aplicaciones creadas expondrán métricas a Prometheus a través de Spring Boot Actuator y Micrometer. Para la trazabilidad se hará uso de OpenZipkin y OpenTelemetry.

Además haremos uso de H2 como Base de Datos en memoria y de WebClient o RestTemplate para realizar la comunicación y de user y accounts.

Si quieres ver el ejemplo completo lo puedes descargar de Github directamente, en este ejemplo se conecta a un agente grafana de manera automática.

Dependencias Maven para Observabilidad en Spring Boot 3

El primer paso será crear la aplicación User, para ello haremos uso del initializr de Spring.

Spring Initializr
Spring Initializr

Una vez hemos generado nuestra aplicación con Spring Boot 3 es momento de añadir las dependencias necesarias, en el ejemplo hemos añadido la posibilidad de exportar a prometheus y grafana.

<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

La primera dependencia que es micrometer-registry-prometheus nos va a servir para exportar las métricas en el formato de Prometheus; para la propagación del contexto haremos uso de micrometer-tracing-bridge-otel y como última dependencia necesaria tenemos que añadir opentelemetry-exporter-zipkin para exportar a Grafana (antiguamente esta dependencia sería con Spring Cloud Sleuth).

A continuación vamos a ir viendo las capas de los dos servicios (user y account).

Configuración propiedades de User Service

spring:
  application:
    name: user-service
  output.ansi.enabled: ALWAYS

server.port: 8089

spring.datasource.url: jdbc:h2:mem:testdb
spring.datasource.driverClassName: org.h2.Driver
spring.datasource.username: sa
spring.datasource.password: password
spring.jpa.database-platform: org.hibernate.dialect.H2Dialect
spring.h2.console.enabled: true

management.endpoints.web.exposure.include: '*'
management.metrics.distribution.percentiles-histogram.http.server.requests: true
management.tracing.sampling.probability: 1.0

logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){red} %clr(%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]){blue} %clr(:){green} %clr(%m){faint}%n" 

Configuración de ResTemplate

Vamos a hacer uso de RestTemplate aunque también podemos hacer uso de WebClient, pero hay que tener en cuenta que debemos crear el restTemplate o WebClient haciendo uso de «builder».

@Configuration
public class ClientConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

}

Controlador User service

Es el punto de entrada a la aplicación. En el controlador del servicio User vamos a tener dos endpoint, uno para obtener los detalles del usuario y otro para obtener el usuario y la cuenta asociada.

    private final UserService userService;

    @GetMapping("/{userId}")
    public ResponseEntity<User> getUserInfo(@PathVariable long userId) {

            return ResponseEntity.ok(userService.getUserDetailByAccountId(userId));
    }

    @GetMapping()
    public ResponseEntity<List<User>> getUserInfo(@RequestParam String name, @RequestParam int page) {

        return ResponseEntity.ok(userService.findUserPageable(name, page));
    }

Capa Service de User service

Esta capa será la encargada de establecer la comunicación con Account Service haciendo uso de RestTemplate o WebClient.

@RequiredArgsConstructor
@Service
@Slf4j
public class UserService {

    private final WebClientConfig webClientConfig;

    private final UserDetailRepository userDetailRepository;

    private final UserMapper userMapper;


    private final RestTemplate restTemplate;

    public List<User> findUserPageable(String name, int initPage) {

        Pageable page = PageRequest.of(initPage, 10);

        List<UserDetail> userDetail = userDetailRepository.findAllByName(name, page);

        return userDetail.stream()
                .map(user -> userMapper.toDto(user))
                .collect(Collectors.toList());

    }

    public User getUserDetailByAccountId(Long userId) {

        log.info("Getting account detail by userId {} ", userId);

        UserDetail userDetail = userDetailRepository.getReferenceById(userId);


        Account accountDetail = restTemplate.getForEntity("http://127.0.0.1:8080/accounts/" + 1, Account.class).getBody();

        //Use with webclient
        //Account accountDetail = getAccountDetailByAccountId(userDetail.getAccountNumber());

        User user = userMapper.toDto(userDetail);
        user.setAccount(accountDetail);

        return user;
    }

    private Account getAccountDetailByAccountId(Long account) {
        WebClient.RequestHeadersSpec<?> response =
                webClientConfig.webClientConfig().get().uri("accounts/"+account);

        return response.retrieve().bodyToMono(Account.class).block();
    }
}

Una vez tenemos el Servicio creado únicamente es necesario crear la capa Repository y la Entity. En la que haremos uso de JpaRepository

Capa Repository

Esta capa será la responsable de establecer la comunicación y la gestión con la Base de Datos.

public interface UserDetailRepository extends JpaRepository<UserDetail, Long> {

    List<UserDetail> findAllByName(String name, Pageable pageable);


}

Creación de Entidad User en Spring Boot 3

El último paso sería la creación de la entidad en la que podemos ver una de las novedades de Spring Boot 3 que es el uso de jarkarta para sustituir a javax.

import jakarta.persistence.*;
import lombok.*;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class UserDetail {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private int age;

    private long accountNumber;

}

Generación Datos en User Service

Los datos que vamos a generar son aleatorios, creando un nombre que siempre empieza por «pepé».

@Component
@RequiredArgsConstructor
public class SaveData implements CommandLineRunner {

    private final UserDetailRepository userDetailRepository;

    @Override
    public void run(String... args) {

        long leftLimit = 1L;
        long rightLimit = 100L;
        IntStream.range(0, 100).forEach(cont -> {
            userDetailRepository.saveAndFlush(UserDetail.builder().name(generateName())
                    .accountNumber(leftLimit + (long) (Math.random() * (rightLimit - leftLimit))).build());
        });


    }

    public String generateName() {

        int leftLimit = 97; 
        int rightLimit = 122; 
        int targetStringLength = 10;
        Random random = new Random();

        return "pepe " + random.ints(leftLimit, rightLimit + 1)
                .limit(targetStringLength)
                .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                .toString();
    }
}

Configuración propiedades Account Service

Spring Boot 3 nos va a permitir referencias a datos fuera de las métricas publicadas por la aplicación, es decir, nos permite asociar datos de métricas a trazas distribuidas con lo que las métricas que se van publicando tienen referencias al traceId de la request.

Por ejemplo en el caso de Account Service las propiedades quedarían de la siguiente manera:

spring:
  application:
    name: account-service
  output.ansi.enabled: ALWAYS

spring.datasource.url: jdbc:h2:mem:testdb
spring.datasource.driverClassName: org.h2.Driver
spring.datasource.username: sa
spring.datasource.password: password
spring.jpa.database-platform: org.hibernate.dialect.H2Dialect
spring.h2.console.enabled: true


management.endpoints.web.exposure.include: '*'
management.metrics.distribution.percentiles-histogram.http.server.requests: true 
management.tracing.sampling.probability: 1.0
logging.pattern.console: "%clr(%d{HH:mm:ss.SSS}){red} %clr(%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]){blue} %clr(:){green} %clr(%m){faint}%n" 

Las propiedades anteriores corresponden al servicio account en el que hemos definido por un lado las propiedades de la Base de Datos H2 y por otro lado la configuración para realizar la trazabilidad y un patrón para escribir logs.

Capa Controlador de Account Service

@RestController
@RequiredArgsConstructor
@RequestMapping("/accounts")
@Slf4j
public class AccountController {

    private final AccountService accountService;

    @GetMapping("/{accountId}")
    public ResponseEntity<Account> getUserInfo(@PathVariable long accountId) {

        log.info("Get Account info by accountID {}", accountId);

        return ResponseEntity.ok(accountService.getAccountById(accountId));
    }

}

Creación del Servicio de Account Service

En el servicio vamos a devolver siempre alguna cuenta (para poder ver el ejemplo) por lo que si la cuenta que se busca no la encuentra se devolverá la primera que exista:

@RequiredArgsConstructor
@Service
@Slf4j
public class AccountService {

    private final AccountDetailRepository accountDetailRepository;

    private final AccountMapper accountMapper;


    public Account getAccountById(Long accountId) {

        log.info("Getting account by id {}", accountId);

        AccountDetail accountDetail = accountDetailRepository.getReferenceById(accountId);

        try {

            return accountMapper.toDto(accountDetail);

        } catch (EntityNotFoundException ex) {
            log.debug("Account not found, getting first {} ", accountId);

            accountDetail = accountDetailRepository.findAll().get(0);

        }

        return accountMapper.toDto(accountDetail);

    }

}

Capa del Repositorio de Account Service

En la capa Repository únicamente hacemos uso de JpaRepository.

public interface AccountDetailRepository extends JpaRepository<AccountDetail, Long> {
}

Generación de Datos en Account Service

Para generar datos aleatorios vamos a hacer uso del siguiente código:

@Component
@RequiredArgsConstructor
public class SaveData implements CommandLineRunner {

    private final AccountDetailRepository accountDetailRepository;

    @Override
    public void run(String... args) {

        IntStream.range(0, 100).forEach(cont -> {
            accountDetailRepository.saveAndFlush(AccountDetail.builder().amount(generateNumber()).build());
        });


    }

    public static BigDecimal generateNumber() {
        BigDecimal min = new BigDecimal(0);
        BigDecimal max = new BigDecimal(10000);

        int digitCount = Math.max(min.precision(), max.precision());
        int bitCount = (int) (digitCount / Math.log10(2.0));

        BigDecimal alpha = new BigDecimal(
                new BigInteger(bitCount, new Random())
        ).movePointLeft(digitCount);

        return min.add(max.subtract(min).multiply(alpha, new MathContext(digitCount)));
    }
}

Probando la aplicación de Observabilidad

Una vez hemos creado nuestra aplicación vamos a realizar unas cuantas peticiones para ver como se propaga el traceId y como aparece en nuestro log customizado.

Para realizar la prueba podemos realizarla escribiendo en el navegador http://localhost:8089/users/1, o con un curl

  • Escribir http://localhost:8089/users/1 en el navegador
  • Realizar el siguiente curl: curl http://localhost:8089/users/1

Y al ejecutar este comando podemos ver la trazabilidad a través del trazeId:

TraceId en Account Service con Spring Boot 3
TraceId en Account Service
TraceId en Account Service con Spring Boot 3
TraceId en User Service

Como podemos ver en las imágenes anteriores el traceId se ha propagado de userService a accountService: 35d2b2c78fd68f378ae71f5d2aace134.

Posible error en propagación de TraceId

Uno de los problemas que nos podemos encontrar al hacer uso de micrometer es que el traceId no lo veamos propagado entre llamadas. En el caso en el que nos ocurra este problema, y tengamos todo bien configurado, suele deberse a que el restTemplate o WebClient creado no se ha hecho con «builder».

Conclusión

En este artículo sobre Ejemplo de observabilidad en Spring Boot 3 hemos visto como podemos hacer uso de la observabilidad en la nueva versión de Spring Boot gracias a micrometer.

Si quieres ver el ejemplo completo puedes descargarlo de aquí.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *