Como registrar uma aplicação cliente num servidor de monitoramento Spring Boot Admin.

Existem várias formas de se monitorar sistemas distribuídos e uma que me agrada bastante pela simplicidade de configuração e confiabilidade é o Spring Boot Admin.

Nesse tutorial criaremos duas aplicações utilizando o Spring Boot, uma que será o servidor de monitoramento e a outra, o cliente que deverá se registrar para ser monitorado.

Também vamos aproveitar para implementar uma camada de segurança utilizando o Spring Security e com o #Maven, poderemos fazer o build das aplicações para serem executadas individualmente.

Versões utilizadas

  • Spring Boot: 2.7.10
  • Spring Boot Admin Server: 2.7.10
  • Spring Boot Admin Client: 2.7.10

Configurando o Servidor

Crie um projeto utilizando o Spring Initilizr com as seguintes dependências:

  • Starter Web
  • Starter Security
  • Admin Starter Server
tela spring initializr

Confira se as dependências relacionadas ao Spring Boot Admin foram adicionadas:

              
    ...
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-server</artifactId>
    </dependency>
    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-server-ui</artifactId>
    </dependency>
    ...
              
            

Adicione a anotação @EnableAdminServer em alguma classe de configuração:


              
    ...

    @EnableAdminServer
    @SpringBootApplication
    public class SpringBootAdminServerApplication {

        public static void main(String[] args) {
            SpringApplication.run(SpringBootAdminServerApplication.class, args);
        }

    }
              
            

Configure o comportamento da aplicação utilizando o arquivo application.yaml. Como adicionamos uma camada de segurança, devemos criar um nome de usuário e senha para logarmos no servidor e também será necessário configurar o login para comunicação entre servidor e cliente.

              
    server:
      port: 8081
      servlet:
        context-path: /admin-console
    spring:
      security:
        user:
          # Configura o login do servidor.
          name: ${SBA_SERVER_USERNAME}
          password: ${SBA_SERVER_PASSWORD}
      boot:
        admin:
          client:
            # Necessários para que o cliente possa se registrar na api do servidor protegido.
            username: ${SBA_SERVER_USERNAME}
            password: ${SBA_SERVER_PASSWORD}
            instance:
              metadata:
                user:
                  # Necessários para que o servidor possa acessar os endpoints protegidos do cliente.
                  name: ${SBA_CLIENT_USERNAME}
                  password: ${SBA_CLIENT_PASSWORD}

    # LOG
    logging:
      file:
        name: ${user.home}/logs/admin/sba-server.log
      level:
        root: info
        web: info
        dev.marksduarte: info
        org.springframework: info
      charset:
        file: utf-8
              
            

Agora vamos configurar o Spring Security criando uma classe e desabilitando o proxy dos beans na anotação @Configuration(proxyBeanMethods = false), pois como vamos trabalhar com @Bean autocontido, podemos evitar o processamento da subclasse CGLIB.

Também vamos permitir os acessos às rotas de login e assets e desabilitar a proteção CSRF paras o métodos HTTP POST e HTTP DELETE nas instâncias dos clientes.

              
    package dev.marksduarte.sbaserver;

    ...

    @Configuration(proxyBeanMethods = false)
    public class SecurityConfig {

        private final AdminServerProperties adminServer;

        public SecurityConfig(AdminServerProperties adminServer) {
            this.adminServer = adminServer;
        }

        @Bean
        protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
            successHandler.setTargetUrlParameter("redirectTo");
            successHandler.setDefaultTargetUrl(this.adminServer.path("/"));

            http.authorizeHttpRequests(authorizeRequests -> authorizeRequests
                            .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/assets/**")))
                            .permitAll()
                            .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/login")))
                            .permitAll()
                            .anyRequest()
                            .authenticated())
                    .formLogin(formLogin -> formLogin.loginPage(this.adminServer.path("/login"))
                            .successHandler(successHandler))
                    .logout(logout -> logout.logoutUrl(this.adminServer.path("/logout")))
                    .httpBasic(Customizer.withDefaults())
                    .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                            .ignoringRequestMatchers(
                                    new AntPathRequestMatcher(this.adminServer.path("/instances"), HttpMethod.POST.toString()),
                                    new AntPathRequestMatcher(this.adminServer.path("/instances/*"), HttpMethod.DELETE.toString()),
                                    new AntPathRequestMatcher(this.adminServer.path("/actuator/**"))));

            return http.build();
        }
    }
              
            

Pronto, agora é só rodar a aplicação e conferir se o Admin Server está acessível através do endereço http://localhost:8081/admin-console e logar com o usuário e senha informados na configuração.

Configurando o Cliente

Crie um projeto utilizando o Spring Initilizr com as seguintes dependências:

  • Starter Web
  • Starter Security
  • Starter Actuator
  • Admin Starter Server
tela spring initializr

Confira no seu arquivo pom.xml, se a dependência do Spring Boot Admin Client e Spring Actuator foram adicionadas:

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

    <dependency>
        <groupId>de.codecentric</groupId>
        <artifactId>spring-boot-admin-starter-client</artifactId>
        <version>2.7.10</version>
    </dependency>
    ...
                
            

Agora vamos configurar o sistema começando pelo arquivo application.yaml:

                
    ## INFO ENDPOINT
    ## Aqui configuramos as informações sobre o sistema, como nome, descrição, versão e etc.
    info:
      name: Spring Boot Admin Client
      description: Sistema Cliente
      version: @project.version@

    server:
      port: 8080
      servlet:
        context-path: /admin-client

    spring:
      # Configuração básica do Spring Security.
      security:
        user:
          name: ${SBA_CLIENT_USERNAME}
          password: ${SBA_CLIENT_PASSWORD}
      boot:
        admin:
          client:
            enabled: true
            # URL do servidor que o cliente deve se registrar.
            url: http://localhost:8081/admin-console
            username: ${SBA_SERVER_USERNAME}
            password: ${SBA_SERVER_PASSWORD}
            instance:
              # URL base para calcular o service-url com o qual se registrar. O caminho é inferido em tempo de execução e anexado à url base.
              service-base-url: http://localhost:8080
              # Essas informações são passadas ao servidor para que ele possa fazer o acesso aos endpoints do sistema cliente.
              metadata:
                user:
                  name: ${SBA_SERVER_USERNAME}
                  password: ${SBA_SERVER_PASSWORD}
            auto-deregistration: true

    ## APP
    app:
      cors-origins:
        - http://localhost
      cors-methods:
        - GET
        - POST
        - PUT
        - DELETE
        - OPTIONS
      cors-headers:
        - Authorization
        - Content-Type
        - Content-Length
        - X-Requested-With

    ## ACTUATOR
    management:
      info:
        env:
          # Desde o Spring Boot 2.6, o env info é desabilitado por padrão.
          enabled: true
      endpoint:
        health:
          show-details: ALWAYS
          enabled: true
        shutdown:
          enabled: true
        logfile:
          enabled: true
          external-file: logs/sba-client.log
      endpoints:
        web:
          exposure:
            # Liberamos todos os endpoints, mas lembre-se, em produção não se deve fazer isso.
            include: "*"
          cors:
            allowed-headers: ${app.cors-headers}
            allowed-methods: ${app.cors-methods}
            allowed-origins: ${app.cors-origins}

    ## LOG
    logging:
      file:
        name: logs/sba-client.log
        path: logs
      level:
        root: info
        web: info
        dev.marksduarte: info
      charset:
        file: utf-8
      logback:
        rollingpolicy:
          clean-history-on-start: true
          max-file-size: 10MB

                
            

Para simplificar, vamos habilitar todas as requisições para os endpoints do "/actuator/**":

                
    package dev.marksduarte.sbaclient
    ...

    @EnableWebSecurity
    @Configuration
    public class SecurityConfig {

        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.csrf()
                    .disable()
                    .authorizeHttpRequests()
                    .antMatchers("/actuator/**")
                    .permitAll();
            return http.build();
        }
    }
                
            

Bom, isso já é suficiente para registar nossa aplicação cliente no servidor.

Mas caso aconteça alguma exceção do tipo: HttpMessageNotWritableException ou um response error HTTP 416 ao tentar acessar o arquivo de log, não se assuste, isso pode acontecer caso seu sistema tenha alguma classe de configuração do Jackson que estenda WebMvcConfigurationSupport.

Nesse caso, essa classe pode estar desativando a instanciação dos Beans com as configurações padrões do Spring Boot.

Para corrigir esse tipo de problema, podemos criar um Bean customizado e substituir a configuração padrão criada na inicialização do sistema.

                
    package dev.marksduarte.sbaclient
    ...
    @Configuration
    public class JacksonConfig extends WebMvcConfigurationSupport {

        private final Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();

        private static final List<MediaType> MEDIA_TYPE_LIST = List.of(
                MediaType.ALL,
                MediaType.parseMediaType("application/vnd.spring-boot.actuator.v2+json")
        );

        @Bean
        public MappingJackson2HttpMessageConverter customMappingJackson2HttpMessageConverter() {
            MappingJackson2HttpMessageConverter converter = actuatorConverter();
            converter.setSupportedMediaTypes(MEDIA_TYPE_LIST);
            return converter;
        }

        private MappingJackson2HttpMessageConverter actuatorConverter() {
            return new MappingJackson2HttpMessageConverter(builder.build()) {
                @Override
                protected boolean canWrite(MediaType mediaType) {
                    // O método super, retorna true se for null.
                    // Assim evitamos a exceção _HttpMessageNotWritableException_ caso o Content-Type 'null' seja enviado.
                    return mediaType != null && super.canWrite(mediaType);
                }
            };
        }

        @Override
        protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
            /*
            Remove somente o MappingJackson2HttpMessageConverter
    padrão e substitui pelo customMappingJackson2HttpMessageConverter.
             */
            var defaultHttpConverterOpt = converters.stream()
                    .filter(MappingJackson2HttpMessageConverter.class::isInstance)
                    .findFirst();

            defaultHttpConverterOpt.ifPresent(converters::remove);
            converters.add(customMappingJackson2HttpMessageConverter());
        }
    }
                
            
tela spring initializr tela spring initializr

Código disponível no Github.

Por enquanto é só. Até mais! ;)