Skip to content

Commit c8599f7

Browse files
committed
Add actuator endpoint for finding and deleting sessions
1 parent eac4c7e commit c8599f7

File tree

15 files changed

+727
-5
lines changed

15 files changed

+727
-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,136 @@
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.Set;
24+
25+
import org.springframework.session.FindByIndexNameSessionRepository;
26+
import org.springframework.session.Session;
27+
28+
/**
29+
* {@link FindByIndexNameSessionRepository} implementation for documentation purposes.
30+
*
31+
* @author Vedran Pavic
32+
*/
33+
public class DocumentationSessionRepository
34+
implements FindByIndexNameSessionRepository<Session> {
35+
36+
static final String MOCK_SESSION_ID = "6993c49c-6d29-4117-9242-802b8185470e";
37+
38+
static final String MOCK_USER = "user";
39+
40+
private static final Map<String, Session> sessions = Collections
41+
.singletonMap(MOCK_SESSION_ID, new DocumentationSession());
42+
43+
@Override
44+
public Session createSession() {
45+
return null;
46+
}
47+
48+
@Override
49+
public void save(Session expiringSession) {
50+
51+
}
52+
53+
@Override
54+
public Session findById(String id) {
55+
return sessions.get(id);
56+
}
57+
58+
@Override
59+
public void deleteById(String id) {
60+
61+
}
62+
63+
@Override
64+
public Map<String, Session> findByIndexNameAndIndexValue(String indexName,
65+
String indexValue) {
66+
if (PRINCIPAL_NAME_INDEX_NAME.equals(indexName) && MOCK_USER.equals(indexValue)) {
67+
return sessions;
68+
}
69+
return Collections.emptyMap();
70+
}
71+
72+
private static class DocumentationSession implements Session {
73+
74+
@Override
75+
public String getId() {
76+
return MOCK_SESSION_ID;
77+
}
78+
79+
@Override
80+
public String changeSessionId() {
81+
return MOCK_SESSION_ID;
82+
}
83+
84+
@Override
85+
public <T> T getAttribute(String attributeName) {
86+
return null;
87+
}
88+
89+
@Override
90+
public Set<String> getAttributeNames() {
91+
return Collections.singleton("principal");
92+
}
93+
94+
@Override
95+
public void setAttribute(String attributeName, Object attributeValue) {
96+
97+
}
98+
99+
@Override
100+
public void removeAttribute(String attributeName) {
101+
102+
}
103+
104+
@Override
105+
public Instant getCreationTime() {
106+
return Instant.ofEpochMilli(1487617610119L);
107+
}
108+
109+
@Override
110+
public void setLastAccessedTime(Instant lastAccessedTime) {
111+
112+
}
113+
114+
@Override
115+
public Instant getLastAccessedTime() {
116+
return Instant.ofEpochMilli(1487617610119L);
117+
}
118+
119+
@Override
120+
public void setMaxInactiveInterval(Duration interval) {
121+
122+
}
123+
124+
@Override
125+
public Duration getMaxInactiveInterval() {
126+
return Duration.ofSeconds(1800);
127+
}
128+
129+
@Override
130+
public boolean isExpired() {
131+
return false;
132+
}
133+
134+
}
135+
136+
}

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
@@ -56,6 +56,7 @@
5656
import org.springframework.util.StringUtils;
5757

5858
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
59+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
5960
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
6061
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
6162
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -65,7 +66,7 @@
6566
@WebAppConfiguration
6667
@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true",
6768
"endpoints.health.sensitive=true", "endpoints.actuator.enabled=false",
68-
"management.security.enabled=false" })
69+
"endpoints.sessions.enabled=true", "management.security.enabled=false" })
6970
@DirtiesContext
7071
@AutoConfigureRestDocs(EndpointDocumentation.RESTDOCS_OUTPUT_DIR)
7172
@AutoConfigureMockMvc(print = MockMvcPrint.NONE)
@@ -76,8 +77,8 @@ public class EndpointDocumentation {
7677
static final File LOG_FILE = new File("target/logs/spring.log");
7778

7879
private static final Set<String> SKIPPED = Collections
79-
.<String>unmodifiableSet(new HashSet<>(
80-
Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents")));
80+
.<String>unmodifiableSet(new HashSet<>(Arrays.asList("/docs", "/logfile",
81+
"/heapdump", "/auditevents", "/sessions")));
8182

8283
@Autowired
8384
private MvcEndpoints mvcEndpoints;
@@ -157,6 +158,34 @@ public void auditEventsByPrincipalAndType() throws Exception {
157158
.andDo(document("auditevents/filter-by-principal-and-type"));
158159
}
159160

161+
@Test
162+
public void findSessionsByUsername() throws Exception {
163+
this.mockMvc
164+
.perform(get("/application/sessions")
165+
.param("username", DocumentationSessionRepository.MOCK_USER)
166+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
167+
.andExpect(status().isOk()).andDo(document("sessions"));
168+
}
169+
170+
@Test
171+
public void getSession() throws Exception {
172+
this.mockMvc
173+
.perform(get("/application/sessions/"
174+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
175+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
176+
.andExpect(status().isOk()).andDo(document("sessions/get-session"));
177+
}
178+
179+
@Test
180+
public void deleteSession() throws Exception {
181+
this.mockMvc
182+
.perform(delete("/application/sessions/"
183+
+ DocumentationSessionRepository.MOCK_SESSION_ID)
184+
.accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON))
185+
.andExpect(status().isNoContent())
186+
.andDo(document("sessions/delete-session"));
187+
}
188+
160189
@Test
161190
public void endpoints() throws Exception {
162191
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>
@@ -128,6 +132,11 @@
128132
<artifactId>commons-dbcp2</artifactId>
129133
<optional>true</optional>
130134
</dependency>
135+
<dependency>
136+
<groupId>org.springframework.session</groupId>
137+
<artifactId>spring-session-core</artifactId>
138+
<optional>true</optional>
139+
</dependency>
131140
<dependency>
132141
<groupId>org.apache.tomcat.embed</groupId>
133142
<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)