Skip to content

Commit 0be94e9

Browse files
committed
Fix #25811: next-generation metrics: absolute users count
1 parent 4b780a4 commit 0be94e9

6 files changed

Lines changed: 345 additions & 0 deletions

File tree

src/main/java/eu/openanalytics/containerproxy/ContainerProxyApplication.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import eu.openanalytics.containerproxy.util.ProxyMappingManager;
2626
import io.undertow.Handlers;
2727
import io.undertow.servlet.api.ServletSessionConfig;
28+
import io.undertow.servlet.api.SessionManagerFactory;
2829
import org.apache.logging.log4j.LogManager;
2930
import org.apache.logging.log4j.Logger;
31+
import org.springframework.beans.factory.annotation.Autowired;
3032
import org.springframework.boot.SpringApplication;
3133
import org.springframework.boot.actuate.health.Health;
3234
import org.springframework.boot.actuate.health.HealthIndicator;
@@ -108,6 +110,9 @@ public void init() {
108110
defaultCookieSerializer.setSameSite(sameSiteCookie);
109111
}
110112

113+
@Autowired(required = false)
114+
private SessionManagerFactory sessionManagerFactory;
115+
111116
@Bean
112117
public UndertowServletWebServerFactory servletContainer() {
113118
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
@@ -123,6 +128,9 @@ public UndertowServletWebServerFactory servletContainer() {
123128
sessionConfig.setHttpOnly(true);
124129
sessionConfig.setSecure(Boolean.valueOf(environment.getProperty("server.secureCookies", "false")));
125130
info.setServletSessionConfig(sessionConfig);
131+
if (sessionManagerFactory != null) {
132+
info.setSessionManagerFactory(sessionManagerFactory);
133+
}
126134
});
127135
try {
128136
factory.setAddress(InetAddress.getByName(environment.getProperty("proxy.bind-address", "0.0.0.0")));
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.service.session;
22+
23+
/**
24+
* Service to manage/query the session of users.
25+
*/
26+
public interface ISessionService {
27+
28+
public Integer getLoggedInUsersCount();
29+
30+
// public void reActivateSession(String sessionId);
31+
32+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.service.session.redis;
22+
23+
import eu.openanalytics.containerproxy.service.session.ISessionService;
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27+
import org.springframework.boot.autoconfigure.session.RedisSessionProperties;
28+
import org.springframework.data.redis.core.RedisTemplate;
29+
import org.springframework.security.core.session.SessionInformation;
30+
import org.springframework.security.core.session.SessionRegistry;
31+
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
32+
import org.springframework.stereotype.Component;
33+
34+
import javax.annotation.PostConstruct;
35+
import javax.inject.Inject;
36+
import java.util.Objects;
37+
import java.util.Set;
38+
import java.util.Timer;
39+
import java.util.TimerTask;
40+
import java.util.regex.Matcher;
41+
import java.util.regex.Pattern;
42+
import java.util.stream.Collectors;
43+
44+
@Component
45+
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis")
46+
public class RedisSessionService implements ISessionService {
47+
48+
private static final Pattern SESSION_ID_PATTERN = Pattern.compile("^.*sessions:([a-z0-9-]*)$");
49+
private static final int CACHE_UPDATE_INTERVAL = 60 * 1000; // update cache every minutes
50+
51+
private final Log logger = LogFactory.getLog(RedisSessionService.class);
52+
53+
@Inject
54+
private RedisIndexedSessionRepository redisIndexedSessionRepository;
55+
56+
@Inject
57+
private SessionRegistry sessionRegistry;
58+
59+
@Inject
60+
private RedisSessionProperties redisSessionProperties;
61+
62+
private String keyPattern;
63+
private RedisTemplate<Object, Object> redisTemplate;
64+
65+
private Integer cachedUsersLoggedInCount = null; // default value;
66+
67+
@PostConstruct
68+
public void init() {
69+
keyPattern = redisSessionProperties.getNamespace() + ":sessions:*";
70+
redisTemplate = (RedisTemplate<Object, Object>) redisIndexedSessionRepository.getSessionRedisOperations();
71+
new Timer().schedule(new TimerTask() {
72+
@Override
73+
public void run() {
74+
updateCachedUsersLoggedInCount();
75+
}
76+
}, 0, CACHE_UPDATE_INTERVAL);
77+
}
78+
79+
@Override
80+
public Integer getLoggedInUsersCount() {
81+
return cachedUsersLoggedInCount;
82+
}
83+
84+
/**
85+
* Updates the cached count of users.
86+
* We only update this value every CACHE_UPDATE_INTERVAL because this is a relative heavy computation to do.
87+
* Therefore we don't want that this calculation is performed every time
88+
* {@link RedisSessionService#getLoggedInUsersCount()} is called. Especially since this function could be called
89+
* using an HTTP request.
90+
* See the warning at https://redis.io/commands/keys .
91+
*/
92+
private void updateCachedUsersLoggedInCount() {
93+
Set<Object> keys = redisTemplate.keys(keyPattern);
94+
95+
if (keys == null) {
96+
return;
97+
}
98+
99+
System.out.println(keys);
100+
101+
Set<String> authenticatedUsers = keys
102+
.stream()
103+
.map((keyId) -> {
104+
String sessionId = extractSessionId(keyId);
105+
if (sessionId != null) {
106+
SessionInformation sessionInformation = sessionRegistry.getSessionInformation(sessionId);
107+
if (sessionInformation != null) {
108+
return sessionInformation.getPrincipal().toString();
109+
}
110+
}
111+
return null;
112+
})
113+
.filter(Objects::nonNull)
114+
.collect(Collectors.toSet());
115+
116+
logger.debug(String.format("Logged in users count %s, all users: %s ", authenticatedUsers.size(), authenticatedUsers));
117+
cachedUsersLoggedInCount = authenticatedUsers.size();
118+
}
119+
120+
/**
121+
* Extracts the sessionId from the Redis Key
122+
*/
123+
private String extractSessionId(Object keyId) {
124+
if (keyId instanceof String) {
125+
Matcher matcher = SESSION_ID_PATTERN.matcher((String) keyId);
126+
127+
if (matcher.matches()) {
128+
return matcher.group(1);
129+
}
130+
}
131+
return null;
132+
}
133+
134+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.service.session.undertow;
22+
23+
import io.undertow.server.session.InMemorySessionManager;
24+
import io.undertow.server.session.SessionManager;
25+
import io.undertow.servlet.api.Deployment;
26+
import io.undertow.servlet.api.SessionManagerFactory;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
28+
import org.springframework.stereotype.Component;
29+
30+
@Component
31+
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "none")
32+
public class CustomSessionManagerFactory implements SessionManagerFactory {
33+
34+
private InMemorySessionManager inMemorySessionManager = null;
35+
36+
@Override
37+
public SessionManager createSessionManager(Deployment deployment) {
38+
if (inMemorySessionManager == null) {
39+
inMemorySessionManager = new InMemorySessionManager(
40+
deployment.getDeploymentInfo().getSessionIdGenerator(),
41+
deployment.getDeploymentInfo().getDeploymentName(),
42+
-1,
43+
false,
44+
deployment.getDeploymentInfo().getMetricsCollector() != null);
45+
}
46+
return inMemorySessionManager;
47+
}
48+
49+
public InMemorySessionManager getInstance() {
50+
return inMemorySessionManager;
51+
}
52+
53+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2021 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.service.session.undertow;
22+
23+
import eu.openanalytics.containerproxy.service.session.ISessionService;
24+
import io.undertow.server.session.InMemorySessionManager;
25+
import io.undertow.server.session.Session;
26+
import org.apache.commons.logging.Log;
27+
import org.apache.commons.logging.LogFactory;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
29+
import org.springframework.security.core.context.SecurityContext;
30+
import org.springframework.stereotype.Component;
31+
32+
import javax.annotation.PostConstruct;
33+
import javax.inject.Inject;
34+
import java.util.Objects;
35+
import java.util.Set;
36+
import java.util.Timer;
37+
import java.util.TimerTask;
38+
import java.util.stream.Collectors;
39+
40+
@Component
41+
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "none")
42+
public class UndertowSessionService implements ISessionService {
43+
44+
private static final int CACHE_UPDATE_INTERVAL = 60 * 1000; // update cache every minutes
45+
46+
private final Log logger = LogFactory.getLog(UndertowSessionService.class);
47+
48+
@Inject
49+
private CustomSessionManagerFactory customInMemorySessionManagerFactory;
50+
51+
// default value, note we cannot use 0 or -1 here as that would cause a dip when restarting ShinyProxy
52+
private Integer cachedUsersLoggedInCount = null;
53+
54+
@PostConstruct
55+
public void init() {
56+
new Timer().schedule(new TimerTask() {
57+
@Override
58+
public void run() {
59+
updateCachedUsersLoggedInCount();
60+
}
61+
}, 0, CACHE_UPDATE_INTERVAL);
62+
}
63+
64+
@Override
65+
public Integer getLoggedInUsersCount() {
66+
return cachedUsersLoggedInCount;
67+
}
68+
69+
/**
70+
* Updates the cached count of users.
71+
* We only update this value every CACHE_UPDATE_INTERVAL because this is a relative heavy computation to do.
72+
* Therefore we don't want that this calculation is performed every time
73+
* {@link UndertowSessionService#getLoggedInUsersCount()} is called. Especially since this function could be called
74+
* using an HTTP request.
75+
* This function does not use external databases (in contrast to
76+
* {@link eu.openanalytics.containerproxy.service.session.redis.RedisSessionService}, but still it needs to loop
77+
* over all sessions in the Servlet).
78+
*/
79+
private void updateCachedUsersLoggedInCount() {
80+
InMemorySessionManager instance = this.customInMemorySessionManagerFactory.getInstance();
81+
if (instance == null) {
82+
return;
83+
}
84+
85+
Set<String> authenticatedUsers = instance
86+
.getAllSessions()
87+
.stream()
88+
.map((sessionId) -> {
89+
Session sessionImpl = instance.getSession(sessionId);
90+
if (sessionImpl == null) return null;
91+
Object object = sessionImpl.getAttribute("SPRING_SECURITY_CONTEXT");
92+
if (object instanceof SecurityContext) {
93+
SecurityContext securityContext = (SecurityContext) object;
94+
if (securityContext.getAuthentication().isAuthenticated()) {
95+
return securityContext.getAuthentication().getName();
96+
}
97+
}
98+
return null;
99+
})
100+
.filter(Objects::nonNull)
101+
.collect(Collectors.toSet());
102+
103+
logger.debug(String.format("Logged in users count %s, all users: %s ", authenticatedUsers.size(), authenticatedUsers));
104+
cachedUsersLoggedInCount = authenticatedUsers.size();
105+
}
106+
107+
108+
}

0 commit comments

Comments
 (0)