feat(database): server_traffic table for per-minute traffic rollups

One row per server and minute (packets, bytes, resends, HTTP requests, errors,
bytes and latency percentiles), written in batches and read summed into
buckets for the longer chart ranges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:50:54 -05:00
parent 6f334c2e3d
commit 01d10f4401
11 changed files with 185 additions and 1 deletions

View File

@@ -51,6 +51,7 @@
#include "IPropertyRent.h"
#include "IPropertyReputation.h"
#include "IBbbAutosave.h"
#include "IServerTraffic.h"
#ifdef _DEBUG
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
@@ -64,7 +65,7 @@ class GameDatabase :
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave {
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave, public IServerTraffic {
public:
virtual ~GameDatabase() = default;
// TODO: These should be made private.

View File

@@ -0,0 +1,40 @@
#ifndef __ISERVERTRAFFIC__H__
#define __ISERVERTRAFFIC__H__
#include <cstdint>
#include <string>
#include <vector>
/**
* Per-minute traffic of each server (packets, bytes, HTTP requests), written by the dashboard once a minute from the
* servers' traffic reports and kept for traffic_days. The last hour at one second resolution is only in memory.
*/
class IServerTraffic {
public:
struct TrafficMinute {
int64_t time{}; // start of the minute (or bucket), Unix seconds
std::string server; // "master", "auth", "chat", "dashboard", "ugc", "world:<zone>:<instance>"
uint64_t packetsIn{};
uint64_t packetsOut{};
uint64_t bytesIn{};
uint64_t bytesOut{};
uint64_t resends{};
uint64_t httpRequests{};
uint64_t http4xx{};
uint64_t http5xx{};
uint64_t httpBytesOut{};
uint32_t latencyP50Us{}; // HTTP; in a bucket of several minutes: the average of the minutes'
uint32_t latencyP95Us{}; // in a bucket: the worst minute's
uint32_t latencyP99Us{}; // in a bucket: the worst minute's
};
// One batch a minute
virtual void InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) = 0;
// Rows between from and to (inclusive), summed per server into buckets of `bucketSeconds`, ordered by time
virtual std::vector<TrafficMinute> GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) = 0;
virtual uint32_t PruneTrafficMinutes(int64_t beforeTime) = 0;
};
#endif //!__ISERVERTRAFFIC__H__

View File

@@ -215,6 +215,10 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IServerTraffic
void InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) override;
std::vector<TrafficMinute> GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override;
uint32_t PruneTrafficMinutes(int64_t beforeTime) override;
// IBbbAutosave
std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) override;
void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override;

View File

@@ -22,6 +22,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
"PropertyRent.cpp"
"PropertyReputation.cpp"
"BbbAutosave.cpp"
"ServerTraffic.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,46 @@
#include "MySQLDatabase.h"
void MySQLDatabase::InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) {
if (minutes.empty()) return;
const auto prevCommit = GetAutoCommit();
SetAutoCommit(false);
for (const auto& m : minutes) {
ExecuteInsert("REPLACE INTO server_traffic (time, server, packets_in, packets_out, bytes_in, bytes_out, resends, http_requests, http_4xx, http_5xx, http_bytes_out, "
"latency_p50_us, latency_p95_us, latency_p99_us) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
m.time, m.server, m.packetsIn, m.packetsOut, m.bytesIn, m.bytesOut, m.resends, m.httpRequests, m.http4xx, m.http5xx, m.httpBytesOut,
m.latencyP50Us, m.latencyP95Us, m.latencyP99Us);
}
Commit();
SetAutoCommit(prevCommit);
}
std::vector<IServerTraffic::TrafficMinute> MySQLDatabase::GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) {
std::vector<TrafficMinute> minutes;
auto result = ExecuteSelect("SELECT FLOOR(time / ?) * ? AS bucket, server, SUM(packets_in) AS packets_in, SUM(packets_out) AS packets_out, SUM(bytes_in) AS bytes_in, "
"SUM(bytes_out) AS bytes_out, SUM(resends) AS resends, SUM(http_requests) AS http_requests, SUM(http_4xx) AS http_4xx, SUM(http_5xx) AS http_5xx, "
"SUM(http_bytes_out) AS http_bytes_out, AVG(latency_p50_us) AS p50, MAX(latency_p95_us) AS p95, MAX(latency_p99_us) AS p99 "
"FROM server_traffic WHERE time >= ? AND time <= ? GROUP BY bucket, server ORDER BY bucket, server;", bucketSeconds, bucketSeconds, from, to);
while (result->next()) {
TrafficMinute m;
m.time = result->getInt64("bucket");
m.server = result->getString("server").c_str();
m.packetsIn = result->getUInt64("packets_in");
m.packetsOut = result->getUInt64("packets_out");
m.bytesIn = result->getUInt64("bytes_in");
m.bytesOut = result->getUInt64("bytes_out");
m.resends = result->getUInt64("resends");
m.httpRequests = result->getUInt64("http_requests");
m.http4xx = result->getUInt64("http_4xx");
m.http5xx = result->getUInt64("http_5xx");
m.httpBytesOut = result->getUInt64("http_bytes_out");
m.latencyP50Us = static_cast<uint32_t>(result->getDouble("p50"));
m.latencyP95Us = result->getUInt("p95");
m.latencyP99Us = result->getUInt("p99");
minutes.push_back(std::move(m));
}
return minutes;
}
uint32_t MySQLDatabase::PruneTrafficMinutes(int64_t beforeTime) {
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM server_traffic WHERE time < ?;", beforeTime));
}

View File

@@ -199,6 +199,10 @@ public:
std::vector<ZoneLimit> GetZoneLimits() override;
void SetZoneLimit(const ZoneLimit& limit) override;
void DeleteZoneLimit(uint32_t zoneId) override;
// IServerTraffic
void InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) override;
std::vector<TrafficMinute> GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override;
uint32_t PruneTrafficMinutes(int64_t beforeTime) override;
// IBbbAutosave
std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) override;
void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override;

View File

@@ -22,6 +22,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
"PropertyRent.cpp"
"PropertyReputation.cpp"
"BbbAutosave.cpp"
"ServerTraffic.cpp"
"MessageCaptures.cpp"
"ChatLog.cpp"
"RelatedData.cpp"

View File

@@ -0,0 +1,46 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) {
if (minutes.empty()) return;
const auto prevCommit = GetAutoCommit();
SetAutoCommit(false);
for (const auto& m : minutes) {
ExecuteInsert("INSERT OR REPLACE INTO server_traffic (time, server, packets_in, packets_out, bytes_in, bytes_out, resends, http_requests, http_4xx, http_5xx, http_bytes_out, "
"latency_p50_us, latency_p95_us, latency_p99_us) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
m.time, m.server, m.packetsIn, m.packetsOut, m.bytesIn, m.bytesOut, m.resends, m.httpRequests, m.http4xx, m.http5xx, m.httpBytesOut,
m.latencyP50Us, m.latencyP95Us, m.latencyP99Us);
}
Commit();
SetAutoCommit(prevCommit);
}
std::vector<IServerTraffic::TrafficMinute> SQLiteDatabase::GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) {
std::vector<TrafficMinute> minutes;
auto [_, result] = ExecuteSelect("SELECT (time / ?) * ? AS bucket, server, SUM(packets_in) AS packets_in, SUM(packets_out) AS packets_out, SUM(bytes_in) AS bytes_in, "
"SUM(bytes_out) AS bytes_out, SUM(resends) AS resends, SUM(http_requests) AS http_requests, SUM(http_4xx) AS http_4xx, SUM(http_5xx) AS http_5xx, "
"SUM(http_bytes_out) AS http_bytes_out, AVG(latency_p50_us) AS p50, MAX(latency_p95_us) AS p95, MAX(latency_p99_us) AS p99 "
"FROM server_traffic WHERE time >= ? AND time <= ? GROUP BY bucket, server ORDER BY bucket, server;", bucketSeconds, bucketSeconds, from, to);
for (; !result.eof(); result.nextRow()) {
TrafficMinute m;
m.time = result.getInt64Field("bucket");
m.server = result.getStringField("server");
m.packetsIn = static_cast<uint64_t>(result.getInt64Field("packets_in"));
m.packetsOut = static_cast<uint64_t>(result.getInt64Field("packets_out"));
m.bytesIn = static_cast<uint64_t>(result.getInt64Field("bytes_in"));
m.bytesOut = static_cast<uint64_t>(result.getInt64Field("bytes_out"));
m.resends = static_cast<uint64_t>(result.getInt64Field("resends"));
m.httpRequests = static_cast<uint64_t>(result.getInt64Field("http_requests"));
m.http4xx = static_cast<uint64_t>(result.getInt64Field("http_4xx"));
m.http5xx = static_cast<uint64_t>(result.getInt64Field("http_5xx"));
m.httpBytesOut = static_cast<uint64_t>(result.getInt64Field("http_bytes_out"));
m.latencyP50Us = static_cast<uint32_t>(result.getFloatField("p50"));
m.latencyP95Us = static_cast<uint32_t>(result.getInt64Field("p95"));
m.latencyP99Us = static_cast<uint32_t>(result.getInt64Field("p99"));
minutes.push_back(std::move(m));
}
return minutes;
}
uint32_t SQLiteDatabase::PruneTrafficMinutes(int64_t beforeTime) {
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM server_traffic WHERE time < ?;", beforeTime));
}

View File

@@ -222,6 +222,9 @@ class TestSQLDatabase : public GameDatabase {
void InsertHealthSample(const HealthSample& sample) override {}
std::vector<HealthSample> GetHealthSamples(int64_t from, int64_t to, int64_t bucketSeconds) override { return {}; }
uint32_t PruneHealthSamples(int64_t beforeTime) override { return 0; }
void InsertTrafficMinutes(const std::vector<TrafficMinute>& minutes) override {}
std::vector<TrafficMinute> GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override { return {}; }
uint32_t PruneTrafficMinutes(int64_t beforeTime) override { return 0; }
void InsertInstanceSamples(const std::vector<InstanceSample>& samples) override {}
std::vector<InstanceSample> GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) override { return {}; }
void InsertPositionSamples(const std::vector<PositionSample>& samples) override {}