Skip to content

Commit 297e675

Browse files
committed
Add actuator endpoint for finding and deleting sessions
1 parent e3a9d76 commit 297e675

File tree

15 files changed

+725
-5
lines changed

15 files changed

+725
-5
lines changed

spring-boot-actuator-docs/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@
9898
<artifactId>spring-restdocs-mockmvc</artifactId>
9999
<scope>provided</scope>
100100
</dependency>
101+
<dependency>
102+
<groupId>org.springframework.session</groupId>
103+
<artifactId>spring-session-core</artifactId>
104+
<scope>provided</scope>
105+
</dependency>
101106
<dependency>
102107
<groupId>org.yaml</groupId>
103108
<artifactId>snakeyaml</artifactId>

spring-boot-actuator-docs/src/main/asciidoc/index.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,44 @@ include::{generated}/partial-logfile/http-response.adoc[]
7777

7878

7979

80+
=== /sessions
81+
This endpoint allows retrieving the sessions from Spring Session backed session store, and
82+
deleting them.
83+
Sessions can be retrieved by username, or by session id, and deleted by session id.
84+
85+
86+
==== Finding sessions for given username
87+
88+
Example cURL request:
89+
include::{generated}/sessions/curl-request.adoc[]
90+
91+
Example HTTP request:
92+
include::{generated}/sessions/http-request.adoc[]
93+
94+
Example HTTP response:
95+
include::{generated}/sessions/http-response.adoc[]
96+
97+
==== Getting a single session
98+
99+
Example cURL request:
100+
include::{generated}/sessions/get-session/curl-request.adoc[]
101+
102+
Example HTTP request:
103+
include::{generated}/sessions/get-session/http-request.adoc[]
104+
105+
Example HTTP response:
106+
include::{generated}/sessions/get-session/http-response.adoc[]
107+
108+
==== Deleting the session
109+
110+
Example cURL request:
111+
include::{generated}/sessions/delete-session/curl-request.adoc[]
112+
113+
Example HTTP request:
114+
include::{generated}/sessions/delete-session/http-request.adoc[]
115+
116+
117+
80118
=== /docs
81119
This endpoint (if available) contains HTML documentation for the other endpoints. Its path
82120
can be "/docs" (if there is an existing home page) or "/" (otherwise, including if the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.hypermedia;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.util.Collections;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
26+
import org.springframework.session.FindByIndexNameSessionRepository;
27+
import org.springframework.session.Session;
28+
29+
/**
30+
* {@link FindByIndexNameSessionRepository} implementation for documentation purposes.
31+
*
32+
* @author Vedran Pavic
33+
*/
34+
public class DocumentationSessionRepository
35+
implements FindByIndexNameSessionRepository<Session> {
36+
37+
static final String MOCK_SESSION_ID = "6993c49c-6d29-4117-9242-802b8185470e";
38+
39+
static final String MOCK_USER = "user";
40+
41+
private static final Map<String, Session> sessions = Collections
42+
.singletonMap(MOCK_SESSION_ID, new DocumentationSession());
43+
44+
@Override
45+
public Session createSession() {
46+
return null;
47+
}
48+
49+
@Override
50+
public void save(Session expiringSession) {
51+
52+
}
53+
54+
@Override
55+
public Session findById(String id) {
56+
return sessions.get(id);
57+
}
58+
59+
@Override
60+
public void deleteById(String id) {
61+
62+
}
63+
64+
@Override
65+
public Map<String, Session> findByIndexNameAndIndexValue(String indexName,
66+
String indexValue) {
67+
if (PRINCIPAL_NAME_INDEX_NAME.equals(indexName) && MOCK_USER.equals(indexValue)) {
68+
return sessions;
69+
}
70+
return Collections.emptyMap();
71+
}
72+
73+
private static class DocumentationSession implements Session {
74+
75+
@Override
76+
public Instant getCreationTime() {
77+
return Instant.ofEpochMilli(1487617610119L);
78+
}
79+
80+
@Override
81+
public Instant getLastAccessedTime() {
82+
return Instant.ofEpochMilli(1487617610119L);
83+
}
84+
85+
@Override
86+
public void setLastAccessedTime(Instant lastAccessedTime) {
87+
88+
}
89+
90+
@Override
91+
public Duration getMaxInactiveInterval() {
92+
return Duration.ofSeconds(1800);
93+
}
94+
95+
@Override
96+
public void setMaxInactiveInterval(Duration interval) {
97+
98+
}
99+
100+
@Override
101+
public boolean isExpired() {
102+
return false;
103+
}
104+
105+
@Override
106+
public String getId() {
107+
return MOCK_SESSION_ID;
108+
}
109+
110+
@Override
111+
public <T> Optional<T> getAttribute(String attributeName) {
112+
return Optional.empty();
113+
}
114+
115+
@Override
116+
public Set<String> getAttributeNames() {
117+
return Collections.singleton("principal");
118+
}
119+
120+
@Override
121+
public void setAttribute(String attributeName, Object attributeValue) {
122+
123+
}
124+
125+
@Override
126+
public void removeAttribute(String attributeName) {
127+
128+
}
129+
130+
}
131+
132+
}

spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.springframework.util.StringUtils;
5959

6060
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
61+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
6162
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6263
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
6364
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -67,7 +68,7 @@
6768
@WebAppConfiguration
6869
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
6970
"endpoints.health.sensitive=true", "endpoints.actuator.enabled=false",
70-
"management.security.enabled=false" })
71+
"endpoints.sessions.enabled=true", "management.security.enabled=false" })
7172
@DirtiesContext
7273
@AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR)
7374
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
@@ -78,8 +79,8 @@ public class EndpointDocumentation {
7879
static final File LOG_FILE = new File("target/logs/spring.log");
7980

8081
private static final Set<String> SKIPPED = Collections
81-
.<String>unmodifiableSet(new HashSet<>(
82-
Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents")));
82+
.<String>unmodifiableSet(new HashSet<>(Arrays.asList("/docs", "/logfile",
83+
"/heapdump", "/auditevents", "/sessions")));
8384

8485
@Autowired
8586
private MvcEndpoints mvcEndpoints;
@@ -159,6 +160,34 @@ public void auditEventsByPrincipalAndType() throws Exception {
159160
.andDo(document("auditevents/filter-by-principal-and-type"));
160161
}
161162

163+
@Test
164+
public void findSessionsByUsername() throws Exception {
165+
this.mockMvc
166+
.perform(get("/application/sessions")
167+
.param("username", DocumentationSessionRepository.MOCK_USER)
168+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
169+
.andExpect(status().isOk()).andDo(document("sessions"));
170+
}
171+
172+
@Test
173+
public void getSession() throws Exception {
174+
this.mockMvc
175+
.perform(get("/application/sessions/"
176+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
177+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
178+
.andExpect(status().isOk()).andDo(document("sessions/get-session"));
179+
}
180+
181+
@Test
182+
public void deleteSession() throws Exception {
183+
this.mockMvc
184+
.perform(delete("/application/sessions/"
185+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
186+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
187+
.andExpect(status().isNoContent())
188+
.andDo(document("sessions/delete-session"));
189+
}
190+
162191
@Test
163192
public void endpoints() throws Exception {
164193
final File docs = new File("src/main/asciidoc");

spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/SpringBootHypermediaApplication.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public EnvironmentEndpoint environmentEndpoint() {
5353
return new LimitedEnvironmentEndpoint();
5454
}
5555

56+
@Bean
57+
public DocumentationSessionRepository sessionRepository() {
58+
return new DocumentationSessionRepository();
59+
}
60+
5661
public static void main(String[] args) {
5762
SpringApplication.run(SpringBootHypermediaApplication.class, args);
5863
}

spring-boot-actuator/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
<groupId>com.fasterxml.jackson.core</groupId>
3434
<artifactId>jackson-databind</artifactId>
3535
</dependency>
36+
<dependency>
37+
<groupId>com.fasterxml.jackson.datatype</groupId>
38+
<artifactId>jackson-datatype-jsr310</artifactId>
39+
</dependency>
3640
<dependency>
3741
<groupId>org.springframework</groupId>
3842
<artifactId>spring-core</artifactId>
@@ -226,6 +230,11 @@
226230
<artifactId>spring-security-config</artifactId>
227231
<optional>true</optional>
228232
</dependency>
233+
<dependency>
234+
<groupId>org.springframework.session</groupId>
235+
<artifactId>spring-session-core</artifactId>
236+
<optional>true</optional>
237+
</dependency>
229238
<dependency>
230239
<groupId>org.apache.tomcat.embed</groupId>
231240
<artifactId>tomcat-embed-core</artifactId>

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointMBeanExportAutoConfiguration.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
import org.springframework.boot.actuate.endpoint.Endpoint;
2828
import org.springframework.boot.actuate.endpoint.jmx.AuditEventsJmxEndpoint;
2929
import org.springframework.boot.actuate.endpoint.jmx.EndpointMBeanExporter;
30+
import org.springframework.boot.actuate.endpoint.jmx.SessionsJmxEndpoint;
3031
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
3132
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
3233
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
3334
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
3435
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
3536
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
37+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
3638
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
3739
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
3840
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -41,6 +43,8 @@
4143
import org.springframework.context.annotation.Conditional;
4244
import org.springframework.context.annotation.Configuration;
4345
import org.springframework.core.type.AnnotatedTypeMetadata;
46+
import org.springframework.session.FindByIndexNameSessionRepository;
47+
import org.springframework.session.Session;
4448
import org.springframework.util.StringUtils;
4549

4650
/**
@@ -50,6 +54,7 @@
5054
* @author Christian Dupuis
5155
* @author Andy Wilkinson
5256
* @author Madhura Bhave
57+
* @author Vedran Pavic
5358
*/
5459
@Configuration
5560
@Conditional(JmxEnabledCondition.class)
@@ -95,6 +100,21 @@ public AuditEventsJmxEndpoint auditEventsEndpoint(
95100
return new AuditEventsJmxEndpoint(this.objectMapper, auditEventRepository);
96101
}
97102

103+
@Configuration
104+
@ConditionalOnSingleCandidate(FindByIndexNameSessionRepository.class)
105+
@ConditionalOnEnabledEndpoint(value = "sessions", enabledByDefault = false)
106+
protected class SessionsJmxEndpointConfiguration {
107+
108+
@Bean
109+
public SessionsJmxEndpoint sessionsEndpoint(
110+
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
111+
return new SessionsJmxEndpoint(
112+
EndpointMBeanExportAutoConfiguration.this.objectMapper,
113+
sessionRepository);
114+
}
115+
116+
}
117+
98118
/**
99119
* Condition to check that spring.jmx and endpoints.jmx are enabled.
100120
*/

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,23 @@
4141
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
4242
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpointSecurityInterceptor;
4343
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
44+
import org.springframework.boot.actuate.endpoint.mvc.SessionsMvcEndpoint;
4445
import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint;
4546
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
4647
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
4748
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
4849
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
50+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
4951
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
5052
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5153
import org.springframework.context.annotation.Bean;
5254
import org.springframework.context.annotation.ConditionContext;
5355
import org.springframework.context.annotation.Conditional;
56+
import org.springframework.context.annotation.Configuration;
5457
import org.springframework.core.env.Environment;
5558
import org.springframework.core.type.AnnotatedTypeMetadata;
59+
import org.springframework.session.FindByIndexNameSessionRepository;
60+
import org.springframework.session.Session;
5661
import org.springframework.util.CollectionUtils;
5762
import org.springframework.util.StringUtils;
5863
import org.springframework.web.cors.CorsConfiguration;
@@ -208,6 +213,19 @@ public AuditEventsMvcEndpoint auditEventMvcEndpoint(
208213
return new AuditEventsMvcEndpoint(auditEventRepository);
209214
}
210215

216+
@Configuration
217+
@ConditionalOnSingleCandidate(FindByIndexNameSessionRepository.class)
218+
@ConditionalOnEnabledEndpoint(value = "sessions", enabledByDefault = false)
219+
protected static class SessionsMvcEndpointConfiguration {
220+
221+
@Bean
222+
public SessionsMvcEndpoint sessionsMvcEndpoint(
223+
FindByIndexNameSessionRepository<? extends Session> sessionRepository) {
224+
return new SessionsMvcEndpoint(sessionRepository);
225+
}
226+
227+
}
228+
211229
private static class LogFileCondition extends SpringBootCondition {
212230

213231
@Override

0 commit comments

Comments
 (0)