Simple Spring Boot Admin Setup

Spring Boot Admin is a cool dashboard for monitoring your spring boot applications. However, setting it up is not that trivial. The documentation outlines two options:

  • Including a client library in your boot application that connects to the admin application – this requires having the admin application deployed somewhere public or at least reachable from your application, and also making your application aware that it is being monitored.
  • Using cloud discovery, which means your application is part of a service discovery infrastructure, e.g. using microservices

Both are not very good options for simpler scenarios like a monolithic application being run on some IaaS and having your admin application deployed either on a local machine or in some local company infrastructure. Cloud discovery is an overkill if you don’t already need it, and including a client library introduces the complexity of making the admin server reachable by your application, rather than vice-versa. And besides, this two-way dependency sounds wrong.

Fortunately, there is an undocumented, but implemented SimpleDiscoveryClient that let’s you simply run the Spring Boot Admin with some configuration on whatever machine and connect it to your spring boot application.

The first requirement is to have the spring boot actuator setup in your boot application. The Actuator exposes all the needed endpoints for the admin application to work. It sounds trivial to setup – you just add a bunch of dependencies and possibly specify some config parameters and that’s it. In fact, in a real application it’s not that easy – in particular regarding the basic authentication for the actuator endpoints. You need a separate spring-security (in addition to your existing spring-security configuration) in order to apply basic auth only to the actuator endpoints. E.g.:

@Configuration
@Order(99) // the default security configuration has order 100
public class ActuatorSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${security.user.name}")
    private String username;
    
    @Value("${security.user.password}")
    private String password;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername(username).password(password).roles("ACTUATOR","ADMIN").build());
        
        http.csrf().disable().authorizeRequests()
            .antMatchers("/manage/**").hasRole("ACTUATOR").and().httpBasic().and().userDetailsService(manager);
    }
}

This is a bit counterintuitive, but it works. Not sure if it’s idiomatic – with spring security and spring boot you never know what is idiomatic. Note – allegedly it should be possible to have the security.user.name (and password) automatically included in some manager, but I failed to find such, so I just instantiated an in-memory one. Note the /manage/** path – in order to have all the actuator endpoints under that path, you need to specify the management.context-path=/manage in your application properties file.

Now that the actuator endpoints are setup, we have to attach our spring admin application. It looks like that:

@Configuration
@EnableAutoConfiguration
@PropertySource("classpath:/application.properties")
@EnableAdminServer
public class BootAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(BootAdminApplication.class, args);
    }

    @Autowired
    private ApplicationDiscoveryListener listener;
    
    @PostConstruct
    public void init() {
        // we have to fire this event in order to trigger the service registration
        InstanceRegisteredEvent<?> event = new InstanceRegisteredEvent<>("prod", null);
        // for some reason publising doesn't work, so we invoke directly
        listener.onInstanceRegistered(event);
    }
}

Normally, one should inject ApplicationEventPublisher and push the message there rather than directly invoking the listener as shown above. I didn’t manage to get it working easily, so I worked around that.

The application.properties file mentioned about should be in src/main/resources and looks like that:

spring.cloud.discovery.client.simple.instances.prod[0].uri=https://your-spring-boot-application-url.com
spring.cloud.discovery.client.simple.instances.prod[0].metadata.user.name=<basic-auth-username>
spring.cloud.discovery.client.simple.instances.prod[0].metadata.user.password=<basic-auth-password>
spring.boot.admin.discovery.converter.management-context-path=/manage
spring.boot.admin.discovery.services=*

What is that doing? It’s using the SimpleDiscoveryClient that gets instantiated by the autoconfiguration. Actually, that client didn’t work until the latest version – it threw NullPointerException because the metadata (which handles the username and password) was always null. In 1.2.2 of the cloud-commons they fixed it:

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-commons</artifactId>
	<version>1.2.2.RELEASE</version>
</dependency>

The simple discovery client is exactly that – you specify the URL of your boot application and it fetches the data form the actuator endpoints periodically. Why that isn’t documented and why it didn’t actually work until very recently – I have no idea. Also, I don’t know why you have to manually send the event that triggers discovery. Maybe it’s not idiomatic, but it doesn’t happen automatically and that made it work.

As usual with things that “just work” and have “simple setups” – it’s never like that. If you have something slightly more complex than a hello world, you have to dig some obscure classes and go “off-road”. Luckily in this case, it actually works, rather than needed ugly workarounds.

The post Simple Spring Boot Admin Setup appeared first on Bozho's tech blog.