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.

Programmmation par aspect avec Spring AOP

Une fois n’est pas coutume, voici un article qui reprend des basiques de la programmation. J’aborde une stack JAVA, mais c’est applicable à d’autres langages.

Il existe une fonctionnalité très intéressante dans Spring (et dans J(akarta)EE) que l’on oublie assez souvent : l’AOP ou encore la programmation par aspect. Cette manière de programmer permet notamment de séparer le code fonctionnel et technique.
Si vous faites du JAVA, vous utilisez déjà l’AOP. En effet, quand vous faites une insertion en base via JPA dans un EJB ou un bean annoté @Transactional, une transaction est initiée au début de la méthode et fermée à la fin.

Avec Spring et notamment dans Spring boot, voici comment initier l’AOP.

Configuration maven

Ajouter le starter AOP:

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

Activation des aspects

Dans la configuration ci-dessous, je prendrai comme exemple le logging des méthodes ( un log en début de méthode et un log en fin ). 

La définition des aspects se fait dans des classes annotées par @Configuration.

@Configuration
@Aspect
@ConditionalOnProperty(name = "debug.enabled", havingValue = "true")
public class DebuggingConfiguration {

private static final Logger LOGGER = LoggerFactory.getLogger(DebuggingConfiguration.class);
private static final String WITHIN_MY_PACKAGE = "within(my.package..*)";

/**
* Log before execution
*
* @param joinPoint the current method
*/
@Before(WITHIN_MY_PACKAGE)
public void logBeforeExecution(JoinPoint joinPoint) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Beginning of method : [{}]", joinPoint.getSignature().getName());
}
}

/**
* Log after execution
*
* @param joinPoint the current method
*/
@After(WITHIN_MY_PACKAGE)
public void logAfterExecution(JoinPoint joinPoint) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("End of method : [{}]", joinPoint.getSignature().getName());
}
}
}

L’utilisation de l’ annotation @ConditionalOnProperty me permet d’activer cette classe de configuration seulement si la propriété debug.enabled est initialisée à true.

Les annotations @Before et @After indiquent à Spring AOP quand exécuter ces méthodes ou sur quelles méthodes. Dans mon cas, quand les méthodes appelées sont définies dans les classes d’un package défini.

Pour plus de détails sur la syntaxe et les possibilités, vous pouvez vous référer à la documentation.


Tracer (facilement) les entrées sorties d’une API REST

Il y a quelques jours, je cherchais comment tracer rapidement et simplement les entrées sorties d’une API REST en appliquant quelques formatages, des filtres, et des insertions en base si besoin.

Travaillant sur une stack SpringBoot, vous allez me dire : oui tu peux faire des filtres. Pour être franc, j’ai essayé d’ appliquer des interceptor et filtres mais dans mon contexte, ça ne collait pas.

Me voilà donc à la recherche d’une solution faisant le taff et qui soit peu intrusive dans mon contexte.

J’ai trouvé par hasard au fil de mes lectures sur Stackoverflow le framework logbook réalisé par … Zalando ( et oui, ils ne font pas que des chaussures) en licence MIT. 
Ce composant ne fait qu’une seule chose, mais il le fait bien !

Il permet entre autres de s’intégrer dans une stack JAVA ( JAX-RS ou SpringMVC), de filtrer, récupérer les différentes informations des requêtes et réponses et enfin de formatter selon l’envie (ex. JSON).

Voici un exemple de mise en œuvre dans un projet SpringBoot:

Dans le  fichier pom.xml, ajouter cette dépendance:

<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>1.11.2</version>
</dependency>

Dans une de vos classes Configuration, définir la factory de Logbook

@Bean
public Logbook createLogBook() {
// too easy : return Logbook.create();
return Logbook.builder()
.condition(Conditions.requestTo("/helloworld"))
.formatter(new JsonHttpLogFormatter())
.build();
}

Dans mon cas j’ai fait un filtre en n’incluant que l’ API /helloworld et j’ai formatté en JSON.
On peut également modifier le processus d’écriture pour ne pas écrire dans un fichier mais en base par ex.

Ensuite, j’ai ajouté la configuration du logger dans le fichier application.properties

logging.level.org.zalando.logbook:TRACE

Et voila !

Dans la console, lors d’un appel ou d’une réponse à mon API, j’ai le message suivant :

018-12-01 15:14:18.373 TRACE 3605 --- [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"c6b345013835273f","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://127.0.0.1:8080/helloworld","headers":{"accept":["/"],"host":["127.0.0.1:8080"],"user-agent":["curl/7.52.1"]}}
2018-12-01 15:14:18.418 TRACE 3605 --- [nio-8080-exec-1] org.zalando.logbook.Logbook : {"origin":"local","type":"response","correlation":"c6b345013835273f","duration":48,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["11"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 01 Dec 2018 14:14:18 GMT"]},"body":"Hello world"}

Vous remarquerez que les requêtes / réponses peuvent désormais être associés grâce à un identifiant de corrélation. On peut facilement déterminer le temps de traitement d’une requête ou encore faciliter les recherches.

Vous trouverez tout le code dans ce repo github.