From 01d10f44015e14bb90c03143e17cba468ff17601 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:50:54 -0500 Subject: [PATCH] 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 --- dDatabase/GameDatabase/GameDatabase.h | 3 +- .../GameDatabase/ITables/IServerTraffic.h | 40 ++++++++++++++++ dDatabase/GameDatabase/MySQL/MySQLDatabase.h | 4 ++ .../GameDatabase/MySQL/Tables/CMakeLists.txt | 1 + .../MySQL/Tables/ServerTraffic.cpp | 46 +++++++++++++++++++ .../GameDatabase/SQLite/SQLiteDatabase.h | 4 ++ .../GameDatabase/SQLite/Tables/CMakeLists.txt | 1 + .../SQLite/Tables/ServerTraffic.cpp | 46 +++++++++++++++++++ .../GameDatabase/TestSQL/TestSQLDatabase.h | 3 ++ migrations/dlu/mysql/84_server_traffic.sql | 20 ++++++++ migrations/dlu/sqlite/67_server_traffic.sql | 18 ++++++++ 11 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 dDatabase/GameDatabase/ITables/IServerTraffic.h create mode 100644 dDatabase/GameDatabase/MySQL/Tables/ServerTraffic.cpp create mode 100644 dDatabase/GameDatabase/SQLite/Tables/ServerTraffic.cpp create mode 100644 migrations/dlu/mysql/84_server_traffic.sql create mode 100644 migrations/dlu/sqlite/67_server_traffic.sql diff --git a/dDatabase/GameDatabase/GameDatabase.h b/dDatabase/GameDatabase/GameDatabase.h index 922843aa8..940e0339d 100644 --- a/dDatabase/GameDatabase/GameDatabase.h +++ b/dDatabase/GameDatabase/GameDatabase.h @@ -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. diff --git a/dDatabase/GameDatabase/ITables/IServerTraffic.h b/dDatabase/GameDatabase/ITables/IServerTraffic.h new file mode 100644 index 000000000..39bb3907d --- /dev/null +++ b/dDatabase/GameDatabase/ITables/IServerTraffic.h @@ -0,0 +1,40 @@ +#ifndef __ISERVERTRAFFIC__H__ +#define __ISERVERTRAFFIC__H__ + +#include +#include +#include + +/** + * 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::" + 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& minutes) = 0; + + // Rows between from and to (inclusive), summed per server into buckets of `bucketSeconds`, ordered by time + virtual std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) = 0; + + virtual uint32_t PruneTrafficMinutes(int64_t beforeTime) = 0; +}; + +#endif //!__ISERVERTRAFFIC__H__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 5a88c5006..ac21abb1b 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -215,6 +215,10 @@ public: std::vector GetZoneLimits() override; void SetZoneLimit(const ZoneLimit& limit) override; void DeleteZoneLimit(uint32_t zoneId) override; + // IServerTraffic + void InsertTrafficMinutes(const std::vector& minutes) override; + std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; + uint32_t PruneTrafficMinutes(int64_t beforeTime) override; // IBbbAutosave std::optional GetBbbAutosave(const LWOOBJID characterId) override; void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override; diff --git a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt index 676ff817b..fd6a524c9 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/MySQL/Tables/CMakeLists.txt @@ -22,6 +22,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES "PropertyRent.cpp" "PropertyReputation.cpp" "BbbAutosave.cpp" + "ServerTraffic.cpp" "MessageCaptures.cpp" "ChatLog.cpp" "RelatedData.cpp" diff --git a/dDatabase/GameDatabase/MySQL/Tables/ServerTraffic.cpp b/dDatabase/GameDatabase/MySQL/Tables/ServerTraffic.cpp new file mode 100644 index 000000000..ffbf5d892 --- /dev/null +++ b/dDatabase/GameDatabase/MySQL/Tables/ServerTraffic.cpp @@ -0,0 +1,46 @@ +#include "MySQLDatabase.h" + +void MySQLDatabase::InsertTrafficMinutes(const std::vector& 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 MySQLDatabase::GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) { + std::vector 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(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(ExecuteUpdate("DELETE FROM server_traffic WHERE time < ?;", beforeTime)); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index 4fcb61205..22f4ca685 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -199,6 +199,10 @@ public: std::vector GetZoneLimits() override; void SetZoneLimit(const ZoneLimit& limit) override; void DeleteZoneLimit(uint32_t zoneId) override; + // IServerTraffic + void InsertTrafficMinutes(const std::vector& minutes) override; + std::vector GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) override; + uint32_t PruneTrafficMinutes(int64_t beforeTime) override; // IBbbAutosave std::optional GetBbbAutosave(const LWOOBJID characterId) override; void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override; diff --git a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt index 475c21a2b..52409b22b 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt +++ b/dDatabase/GameDatabase/SQLite/Tables/CMakeLists.txt @@ -22,6 +22,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES "PropertyRent.cpp" "PropertyReputation.cpp" "BbbAutosave.cpp" + "ServerTraffic.cpp" "MessageCaptures.cpp" "ChatLog.cpp" "RelatedData.cpp" diff --git a/dDatabase/GameDatabase/SQLite/Tables/ServerTraffic.cpp b/dDatabase/GameDatabase/SQLite/Tables/ServerTraffic.cpp new file mode 100644 index 000000000..731a16bc5 --- /dev/null +++ b/dDatabase/GameDatabase/SQLite/Tables/ServerTraffic.cpp @@ -0,0 +1,46 @@ +#include "SQLiteDatabase.h" + +void SQLiteDatabase::InsertTrafficMinutes(const std::vector& 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 SQLiteDatabase::GetTrafficMinutes(int64_t from, int64_t to, int64_t bucketSeconds) { + std::vector 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(result.getInt64Field("packets_in")); + m.packetsOut = static_cast(result.getInt64Field("packets_out")); + m.bytesIn = static_cast(result.getInt64Field("bytes_in")); + m.bytesOut = static_cast(result.getInt64Field("bytes_out")); + m.resends = static_cast(result.getInt64Field("resends")); + m.httpRequests = static_cast(result.getInt64Field("http_requests")); + m.http4xx = static_cast(result.getInt64Field("http_4xx")); + m.http5xx = static_cast(result.getInt64Field("http_5xx")); + m.httpBytesOut = static_cast(result.getInt64Field("http_bytes_out")); + m.latencyP50Us = static_cast(result.getFloatField("p50")); + m.latencyP95Us = static_cast(result.getInt64Field("p95")); + m.latencyP99Us = static_cast(result.getInt64Field("p99")); + minutes.push_back(std::move(m)); + } + return minutes; +} + +uint32_t SQLiteDatabase::PruneTrafficMinutes(int64_t beforeTime) { + return static_cast(ExecuteUpdate("DELETE FROM server_traffic WHERE time < ?;", beforeTime)); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index b65e6b377..d394a781b 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -222,6 +222,9 @@ class TestSQLDatabase : public GameDatabase { void InsertHealthSample(const HealthSample& sample) override {} std::vector 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& minutes) override {} + std::vector 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& samples) override {} std::vector GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) override { return {}; } void InsertPositionSamples(const std::vector& samples) override {} diff --git a/migrations/dlu/mysql/84_server_traffic.sql b/migrations/dlu/mysql/84_server_traffic.sql new file mode 100644 index 000000000..dc5818d02 --- /dev/null +++ b/migrations/dlu/mysql/84_server_traffic.sql @@ -0,0 +1,20 @@ +/* Per-minute traffic of each server (packets, bytes, HTTP requests and latency), written by the dashboard once a minute + from the servers' traffic reports and kept for traffic_days. server: master, auth, chat, dashboard, ugc or + world::. The latency columns are the HTTP latency percentiles of the minute in microseconds. */ +CREATE TABLE IF NOT EXISTS server_traffic ( + time BIGINT NOT NULL, + server VARCHAR(64) NOT NULL, + packets_in BIGINT UNSIGNED NOT NULL DEFAULT 0, + packets_out BIGINT UNSIGNED NOT NULL DEFAULT 0, + bytes_in BIGINT UNSIGNED NOT NULL DEFAULT 0, + bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0, + resends BIGINT UNSIGNED NOT NULL DEFAULT 0, + http_requests BIGINT UNSIGNED NOT NULL DEFAULT 0, + http_4xx BIGINT UNSIGNED NOT NULL DEFAULT 0, + http_5xx BIGINT UNSIGNED NOT NULL DEFAULT 0, + http_bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0, + latency_p50_us INT UNSIGNED NOT NULL DEFAULT 0, + latency_p95_us INT UNSIGNED NOT NULL DEFAULT 0, + latency_p99_us INT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (time, server) +); diff --git a/migrations/dlu/sqlite/67_server_traffic.sql b/migrations/dlu/sqlite/67_server_traffic.sql new file mode 100644 index 000000000..5dcd76ead --- /dev/null +++ b/migrations/dlu/sqlite/67_server_traffic.sql @@ -0,0 +1,18 @@ +/* Per-minute traffic of each server. See the MySQL migration. */ +CREATE TABLE IF NOT EXISTS server_traffic ( + time BIGINT NOT NULL, + server TEXT NOT NULL, + packets_in BIGINT NOT NULL DEFAULT 0, + packets_out BIGINT NOT NULL DEFAULT 0, + bytes_in BIGINT NOT NULL DEFAULT 0, + bytes_out BIGINT NOT NULL DEFAULT 0, + resends BIGINT NOT NULL DEFAULT 0, + http_requests BIGINT NOT NULL DEFAULT 0, + http_4xx BIGINT NOT NULL DEFAULT 0, + http_5xx BIGINT NOT NULL DEFAULT 0, + http_bytes_out BIGINT NOT NULL DEFAULT 0, + latency_p50_us INTEGER NOT NULL DEFAULT 0, + latency_p95_us INTEGER NOT NULL DEFAULT 0, + latency_p99_us INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (time, server) +);