Skip to content

Commit 0314443

Browse files
authored
Merge pull request #86 from SentriusLLC/copilot/fix-85
Add agent session duration tracking to dashboard graphs
2 parents afd0dae + 89ccaad commit 0314443

File tree

9 files changed

+546
-7
lines changed

9 files changed

+546
-7
lines changed

.local.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
SENTRIUS_VERSION=1.1.325
1+
SENTRIUS_VERSION=1.1.334
22
SENTRIUS_SSH_VERSION=1.1.41
33
SENTRIUS_KEYCLOAK_VERSION=1.1.53
44
SENTRIUS_AGENT_VERSION=1.1.42
55
SENTRIUS_AI_AGENT_VERSION=1.1.263
66
LLMPROXY_VERSION=1.0.78
77
LAUNCHER_VERSION=1.0.82
8-
AGENTPROXY_VERSION=1.0.75
8+
AGENTPROXY_VERSION=1.0.85

.local.env.bak

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
SENTRIUS_VERSION=1.1.325
1+
SENTRIUS_VERSION=1.1.334
22
SENTRIUS_SSH_VERSION=1.1.41
33
SENTRIUS_KEYCLOAK_VERSION=1.1.53
44
SENTRIUS_AGENT_VERSION=1.1.42
55
SENTRIUS_AI_AGENT_VERSION=1.1.263
66
LLMPROXY_VERSION=1.0.78
77
LAUNCHER_VERSION=1.0.82
8-
AGENTPROXY_VERSION=1.0.75
8+
AGENTPROXY_VERSION=1.0.85

agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ private ReactiveJwtAuthenticationConverter grantedAuthoritiesExtractor() {
6565
@Bean
6666
public CorsConfigurationSource corsConfigurationSource() {
6767
CorsConfiguration config = new CorsConfiguration();
68+
log.info("Configuring CORS for agent API URL: {}", agentApiUrl);
6869
config.setAllowedOrigins(List.of(agentApiUrl));
6970
config.setAllowedMethods(List.of("GET", "POST", "OPTIONS"));
7071
config.setAllowedHeaders(List.of("*"));

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: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,55 @@
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;
711
import java.util.concurrent.ConcurrentHashMap;
812
import java.util.stream.Collectors;
913
import io.sentrius.sso.core.dto.TerminalLogDTO;
14+
import lombok.extern.slf4j.Slf4j;
1015
import org.springframework.stereotype.Component;
1116
import org.springframework.web.reactive.socket.WebSocketSession;
1217

18+
@Slf4j
1319
@Component
1420
public class ActiveWebSocketSessionManager {
1521
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
22+
private final Map<String, Timestamp> sessionStartTimes = new ConcurrentHashMap<>();
23+
private final List<Map<String, Object>> completedAgentSessions = new ArrayList<>();
1624

1725
public void register(String sessionId, WebSocketSession session) {
1826
sessions.put(sessionId, session);
27+
sessionStartTimes.put(sessionId, new Timestamp(System.currentTimeMillis()));
1928
}
2029

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

2555
public WebSocketSession get(String sessionId) {
@@ -37,4 +67,46 @@ public List<TerminalLogDTO> getActiveSessions() {
3767
.build())
3868
.collect(Collectors.toList());
3969
}
70+
71+
/**
72+
* Get session duration data for agent sessions
73+
* @return List of session duration data
74+
*/
75+
public List<Map<String, Object>> getAgentSessionDurations() {
76+
synchronized (completedAgentSessions) {
77+
log.info("Returning {} completed agent sessions", completedAgentSessions.size());
78+
return new ArrayList<>(completedAgentSessions);
79+
}
80+
}
81+
82+
/**
83+
* Get current active agent session durations (for sessions still in progress)
84+
* @return List of active session duration data
85+
*/
86+
public List<Map<String, Object>> getActiveAgentSessionDurations() {
87+
List<Map<String, Object>> activeDurations = new ArrayList<>();
88+
89+
for (Map.Entry<String, Timestamp> entry : sessionStartTimes.entrySet()) {
90+
String sessionId = entry.getKey();
91+
Timestamp startTime = entry.getValue();
92+
93+
if (sessions.containsKey(sessionId)) {
94+
long durationMinutes = ChronoUnit.MINUTES.between(
95+
startTime.toLocalDateTime(),
96+
LocalDateTime.now()
97+
);
98+
99+
Map<String, Object> activeSession = new HashMap<>();
100+
activeSession.put("sessionId", sessionId);
101+
activeSession.put("startTime", startTime);
102+
activeSession.put("durationMinutes", durationMinutes);
103+
activeSession.put("sessionType", "agent");
104+
activeSession.put("active", true);
105+
106+
activeDurations.add(activeSession);
107+
}
108+
}
109+
log.info("Returning {} active agent session durations", activeDurations.size());
110+
return activeDurations;
111+
}
40112
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
76+
// When
77+
sessionManager.register(sessionId, webSocketSession);
78+
79+
// Wait a moment to ensure some time passes
80+
Thread.sleep(100);
81+
82+
// Then
83+
List<Map<String, Object>> activeSessions = sessionManager.getActiveAgentSessionDurations();
84+
assertEquals(1, activeSessions.size());
85+
86+
Map<String, Object> activeSession = activeSessions.get(0);
87+
assertEquals(sessionId, activeSession.get("sessionId"));
88+
assertEquals("agent", activeSession.get("sessionType"));
89+
assertEquals(true, activeSession.get("active"));
90+
assertNotNull(activeSession.get("startTime"));
91+
assertNotNull(activeSession.get("durationMinutes"));
92+
assertTrue((Long) activeSession.get("durationMinutes") >= 0);
93+
}
94+
95+
@Test
96+
void testMultipleSessionsHandling() {
97+
// Given
98+
String sessionId1 = "session-1";
99+
String sessionId2 = "session-2";
100+
WebSocketSession session1 = mock(WebSocketSession.class);
101+
WebSocketSession session2 = mock(WebSocketSession.class);
102+
103+
when(session1.getId()).thenReturn(sessionId1);
104+
when(session1.isOpen()).thenReturn(true);
105+
when(session1.getHandshakeInfo()).thenReturn(handshakeInfo);
106+
when(session2.getId()).thenReturn(sessionId2);
107+
when(session2.isOpen()).thenReturn(true);
108+
when(session2.getHandshakeInfo()).thenReturn(handshakeInfo);
109+
when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080));
110+
111+
// When
112+
sessionManager.register(sessionId1, session1);
113+
sessionManager.register(sessionId2, session2);
114+
115+
// Then
116+
assertEquals(2, sessionManager.getActiveSessions().size());
117+
assertEquals(2, sessionManager.getActiveAgentSessionDurations().size());
118+
119+
// When - unregister one session
120+
sessionManager.unregister(sessionId1);
121+
122+
// Then
123+
assertEquals(1, sessionManager.getActiveSessions().size());
124+
assertEquals(1, sessionManager.getActiveAgentSessionDurations().size());
125+
assertEquals(1, sessionManager.getAgentSessionDurations().size());
126+
}
127+
128+
@Test
129+
void testUnregisterNonExistentSession() {
130+
// Given
131+
String nonExistentSessionId = "non-existent";
132+
133+
// When
134+
sessionManager.unregister(nonExistentSessionId);
135+
136+
// Then - should not throw exception and should not affect other data
137+
assertEquals(0, sessionManager.getActiveSessions().size());
138+
assertEquals(0, sessionManager.getAgentSessionDurations().size());
139+
assertEquals(0, sessionManager.getActiveAgentSessionDurations().size());
140+
}
141+
}

0 commit comments

Comments
 (0)