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
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.springbootadminserver; import de.codecentric.boot.admin.server.config.AdminServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @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 Actuator
- Starter Security
- Admin Starter Client
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/**":
... @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.
@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()); } }
Código disponível em GitHub Marks Duarte
Por enquanto é só. Até mais! ;)
Top comments (0)