Skip to content

Commit 6742fac

Browse files
Copilotphrocker
andcommitted
Add agent session duration tracking to dashboard graphs
Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com>
1 parent 5b50b90 commit 6742fac

File tree

5 files changed

+503
-1
lines changed

5 files changed

+503
-1
lines changed

agent-proxy/src/main/java/io/sentrius/sso/controller/SessionController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentrius.sso.controller;
22

33
import java.util.List;
4+
import java.util.Map;
45
import io.sentrius.sso.core.dto.TerminalLogDTO;
56
import io.sentrius.sso.service.ActiveWebSocketSessionManager;
67
import org.springframework.http.HttpHeaders;
@@ -27,4 +28,14 @@ public List<TerminalLogDTO> listSessions() {
2728
return activeWebSocketSessionManager.getActiveSessions();
2829
}
2930

31+
@GetMapping("/agent/durations")
32+
public List<Map<String, Object>> getAgentSessionDurations() {
33+
return activeWebSocketSessionManager.getAgentSessionDurations();
34+
}
35+
36+
@GetMapping("/agent/active-durations")
37+
public List<Map<String, Object>> getActiveAgentSessionDurations() {
38+
return activeWebSocketSessionManager.getActiveAgentSessionDurations();
39+
}
40+
3041
}

agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package io.sentrius.sso.service;
22

33
import java.sql.Timestamp;
4+
import java.time.LocalDateTime;
5+
import java.time.temporal.ChronoUnit;
6+
import java.util.ArrayList;
7+
import java.util.HashMap;
48
import java.util.List;
59
import java.util.Map;
610
import java.util.Objects;
@@ -13,13 +17,37 @@
1317
@Component
1418
public class ActiveWebSocketSessionManager {
1519
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
20+
private final Map<String, Timestamp> sessionStartTimes = new ConcurrentHashMap<>();
21+
private final List<Map<String, Object>> completedAgentSessions = new ArrayList<>();
1622

1723
public void register(String sessionId, WebSocketSession session) {
1824
sessions.put(sessionId, session);
25+
sessionStartTimes.put(sessionId, new Timestamp(System.currentTimeMillis()));
1926
}
2027

2128
public void unregister(String sessionId) {
22-
sessions.remove(sessionId);
29+
WebSocketSession session = sessions.remove(sessionId);
30+
Timestamp startTime = sessionStartTimes.remove(sessionId);
31+
32+
if (startTime != null) {
33+
// Calculate duration and store completed session
34+
Timestamp endTime = new Timestamp(System.currentTimeMillis());
35+
long durationMinutes = ChronoUnit.MINUTES.between(
36+
startTime.toLocalDateTime(),
37+
endTime.toLocalDateTime()
38+
);
39+
40+
Map<String, Object> completedSession = new HashMap<>();
41+
completedSession.put("sessionId", sessionId);
42+
completedSession.put("startTime", startTime);
43+
completedSession.put("endTime", endTime);
44+
completedSession.put("durationMinutes", durationMinutes);
45+
completedSession.put("sessionType", "agent");
46+
47+
synchronized (completedAgentSessions) {
48+
completedAgentSessions.add(completedSession);
49+
}
50+
}
2351
}
2452

2553
public WebSocketSession get(String sessionId) {
@@ -37,4 +65,45 @@ public List<TerminalLogDTO> getActiveSessions() {
3765
.build())
3866
.collect(Collectors.toList());
3967
}
68+
69+
/**
70+
* Get session duration data for agent sessions
71+
* @return List of session duration data
72+
*/
73+
public List<Map<String, Object>> getAgentSessionDurations() {
74+
synchronized (completedAgentSessions) {
75+
return new ArrayList<>(completedAgentSessions);
76+
}
77+
}
78+
79+
/**
80+
* Get current active agent session durations (for sessions still in progress)
81+
* @return List of active session duration data
82+
*/
83+
public List<Map<String, Object>> getActiveAgentSessionDurations() {
84+
List<Map<String, Object>> activeDurations = new ArrayList<>();
85+
86+
for (Map.Entry<String, Timestamp> entry : sessionStartTimes.entrySet()) {
87+
String sessionId = entry.getKey();
88+
Timestamp startTime = entry.getValue();
89+
90+
if (sessions.containsKey(sessionId)) {
91+
long durationMinutes = ChronoUnit.MINUTES.between(
92+
startTime.toLocalDateTime(),
93+
LocalDateTime.now()
94+
);
95+
96+
Map<String, Object> activeSession = new HashMap<>();
97+
activeSession.put("sessionId", sessionId);
98+
activeSession.put("startTime", startTime);
99+
activeSession.put("durationMinutes", durationMinutes);
100+
activeSession.put("sessionType", "agent");
101+
activeSession.put("active", true);
102+
103+
activeDurations.add(activeSession);
104+
}
105+
}
106+
107+
return activeDurations;
108+
}
40109
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package io.sentrius.sso.service;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.extension.ExtendWith;
6+
import org.mockito.Mock;
7+
import org.mockito.junit.jupiter.MockitoExtension;
8+
import org.springframework.web.reactive.socket.WebSocketSession;
9+
import org.springframework.web.reactive.socket.HandshakeInfo;
10+
11+
import java.net.InetSocketAddress;
12+
import java.sql.Timestamp;
13+
import java.util.List;
14+
import java.util.Map;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
import static org.mockito.Mockito.*;
18+
19+
@ExtendWith(MockitoExtension.class)
20+
class ActiveWebSocketSessionManagerTest {
21+
22+
@Mock
23+
private WebSocketSession webSocketSession;
24+
25+
@Mock
26+
private HandshakeInfo handshakeInfo;
27+
28+
private ActiveWebSocketSessionManager sessionManager;
29+
30+
@BeforeEach
31+
void setUp() {
32+
sessionManager = new ActiveWebSocketSessionManager();
33+
}
34+
35+
@Test
36+
void testRegisterAndUnregisterSession() {
37+
// Given
38+
String sessionId = "test-session-1";
39+
when(webSocketSession.getId()).thenReturn(sessionId);
40+
when(webSocketSession.isOpen()).thenReturn(true);
41+
when(webSocketSession.getHandshakeInfo()).thenReturn(handshakeInfo);
42+
when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080));
43+
44+
// When - register session
45+
sessionManager.register(sessionId, webSocketSession);
46+
47+
// Then - session should be active
48+
assertEquals(webSocketSession, sessionManager.get(sessionId));
49+
assertEquals(1, sessionManager.getActiveSessions().size());
50+
assertEquals(0, sessionManager.getAgentSessionDurations().size());
51+
52+
// When - unregister session
53+
sessionManager.unregister(sessionId);
54+
55+
// Then - session should be removed and duration recorded
56+
assertNull(sessionManager.get(sessionId));
57+
assertEquals(0, sessionManager.getActiveSessions().size());
58+
assertEquals(1, sessionManager.getAgentSessionDurations().size());
59+
60+
// Verify session duration data
61+
List<Map<String, Object>> completedSessions = sessionManager.getAgentSessionDurations();
62+
Map<String, Object> sessionData = completedSessions.get(0);
63+
assertEquals(sessionId, sessionData.get("sessionId"));
64+
assertEquals("agent", sessionData.get("sessionType"));
65+
assertNotNull(sessionData.get("startTime"));
66+
assertNotNull(sessionData.get("endTime"));
67+
assertNotNull(sessionData.get("durationMinutes"));
68+
assertTrue((Long) sessionData.get("durationMinutes") >= 0);
69+
}
70+
71+
@Test
72+
void testGetActiveAgentSessionDurations() throws InterruptedException {
73+
// Given
74+
String sessionId = "active-session-1";
75+
when(webSocketSession.getId()).thenReturn(sessionId);
76+
when(webSocketSession.isOpen()).thenReturn(true);
77+
when(webSocketSession.getHandshakeInfo()).thenReturn(handshakeInfo);
78+
when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080));
79+
80+
// When
81+
sessionManager.register(sessionId, webSocketSession);
82+
83+
// Wait a moment to ensure some time passes
84+
Thread.sleep(100);
85+
86+
// Then
87+
List<Map<String, Object>> activeSessions = sessionManager.getActiveAgentSessionDurations();
88+
assertEquals(1, activeSessions.size());
89+
90+
Map<String, Object> activeSession = activeSessions.get(0);
91+
assertEquals(sessionId, activeSession.get("sessionId"));
92+
assertEquals("agent", activeSession.get("sessionType"));
93+
assertEquals(true, activeSession.get("active"));
94+
assertNotNull(activeSession.get("startTime"));
95+
assertNotNull(activeSession.get("durationMinutes"));
96+
assertTrue((Long) activeSession.get("durationMinutes") >= 0);
97+
}
98+
99+
@Test
100+
void testMultipleSessionsHandling() {
101+
// Given
102+
String sessionId1 = "session-1";
103+
String sessionId2 = "session-2";
104+
WebSocketSession session1 = mock(WebSocketSession.class);
105+
WebSocketSession session2 = mock(WebSocketSession.class);
106+
107+
when(session1.getId()).thenReturn(sessionId1);
108+
when(session1.isOpen()).thenReturn(true);
109+
when(session1.getHandshakeInfo()).thenReturn(handshakeInfo);
110+
when(session2.getId()).thenReturn(sessionId2);
111+
when(session2.isOpen()).thenReturn(true);
112+
when(session2.getHandshakeInfo()).thenReturn(handshakeInfo);
113+
when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080));
114+
115+
// When
116+
sessionManager.register(sessionId1, session1);
117+
sessionManager.register(sessionId2, session2);
118+
119+
// Then
120+
assertEquals(2, sessionManager.getActiveSessions().size());
121+
assertEquals(2, sessionManager.getActiveAgentSessionDurations().size());
122+
123+
// When - unregister one session
124+
sessionManager.unregister(sessionId1);
125+
126+
// Then
127+
assertEquals(1, sessionManager.getActiveSessions().size());
128+
assertEquals(1, sessionManager.getActiveAgentSessionDurations().size());
129+
assertEquals(1, sessionManager.getAgentSessionDurations().size());
130+
}
131+
132+
@Test
133+
void testUnregisterNonExistentSession() {
134+
// Given
135+
String nonExistentSessionId = "non-existent";
136+
137+
// When
138+
sessionManager.unregister(nonExistentSessionId);
139+
140+
// Then - should not throw exception and should not affect other data
141+
assertEquals(0, sessionManager.getActiveSessions().size());
142+
assertEquals(0, sessionManager.getAgentSessionDurations().size());
143+
assertEquals(0, sessionManager.getActiveAgentSessionDurations().size());
144+
}
145+
}

dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
import io.sentrius.sso.core.repository.SessionLogRepository;
88
import io.sentrius.sso.core.repository.TerminalLogRepository;
99
import lombok.NonNull;
10+
import lombok.extern.slf4j.Slf4j;
1011
import org.springframework.beans.factory.annotation.Autowired;
12+
import org.springframework.beans.factory.annotation.Value;
13+
import org.springframework.core.ParameterizedTypeReference;
14+
import org.springframework.http.HttpMethod;
1115
import org.springframework.stereotype.Service;
1216
import org.springframework.transaction.annotation.Transactional;
17+
import org.springframework.web.client.RestTemplate;
1318

1419
import java.sql.Timestamp;
1520
import java.time.LocalDateTime;
@@ -21,6 +26,7 @@
2126
import java.util.Optional;
2227
import java.util.concurrent.ConcurrentHashMap;
2328

29+
@Slf4j
2430
@Service
2531
public class SessionService {
2632

@@ -30,6 +36,11 @@ public class SessionService {
3036
@Autowired
3137
private TerminalLogRepository terminalLogRepository;
3238

39+
@Value("${agentproxy.externalUrl:}")
40+
private String agentProxyExternalUrl;
41+
42+
private final RestTemplate restTemplate = new RestTemplate();
43+
3344
private final Map<Long, SessionLog> activeSessions = new ConcurrentHashMap<>();
3445
private final Map<Long, TerminalLogs> activeTerminals = new ConcurrentHashMap<>();
3546

@@ -133,6 +144,10 @@ public List<Map<String, Object>> getSessionDurationData(String username) {
133144

134145
public Map<String, Integer> getGraphData(String username) {
135146
List<Map<String, Object>> sessionDurations = getSessionDurationData(username);
147+
148+
// Add agent session durations
149+
List<Map<String, Object>> agentSessionDurations = getAgentSessionDurations();
150+
sessionDurations.addAll(agentSessionDurations);
136151

137152
Map<String, Integer> graphData = new HashMap<>();
138153
graphData.put("0-5 min", 0);
@@ -157,4 +172,52 @@ public Map<String, Integer> getGraphData(String username) {
157172
return graphData;
158173
}
159174

175+
/**
176+
* Fetch agent session duration data from agent proxy service
177+
* @return List of agent session duration data
178+
*/
179+
private List<Map<String, Object>> getAgentSessionDurations() {
180+
List<Map<String, Object>> agentSessions = new ArrayList<>();
181+
182+
if (agentProxyExternalUrl == null || agentProxyExternalUrl.trim().isEmpty()) {
183+
log.warn("Agent proxy URL not configured, skipping agent session data");
184+
return agentSessions;
185+
}
186+
187+
try {
188+
// Fetch completed agent sessions
189+
String completedUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/durations";
190+
var completedResponse = restTemplate.exchange(
191+
completedUrl,
192+
HttpMethod.GET,
193+
null,
194+
new ParameterizedTypeReference<List<Map<String, Object>>>() {}
195+
);
196+
197+
if (completedResponse.getBody() != null) {
198+
agentSessions.addAll(completedResponse.getBody());
199+
}
200+
201+
// Fetch active agent sessions
202+
String activeUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/active-durations";
203+
var activeResponse = restTemplate.exchange(
204+
activeUrl,
205+
HttpMethod.GET,
206+
null,
207+
new ParameterizedTypeReference<List<Map<String, Object>>>() {}
208+
);
209+
210+
if (activeResponse.getBody() != null) {
211+
agentSessions.addAll(activeResponse.getBody());
212+
}
213+
214+
log.info("Fetched {} agent session duration records", agentSessions.size());
215+
216+
} catch (Exception e) {
217+
log.warn("Failed to fetch agent session data from {}: {}", agentProxyExternalUrl, e.getMessage());
218+
}
219+
220+
return agentSessions;
221+
}
222+
160223
}

0 commit comments

Comments
 (0)