Skip to content

Commit cde09e8

Browse files
committed
Add shinyproxy_absolute_users_active metric
1 parent 5d03377 commit cde09e8

4 files changed

Lines changed: 117 additions & 44 deletions

File tree

src/main/java/eu/openanalytics/containerproxy/service/session/ISessionService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public interface ISessionService {
2727

2828
public Integer getLoggedInUsersCount();
2929

30+
public Integer getActiveUsersCount();
31+
3032
// public void reActivateSession(String sessionId);
3133

3234
}

src/main/java/eu/openanalytics/containerproxy/service/session/redis/RedisSessionService.java

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@
2626
import org.apache.commons.logging.LogFactory;
2727
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2828
import org.springframework.data.redis.core.RedisTemplate;
29+
import org.springframework.security.core.Authentication;
2930
import org.springframework.security.core.context.SecurityContext;
3031
import org.springframework.session.Session;
3132
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
3233
import org.springframework.stereotype.Component;
3334

3435
import javax.annotation.PostConstruct;
3536
import javax.inject.Inject;
37+
import java.time.Instant;
38+
import java.util.HashSet;
3639
import java.util.Objects;
3740
import java.util.Set;
3841
import java.util.Timer;
@@ -43,10 +46,10 @@
4346

4447
@Component
4548
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "redis")
46-
public class RedisSessionService implements ISessionService {
49+
public class RedisSessionService implements ISessionService {
4750

4851
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
52+
private static final int CACHE_UPDATE_INTERVAL = 20 * 1000; // update cache every minutes
5053

5154
private final Log logger = LogFactory.getLog(RedisSessionService.class);
5255

@@ -60,6 +63,7 @@ public class RedisSessionService implements ISessionService {
6063
private RedisTemplate<Object, Object> redisTemplate;
6164

6265
private Integer cachedUsersLoggedInCount = null; // default value;
66+
private Integer cachedActiveUsersCount = null; // default value;
6367

6468
@PostConstruct
6569
public void init() {
@@ -78,6 +82,11 @@ public Integer getLoggedInUsersCount() {
7882
return cachedUsersLoggedInCount;
7983
}
8084

85+
@Override
86+
public Integer getActiveUsersCount() {
87+
return cachedActiveUsersCount;
88+
}
89+
8190
/**
8291
* Updates the cached count of users.
8392
* We only update this value every CACHE_UPDATE_INTERVAL because this is a relative heavy computation to do.
@@ -93,30 +102,33 @@ private void updateCachedUsersLoggedInCount() {
93102
return;
94103
}
95104

96-
Set<String> authenticatedUsers = keys
97-
.stream()
98-
.map((keyId) -> {
99-
String sessionId = extractSessionId(keyId);
100-
if (sessionId != null) {
101-
logger.debug(String.format("Extracted sessionId %s ", sessionId));
102-
Session session = redisIndexedSessionRepository.findById(sessionId);
103-
if (session != null) {
104-
Object object = session.getAttribute("SPRING_SECURITY_CONTEXT");
105-
if (object instanceof SecurityContext) {
106-
SecurityContext securityContext = (SecurityContext) object;
107-
if (securityContext.getAuthentication().isAuthenticated()) {
108-
return securityContext.getAuthentication().getName();
109-
}
110-
}
111-
}
112-
}
113-
return null;
114-
})
115-
.filter(Objects::nonNull)
116-
.collect(Collectors.toSet());
105+
// users are only counted as active, if they are active after this time.
106+
Instant minimumInstantTime = Instant.now().minusSeconds(60);
107+
108+
Set<String> authenticatedUsers = new HashSet<>();
109+
Set<String> activeUsers = new HashSet<>();
110+
111+
for (Object keyId : keys) {
112+
String sessionId = extractSessionId(keyId);
113+
if (sessionId == null) continue;
114+
115+
Session session = redisIndexedSessionRepository.findById(sessionId);
116+
if (session == null) continue;
117+
118+
Authentication authentication = extractAuthenticationIfAuthenticated(session);
119+
if (authentication == null) continue;
120+
121+
authenticatedUsers.add(authentication.getName());
122+
123+
if (session.getLastAccessedTime().isAfter(minimumInstantTime)) {
124+
activeUsers.add(authentication.getName());
125+
}
126+
}
117127

118128
logger.debug(String.format("Logged in users count %s, all users: %s ", authenticatedUsers.size(), authenticatedUsers));
119129
cachedUsersLoggedInCount = authenticatedUsers.size();
130+
logger.debug(String.format("Active users count %s, %s ", activeUsers.size(), activeUsers));
131+
cachedActiveUsersCount = activeUsers.size();
120132
}
121133

122134
/**
@@ -133,4 +145,24 @@ private String extractSessionId(Object keyId) {
133145
return null;
134146
}
135147

148+
/**
149+
* Extracts the {@link Authentication} object from the given Session, if and only if the user is authenticated.
150+
* @param session the session
151+
* @return Authentication|null
152+
*/
153+
private Authentication extractAuthenticationIfAuthenticated(Session session) {
154+
Object object = session.getAttribute("SPRING_SECURITY_CONTEXT");
155+
156+
if (object instanceof SecurityContext) {
157+
SecurityContext securityContext = (SecurityContext) object;
158+
159+
if (securityContext.getAuthentication().isAuthenticated()) {
160+
161+
return securityContext.getAuthentication();
162+
}
163+
}
164+
165+
return null;
166+
}
167+
136168
}

src/main/java/eu/openanalytics/containerproxy/service/session/undertow/UndertowSessionService.java

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
2828
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
29+
import org.springframework.security.core.Authentication;
2930
import org.springframework.security.core.context.SecurityContext;
3031
import org.springframework.stereotype.Component;
3132

3233
import javax.annotation.PostConstruct;
3334
import javax.inject.Inject;
35+
import java.time.Instant;
36+
import java.util.HashSet;
3437
import java.util.Objects;
3538
import java.util.Set;
3639
import java.util.Timer;
@@ -41,7 +44,7 @@
4144
@ConditionalOnProperty(name = "spring.session.store-type", havingValue = "none")
4245
public class UndertowSessionService implements ISessionService {
4346

44-
private static final int CACHE_UPDATE_INTERVAL = 60 * 1000; // update cache every minutes
47+
private static final int CACHE_UPDATE_INTERVAL = 20 * 1000; // update cache every minutes
4548

4649
private final Log logger = LogFactory.getLog(UndertowSessionService.class);
4750

@@ -51,6 +54,8 @@ public class UndertowSessionService implements ISessionService {
5154
// default value, note we cannot use 0 or -1 here as that would cause a dip when restarting ShinyProxy
5255
private Integer cachedUsersLoggedInCount = null;
5356

57+
private Integer cachedActiveUsersCount = null;
58+
5459
@PostConstruct
5560
public void init() {
5661
new Timer().schedule(new TimerTask() {
@@ -66,6 +71,11 @@ public Integer getLoggedInUsersCount() {
6671
return cachedUsersLoggedInCount;
6772
}
6873

74+
@Override
75+
public Integer getActiveUsersCount() {
76+
return cachedActiveUsersCount;
77+
}
78+
6979
/**
7080
* Updates the cached count of users.
7181
* We only update this value every CACHE_UPDATE_INTERVAL because this is a relative heavy computation to do.
@@ -82,26 +92,51 @@ private void updateCachedUsersLoggedInCount() {
8292
return;
8393
}
8494

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());
95+
// users are only counted as active, if they are active after this time.
96+
Instant minimumInstantTime = Instant.now().minusSeconds(60);
97+
98+
Set<String> authenticatedUsers = new HashSet<>();
99+
Set<String> activeUsers = new HashSet<>();
100+
101+
for (String sessionId: instance.getAllSessions()) {
102+
Session session = instance.getSession(sessionId);
103+
if (session == null) continue;
104+
105+
Authentication authentication = extractAuthenticationIfAuthenticated(session);
106+
if (authentication == null) continue;
107+
108+
authenticatedUsers.add(authentication.getName());
109+
110+
if (Instant.ofEpochMilli(session.getLastAccessedTime()).isAfter(minimumInstantTime)) {
111+
activeUsers.add(authentication.getName());
112+
}
113+
}
102114

103115
logger.debug(String.format("Logged in users count %s, all users: %s ", authenticatedUsers.size(), authenticatedUsers));
104116
cachedUsersLoggedInCount = authenticatedUsers.size();
117+
logger.debug(String.format("Active users count %s, %s ", activeUsers.size(), activeUsers));
118+
cachedActiveUsersCount = activeUsers.size();
119+
}
120+
121+
122+
/**
123+
* Extracts the {@link Authentication} object from the given Session, if and only if the user is authenticated.
124+
* @param session the session
125+
* @return Authentication|null
126+
*/
127+
private Authentication extractAuthenticationIfAuthenticated(Session session) {
128+
Object object = session.getAttribute("SPRING_SECURITY_CONTEXT");
129+
130+
if (object instanceof SecurityContext) {
131+
SecurityContext securityContext = (SecurityContext) object;
132+
133+
if (securityContext.getAuthentication().isAuthenticated()) {
134+
135+
return securityContext.getAuthentication();
136+
}
137+
}
138+
139+
return null;
105140
}
106141

107142

src/main/java/eu/openanalytics/containerproxy/stat/impl/Micrometer.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@
2020
*/
2121
package eu.openanalytics.containerproxy.stat.impl;
2222

23-
import eu.openanalytics.containerproxy.event.*;
23+
import eu.openanalytics.containerproxy.event.AuthFailedEvent;
24+
import eu.openanalytics.containerproxy.event.ProxyStartEvent;
25+
import eu.openanalytics.containerproxy.event.ProxyStartFailedEvent;
26+
import eu.openanalytics.containerproxy.event.ProxyStopEvent;
27+
import eu.openanalytics.containerproxy.event.UserLoginEvent;
28+
import eu.openanalytics.containerproxy.event.UserLogoutEvent;
2429
import eu.openanalytics.containerproxy.model.spec.ProxySpec;
2530
import eu.openanalytics.containerproxy.service.ProxyService;
2631
import eu.openanalytics.containerproxy.service.session.ISessionService;
2732
import eu.openanalytics.containerproxy.stat.IStatCollector;
2833
import io.micrometer.core.instrument.Counter;
29-
import io.micrometer.core.instrument.Gauge;
3034
import io.micrometer.core.instrument.MeterRegistry;
3135
import io.micrometer.core.instrument.Tags;
3236
import org.apache.logging.log4j.LogManager;
@@ -40,7 +44,6 @@
4044
import java.util.Timer;
4145
import java.util.TimerTask;
4246
import java.util.concurrent.ConcurrentHashMap;
43-
import java.util.function.DoubleFunction;
4447

4548
public class Micrometer implements IStatCollector {
4649

@@ -79,6 +82,7 @@ public void init() {
7982
appStartFailedCounter = registry.counter("startFailed");
8083
authFailedCounter = registry.counter("authFailed");
8184
registry.gauge("shinyproxy_absolute_users_logged_in", Tags.empty(), sessionService, ISessionService::getLoggedInUsersCount);
85+
registry.gauge("shinyproxy_absolute_users_active", Tags.empty(), sessionService, ISessionService::getActiveUsersCount);
8286

8387
for (ProxySpec spec : proxyService.getProxySpecs(null, true)) {
8488
registry.counter("appStarts", "spec.id", spec.getId());

0 commit comments

Comments
 (0)