2023-10-03 19:51:30 +00:00
|
|
|
module live_tracker;
|
|
|
|
|
2023-10-04 17:30:00 +00:00
|
|
|
import data;
|
|
|
|
import handy_httpd.components.websocket.handler;
|
2023-10-03 19:51:30 +00:00
|
|
|
import slf4d;
|
|
|
|
import std.uuid;
|
|
|
|
import std.datetime;
|
|
|
|
import std.json;
|
|
|
|
import std.string;
|
2023-10-04 17:30:00 +00:00
|
|
|
import std.parallelism;
|
|
|
|
import core.sync.rwmutex;
|
2023-10-03 19:51:30 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A websocket message handler that keeps track of each connected session, and
|
|
|
|
* records information about that session's activities.
|
|
|
|
*/
|
|
|
|
class LiveTracker : WebSocketMessageHandler {
|
|
|
|
private StatSession[UUID] sessions;
|
2023-10-04 17:30:00 +00:00
|
|
|
static immutable uint DEFAULT_MIN_SESSION_DURATION = 1000;
|
|
|
|
private immutable uint minSessionDurationMillis;
|
|
|
|
private TaskPool sessionPersistencePool;
|
|
|
|
private ReadWriteMutex sessionsMutex;
|
|
|
|
|
|
|
|
this(uint minSessionDurationMillis = DEFAULT_MIN_SESSION_DURATION) {
|
|
|
|
this.minSessionDurationMillis = minSessionDurationMillis;
|
|
|
|
this.sessionPersistencePool = new TaskPool(1);
|
|
|
|
this.sessionsMutex = new ReadWriteMutex();
|
|
|
|
}
|
2023-10-03 19:51:30 +00:00
|
|
|
|
|
|
|
override void onConnectionEstablished(WebSocketConnection conn) {
|
2023-10-04 17:30:00 +00:00
|
|
|
debugF!"Connection established: %s"(conn.id);
|
|
|
|
synchronized(sessionsMutex.writer) {
|
|
|
|
sessions[conn.id] = StatSession(
|
|
|
|
conn.id,
|
|
|
|
Clock.currTime(UTC())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
infoF!"Started tracking session %s"(conn.id);
|
2023-10-03 19:51:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override void onTextMessage(WebSocketTextMessage msg) {
|
2023-10-04 17:30:00 +00:00
|
|
|
StatSession* session;
|
|
|
|
synchronized(sessionsMutex.reader) {
|
|
|
|
session = msg.conn.id in sessions;
|
|
|
|
}
|
2023-10-03 19:51:30 +00:00
|
|
|
if (session is null) {
|
2023-10-04 17:30:00 +00:00
|
|
|
warnF!"Got a websocket text message from a client without a session: %s"(msg.conn.id);
|
2023-10-03 19:51:30 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
JSONValue obj = parseJSON(msg.payload);
|
|
|
|
immutable string msgType = obj.object["type"].str;
|
|
|
|
if (msgType == MessageTypes.IDENT) {
|
2023-10-04 17:30:00 +00:00
|
|
|
handleIdent(obj, session);
|
2023-10-03 19:51:30 +00:00
|
|
|
} else if (msgType == MessageTypes.EVENT) {
|
2023-10-04 17:30:00 +00:00
|
|
|
session.eventCount++;
|
|
|
|
// session.events ~= EventRecord(
|
|
|
|
// Clock.currTime(UTC()),
|
|
|
|
// obj.object["event"].str
|
|
|
|
// );
|
2023-10-03 19:51:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override void onConnectionClosed(WebSocketConnection conn) {
|
2023-10-04 17:30:00 +00:00
|
|
|
StatSession* session;
|
|
|
|
synchronized(sessionsMutex.reader) {
|
|
|
|
session = conn.id in sessions;
|
|
|
|
}
|
2023-10-03 19:51:30 +00:00
|
|
|
if (session !is null && session.isValid) {
|
2023-10-04 17:30:00 +00:00
|
|
|
SysTime endTimestamp = Clock.currTime(UTC());
|
|
|
|
Duration dur = endTimestamp - session.connectedAt;
|
2023-10-03 19:51:30 +00:00
|
|
|
if (dur.total!"msecs" >= minSessionDurationMillis) {
|
2023-10-04 17:30:00 +00:00
|
|
|
infoF!"Session lasted %d seconds, %d events."(dur.total!"seconds", session.eventCount);
|
2023-10-03 19:51:30 +00:00
|
|
|
}
|
2023-10-04 17:30:00 +00:00
|
|
|
immutable storedSession = StoredSession(
|
|
|
|
-1,
|
|
|
|
session.connectedAt,
|
|
|
|
endTimestamp,
|
|
|
|
session.url,
|
|
|
|
session.href,
|
|
|
|
session.userAgent,
|
|
|
|
session.eventCount
|
|
|
|
);
|
|
|
|
this.sessionPersistencePool.put(task!storeSession(storedSession));
|
|
|
|
}
|
|
|
|
synchronized(sessionsMutex.writer) {
|
|
|
|
sessions.remove(conn.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void handleIdent(JSONValue msg, StatSession* session) {
|
|
|
|
string fullUrl = msg.object["href"].str;
|
|
|
|
session.href = fullUrl;
|
|
|
|
ptrdiff_t paramsIdx = std.string.indexOf(fullUrl, '?');
|
|
|
|
if (paramsIdx == -1) {
|
|
|
|
session.url = fullUrl;
|
|
|
|
} else {
|
|
|
|
session.url = fullUrl[0 .. paramsIdx];
|
2023-10-03 19:51:30 +00:00
|
|
|
}
|
2023-10-04 17:30:00 +00:00
|
|
|
session.userAgent = msg.object["userAgent"].str;
|
2023-10-03 19:51:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private enum MessageTypes : string {
|
|
|
|
IDENT = "ident",
|
|
|
|
EVENT = "event"
|
|
|
|
}
|
|
|
|
|
|
|
|
struct StatSession {
|
|
|
|
UUID id;
|
|
|
|
SysTime connectedAt;
|
|
|
|
string url = null;
|
|
|
|
string href = null;
|
|
|
|
string userAgent = null;
|
2023-10-04 17:30:00 +00:00
|
|
|
uint eventCount = 0;
|
2023-10-03 19:51:30 +00:00
|
|
|
|
|
|
|
bool isValid() const {
|
|
|
|
return url !is null && href !is null && userAgent !is null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct EventRecord {
|
|
|
|
SysTime timestamp;
|
|
|
|
string eventType;
|
|
|
|
}
|