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)