Observabilité et Circuit Breaker avec Spring

Il y a quelques mois déjà, je discutais avec un collègue d’ observabilité, opentracing, … avec Quarkus. On est tombé sur un super exemple réalisé par Antonio Concalves. Ce projet démontre les capacités de Quarkus sur les sujets suivants:

  • Circuit Breaker
  • Observabilité
  • OpenTracing
  • Tests

Et la on peut se demander quid de Spring? Je me doutais que ces fonctionnalités étaient soient disponibles par défaut soient facilement intégrables vu la richesse de l’écosystème.

J’ai donc réalisé un clone de ce projet basé sur Spring Boot/Cloud. Je ne vais pas détailler plus que ça les différentes fonctionnalités, vous pouvez vous référer au fichier README. Il est suffisamment détaillé pour que vous puissiez exécuter et les mettre en œuvre.

Architecture de l’application

Vous trouverez ci-dessous un schéma d’architecture de l’application au format C4.


Circuit Breaker

Lors des appels entre le bookstore et le booknumberservice, il peut être intéressant d’ implémenter un circuit breaker pour pallier aux indisponibilités de ce dernier.
Avec Spring, on peut utiliser Resilience4J au travers de Spring Cloud. Tout ceci se fait de manière programmatique

Il faut tout d’abord configurer les circuit breakers au travers d’une classe Configuration.

   @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> createDefaultCustomizer() {
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(timeoutInSec)).build())
                .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
                .build());
    }

    /**
     * Creates a circuit breaker customizer applying a timeout specified by the <code>booknumbers.api.timeout_sec</code> property.
     * This customizer could be reached using this id: <code>slowNumbers</code>
     * @return the circuit breaker customizer to apply when calling to numbers api
     */
    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> createSlowNumbersAPICallCustomizer() {
        return factory -> factory.configure(builder -> builder.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(timeoutInSec)).build()), "slowNumbers");
    }

Grâce à ces instanciations, on référence les différents circuit breakers.

Maintenant, on peut les utiliser dans le code de la manière suivante:

public Book registerBook(@Valid Book book) {
        circuitBreakerFactory.create("slowNumbers").run(
                () -> persistBook(book),
                throwable -> fallbackPersistBook(book)
        );

        return bookRepository.save(book);
    }

Maintenant, il ne reste plus qu’à créer une méthode de « fallback » utilisée si un service est indisponible. Cette dernière nous permettra, par exemple, de mettre le payload dans un fichier pour futur traitement batch.

Observabilité

L’observabilité est sans contexte la pierre angulaire (oui, rien que ça…) de toute application cloud native. Sans ça, pas de scalabilité, de redémarrage automatique,etc.
Les architectures de ce type d’applications sont idempotentes. On a donc besoin d’avoir toutes les informations à notre disposition. Heureusement, Spring fournit par le biais d’ Actuator toutes les informations nécessaires. Ces dernières pourront soit être utilisées par Kubernetes (ex. le livenessProbe) ou agrégées dans une base de données Prometheus.

Pour activer certaines métriques d’actuator, il suffit de :

Ajouter la/les dépendance(s)

    dependencies {
[...]
        implementation 'org.springframework.boot:spring-boot-starter-actuator'
        implementation 'io.micrometer:micrometer-registry-prometheus'
     [...]
    }

Spécifier la configuration adéquate:

management:
  endpoints:
    enabled-by-default: true
    web:
      exposure:
        include: '*'
    jmx:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
      enabled: true
      probes:
        enabled: true
    shutdown:
      enabled: true
    prometheus:
      enabled: true
    metrics:
      enabled: true
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true
    datasource:
      enabled: true
  metrics:
    web:
      client:
        request:
          autotime:
            enabled: true

OpenTracing

Sur les applications distribuées, il peut s’avérer compliqué de concentrer les logs et de les corréler. Certes, avec un ID de corrélation, on peut avoir certaines informations. Cependant, il faut que les logs soient bien positionnées dans le code. On peut également passer à travers de certaines informations (ex. connexion aux bases de données, temps d’exécution des APIS,…). Je ne vous parle pas des soucis de volumétrie engendrées par des index Elasticsearch/Splunk sur des applications à forte volumétrie.

Depuis quelques temps, le CNCF propose un projet (encore en incubation) : OpenTracing. Ce dernier fait désormais partie d’OpenTelemetry.
Grâce à cet librairie, nous allons pouvoir tracer toutes les transactions de notre application microservices et pouvoir réaliser une corrélation « out of the box » grâce à l’intégration avec Jaeger.

Pour activer la fonctionnalité il suffit d’ajouter la dépendance au classpath:

implementation 'io.opentracing.contrib:opentracing-spring-jaeger-cloud-starter:3.3.1'

et de configurer l’URL de Jaeger dans l’application

# Default values
opentracing:
  jaeger:
    udp-sender:
      host: localhost
      port: 6831
    enabled: true

Une fois l’application reconstruite et redémarrée, vous pourrez visualiser les transactions dans JAEGER:

Conclusion

Je ne vais pas exposer l’implémentation des tests unitaires et d’intégration. Si vous voulez voir comment j’ai réussi à mocker simplement les appels REST à une API distante, vous pouvez regarder cette classe pour voir une utilisation du MockServer.
Aussi, n’hésitez pas à cloner, tester ce projet et me donner votre retour. J’essaierai de le mettre à jour au fur et à mesure de mes découvertes (par ex. OpenTelemetry).

Ajouter un mode « maintenance » à votre API grâce à Spring boot

Photo by Pixabay on Pexels.com

Quand vous avez une API, et a fortiori une application, il peut être parfois nécessaire de passer l’application en mode « maintenance ».
Pour certaines applications il est parfois inutile de le traiter au niveau applicatif, car ça peut être pris géré par certaines couches de sécurité ou frontaux web par ex. (Apache HTTPD, WAF,…)

Kubernetes a introduit ( ou popularisé ) les notions de « probes » et plus particulièrement les livenessProbes et readinessProbes.
Le premier nous indique si l’application est en état de fonctionnement, le second nous permet de savoir si cette dernière est apte à recevoir des requêtes (ex. lors d’un démarrage).

Je vais exposer dans cet article comment utiliser au mieux ces probes et les APIs SPRING pour intégrer dans une API un mode « maintenance »

Stack utilisée

Dans l’exemple que j’ai développé, j’ai pu utiliser les briques suivantes:

  • OpenJDK 11.0.10
  • Spring Boot 2.5.0 (web, actuator)
  • Maven 3.8.1

Bref, rien de neuf à l’horizon 🙂

Configuration de Spring Actuator

Pour activer les différents probes, vous devez activer Actuator.

Dans le fichier pom.xml, vous devez ajouter le starter correspondant:

<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Puis vous devez déclarer ces differentes propriétés:

management.endpoints.enabled-by-default=true
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true
management.endpoint.health.show-details=always
management.endpoint.health.probes.enabled=true
management.endpoint.health.enabled=true

Après avoir redémarré votre application, vous pourrez connaître son statut grâce à un appel HTTP

curl -s http://localhost:8080/actuator/health/readiness 

Comment récupérer le statut des probes?

Avec Spring, vous pouvez modifier les différents statuts avec les classes ApplicationEventPublisher et ApplicationAvailability.

Par exemple, pour connaître le statut "Readiness" vous pouvez exécuter le code suivant:

 @ApiResponses(value = {
 @ApiResponse(responseCode = "200", description = "Checks if the application in under maitenance")})
 @GetMapping
 public ResponseEntity<MaintenanceDTO> retreiveInMaintenance() {
        var lastChangeEvent = availability.getLastChangeEvent(ReadinessState.class);
        return ResponseEntity.ok(new MaintenanceDTO(lastChangeEvent.getState().equals(ReadinessState.REFUSING_TRAFFIC), new Date(lastChangeEvent.getTimestamp())));
    }

Et la modification ?

Grâce à la même API, on peut également modifier ce statut dans via du code:

@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Put the app under maitenance")})
@PutMapping
public ResponseEntity<Void> initInMaintenance(@NotNull @RequestBody String inMaintenance) {
        AvailabilityChangeEvent.publish(eventPublisher, this, Boolean.valueOf(inMaintenance) ? ReadinessState.REFUSING_TRAFFIC : ReadinessState.ACCEPTING_TRAFFIC);
        return ResponseEntity.noContent().build();
}

Filtre les appels et indiquer que l’application est en maintenance

Maintenant qu’on a codé les mécanismes de récupération du statut de l’application et de la mise en maintenance, on peut ajouter le mécanisme permettant de traiter ou non les appels entrants.
Pour ça on va utiliser un bon vieux filtre servlet.

Ce dernier aura la tâche de laisser passer les requêtes entrantes si l’application n’est pas en maintenance et de déclencher une MaintenanceException le cas échéant qui sera traité par la gestion d’erreur globale de l’application ( traité via un @RestControllerAdvice).

Pour que l’exception soit bien traitée par ce mécanisme, il faut le déclencher via le HandlerExceptionResolver.

@Component
public class CheckMaintenanceFilter implements Filter {
    private final static Logger LOGGER = LoggerFactory.getLogger(CheckMaintenanceFilter.class);
    @Autowired
    private ApplicationAvailability availability;

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver exceptionHandler;

    /**
     * Checks if the application is under maintenance. If it is and if the requested URI is not '/api/maintenance', it throws a <code>MaintenanceException</code>
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     * @throws info.touret.spring.maintenancemode.exception.MaintenanceException the application is under maintenance
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (availability.getReadinessState().equals(ReadinessState.REFUSING_TRAFFIC) &&
                !((HttpServletRequest) request).getRequestURI().equals(API_MAINTENANCE_URI)) {
            LOGGER.warn("Message handled during maintenance [{}]", ((HttpServletRequest) request).getRequestURI());
            exceptionHandler.resolveException((HttpServletRequest) request, (HttpServletResponse) response, null, new MaintenanceException("Service currently in maintenance"));
        } else {
            chain.doFilter(request, response);
        }
    }

}

Enfin, voici la gestion des erreurs de l’API:

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * Indicates that the application is on maintenance
     */
    @ResponseStatus(HttpStatus.I_AM_A_TEAPOT)
    @ExceptionHandler(MaintenanceException.class)
    public APIError maintenance() {
        return new APIError(HttpStatus.I_AM_A_TEAPOT.value(),"Service currently in maintenance");
    }

    /**
     * Any other exception
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler({RuntimeException.class, Exception.class})
    public APIError anyException() {
        return new APIError(HttpStatus.INTERNAL_SERVER_ERROR.value(),"An unexpected server error occured");
    }
}

Conclusion

On a pu voir comment intéragir simplement avec les APIS SPRING pour gérer le statut de l’application pour répondre à cette question :Est-elle disponible ou non?
Bien évidemment, selon le contexte, il conviendra d’ajouter un peu de sécurité pour que cette API ne soit pas disponible à tout le monde 🙂

Le code exposé ici est disponible sur Github. Le Readme est suffisamment détaillé pour que vous puissiez tester et réutiliser le code.