Spring Boot: Sessions actuator endpoint

This post shows how you can implement a custom Spring Boot Actuator endpoint that prints information about all active HttpSessions:

HttpSession meta data is prefixed with @ signs: id, creation time and last accessed time. The other values are a raw dump of all the HttpSession attributes.

You can use this endpoint during development to inspect active sessions. Or you can use it on production systems when troubleshooting customer issues. Whatever you choose, make sure that you secure such an endpoint appropriately: You could expose some very sensitive data.

For an introduction to custom Spring Boot Actuator endpoints, refer to my previous post: Spring Boot: Introduce your own insight endpoints.

Implementation

I have prepared a GitHub example based on Spring Boot 1.4 - find it here. Consult that to see the code in it’s entirety and full context. Here’s the main principles that makes this possible:

  • Create a class to act as a registry of all active HttpSessions
  • Create an HttpSession listener that registers and de-registers the HttpSessions
  • Create a Spring Boot Actuator endpoint that dumps the internal state of all of the HttpSessions

Step 1 of 3: Class SessionRegistry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class SessionRegistry {

    private final Map<String, HttpSession> httpSessionMap = new ConcurrentSkipListMap<>();

    public void addSession(HttpSession httpSession) {
        this.httpSessionMap.put(httpSession.getId(), httpSession);
    }

    public void removeSession(HttpSession httpSession) {
        this.httpSessionMap.remove(httpSession.getId());
    }

    public List<HttpSession> getSessions() {
        return new ArrayList<>(httpSessionMap.values());
    }
}

The httpSessionMap is a concurrent version of Map: to avoid ConcurrentModificationExceptions.

Step 2 of 3: Class SessionListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class SessionListener implements HttpSessionListener {

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        sessionRegistry.addSession(se.getSession());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        sessionRegistry.removeSession(se.getSession());
    }
}

This is a Spring’ified version of a traditional Servlet container component: HttpSessionListener. This class maintains the registry.

Step 3 of 3: Class SessionActuatorEndpoint

This is the interesting part - where we create a Spring Boot Actuator endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Component
public class SessionsActuatorEndpoint extends AbstractMvcEndpoint {

    @Autowired
    private SessionRegistry sessionRegistry;

    public SessionsActuatorEndpoint() {
        super("/sessions", true/*sensitive*/);
    }

    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public SessionStateActuatorEndpointResponse listActiveSessions() {
        return new SessionStateActuatorEndpointResponse("Active HTTP sessions", sessionRegistry.getSessions());
    }

    @JsonPropertyOrder({"info", "numberOfActiveSessions", "sessions"})
    public static class SessionStateActuatorEndpointResponse {

        @JsonProperty
        private String info;

        @JsonProperty
        private int numberOfActiveSessions;

        @JsonProperty
        private List<Map<String, String>> sessions;

        public SessionStateActuatorEndpointResponse(String info, List<HttpSession> httpSessions) {
            this.info = info;
            this.numberOfActiveSessions = httpSessions.size();
            this.sessions = httpSessions.stream()
                    .map(this::getSessionState)
                    .collect(Collectors.toList());
        }

        private Map<String, String> getSessionState(HttpSession httpSession) {
            Map<String, String> sessionState = new LinkedHashMap<>();
            sessionState.put("@session_id", httpSession.getId());
            sessionState.put("@session_creation_time", formatDateTime(httpSession.getCreationTime()));
            sessionState.put("@session_last_accessed_time", formatDateTime(httpSession.getLastAccessedTime()));
            for (Enumeration<String> attributeNames = httpSession.getAttributeNames(); attributeNames.hasMoreElements(); ) {
                String attributeName = attributeNames.nextElement();
                Object attributeValue = httpSession.getAttribute(attributeName);
                sessionState.put(attributeName, attributeValue instanceof String || attributeValue instanceof Number ?
                        attributeValue.toString() : ReflectionToStringBuilder.toString(attributeValue));
            }
            return sessionState;
        }

        private String formatDateTime(long epoch) {
            Instant instant = Instant.ofEpochMilli(epoch);
            ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
            return zonedDataTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
        }
    }

}

The thing that makes this a Spring Boot Actuator endpoint is that it declares AbstractMvcEndpoint as its superclass. Also take note of the constructor: this is where you register the path at which this endpoint can be found (/sessions).

The inner class SessionStateActuatorEndpointResponse represents the JSON response structure. I’ve selected some meta data that I find interesting and added that in the output - prefixed with ‘@’ characters. Other than that, it simply dumps all HttpSession attributes. Notice that it uses commons ReflectionToStringBuilder for object structures other than Strings and Numbers.