feat(traffic): count traffic by peer and by connection

Every server now splits its packet counts by peer: its own connections
(players on auth and worlds), its master link, or other servers (the
worlds on chat, every server on master, a world's chat link, which is
now counted too). HTTP requests carrying X-Darkflame-Server count as
another server's, the dashboard counts its own requests to the UGC
server, and each HTTP client address is counted. The report also gets
each RakNet connection's statistics (worlds name the player on it),
trimmed to the 32 busiest with the rest summed. Counting stays on the
main loop, except the dashboard's UGC fetches, which only touch the
locked recorder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 18:23:31 -05:00
parent 8a53562629
commit 9e8f161dd8
10 changed files with 273 additions and 27 deletions

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <cmath>
#include <ctime>
#include <tuple>
#include "MessageIdentifiers.h"
#include "ServiceType.h"
@@ -134,7 +135,15 @@ namespace TrafficStats {
return key;
}
void PeerCounts::Merge(const PeerCounts& other) {
packetsIn += other.packetsIn;
packetsOut += other.packetsOut;
bytesIn += other.bytesIn;
bytesOut += other.bytesOut;
}
void Second::Merge(const Second& other) {
for (size_t i = 0; i < PEER_CLASSES; i++) peers[i].Merge(other.peers[i]);
packetsIn += other.packetsIn;
packetsOut += other.packetsOut;
bytesIn += other.bytesIn;
@@ -143,6 +152,10 @@ namespace TrafficStats {
AddArrays(httpStatus, other.httpStatus);
httpBytesOut += other.httpBytesOut;
httpLatency.Merge(other.httpLatency);
httpFromServers += other.httpFromServers;
httpFromServersBytesOut += other.httpFromServersBytesOut;
httpOutRequests += other.httpOutRequests;
httpOutBytesIn += other.httpOutBytesIn;
}
void RouteStats::Merge(const RouteStats& other) {
@@ -152,16 +165,21 @@ namespace TrafficStats {
latency.Merge(other.latency);
}
void Recorder::Packet(int64_t now, const MessageKey& key, uint64_t bytes, uint32_t fanout) {
void Recorder::Packet(int64_t now, const MessageKey& key, uint64_t bytes, uint32_t fanout, Peer peer) {
if (fanout == 0) return;
std::lock_guard lock(m_Mutex);
auto& second = SecondAt(now);
auto& side = second.peers[std::min<size_t>(static_cast<size_t>(peer), PEER_CLASSES - 1)];
if (key.outbound) {
second.packetsOut += fanout;
second.bytesOut += bytes * fanout;
side.packetsOut += fanout;
side.bytesOut += bytes * fanout;
} else {
second.packetsIn += fanout;
second.bytesIn += bytes * fanout;
side.packetsIn += fanout;
side.bytesIn += bytes * fanout;
}
auto& message = m_Messages[key.Packed()];
message.key = key;
@@ -169,9 +187,58 @@ namespace TrafficStats {
message.bytes += bytes * fanout;
}
void Recorder::Http(int64_t now, const std::string& route, uint16_t status, uint64_t microseconds, uint64_t bytesOut) {
void Connection::Merge(const Connection& other) {
packetsIn += other.packetsIn;
packetsOut += other.packetsOut;
bytesIn += other.bytesIn;
bytesOut += other.bytesOut;
resends += other.resends;
}
void TrimConnections(Report& report, size_t limit) {
report.hasConnections = true;
auto& list = report.connections;
std::sort(list.begin(), list.end(), [](const Connection& a, const Connection& b) {
return a.Bytes() != b.Bytes() ? a.Bytes() > b.Bytes() : std::tie(a.address, a.port) < std::tie(b.address, b.port);
});
for (size_t i = limit; i < list.size(); i++) {
report.otherConnections.Merge(list[i]);
report.otherConnectionCount++;
}
if (list.size() > limit) list.resize(limit);
}
void Recorder::HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut) {
std::lock_guard lock(m_Mutex);
auto it = m_HttpClients.find(address);
if (it == m_HttpClients.end()) {
const bool full = m_HttpClients.size() >= MAX_HTTP_CLIENTS;
it = m_HttpClients.try_emplace(full ? std::string() : address).first;
it->second.address = it->first;
it->second.http = true;
}
auto& client = it->second;
if (fromServer) client.peer = Peer::SERVERS;
client.packetsIn++;
client.packetsOut++;
client.bytesIn += bytesIn;
client.bytesOut += bytesOut;
}
void Recorder::HttpOut(int64_t now, uint64_t bytesIn) {
std::lock_guard lock(m_Mutex);
auto& second = SecondAt(now);
second.httpOutRequests++;
second.httpOutBytesIn += bytesIn;
}
void Recorder::Http(int64_t now, const std::string& route, uint16_t status, uint64_t microseconds, uint64_t bytesOut, bool fromServer) {
std::lock_guard lock(m_Mutex);
auto& second = SecondAt(now);
if (fromServer) {
second.httpFromServers++;
second.httpFromServersBytesOut += bytesOut;
}
second.httpRequests++;
second.httpStatus[StatusClass(status)]++;
second.httpBytesOut += bytesOut;
@@ -221,6 +288,7 @@ namespace TrafficStats {
Report Recorder::Take(int64_t now) {
Report report;
report.peerSplit = true;
std::vector<std::pair<std::string, std::function<double()>>> gauges;
{
std::lock_guard lock(m_Mutex);
@@ -245,6 +313,15 @@ namespace TrafficStats {
for (auto& [_, route] : m_Routes) report.routes.push_back(std::move(route));
m_Routes.clear();
for (auto& [address, client] : m_HttpClients) {
if (address.empty()) { // the ones over MAX_HTTP_CLIENTS
report.otherConnections.Merge(client);
report.otherConnectionCount++;
} else {
report.connections.push_back(std::move(client));
}
}
m_HttpClients.clear();
gauges = m_Gauges;
}
for (const auto& [name, source] : gauges) report.gauges.emplace_back(name, source ? source() : 0.0);

View File

@@ -55,6 +55,9 @@ namespace TrafficStats {
uint64_t m_Sum{};
};
// The header a server puts on HTTP requests it makes to another server's web server, naming itself
constexpr const char* SERVER_HEADER = "X-Darkflame-Server";
// HTTP status classes 1xx..5xx as index 0..4 (anything else counts as 5xx)
size_t StatusClass(uint16_t status);
@@ -75,6 +78,25 @@ namespace TrafficStats {
// Reads the key from a packet's first bytes (never reads past `length`)
MessageKey KeyOf(const uint8_t* data, size_t length, bool outbound);
/**
* Which side of a server a packet went to or came from, so the dashboard can draw each link: its own connections
* (players' game clients on auth and worlds), its link to master, or other servers (every server on master, the
* worlds on chat, chat on a world).
*/
enum class Peer : uint8_t { CLIENTS = 0, MASTER = 1, SERVERS = 2 };
constexpr size_t PEER_CLASSES = 3;
struct PeerCounts {
uint64_t packetsIn{};
uint64_t packetsOut{};
uint64_t bytesIn{};
uint64_t bytesOut{};
void Merge(const PeerCounts& other);
bool Empty() const { return packetsIn == 0 && packetsOut == 0; }
bool operator==(const PeerCounts&) const = default;
};
struct MessageCount {
MessageKey key;
uint64_t count{};
@@ -92,6 +114,11 @@ namespace TrafficStats {
std::array<uint64_t, 5> httpStatus{}; // by StatusClass
uint64_t httpBytesOut{};
Histogram httpLatency;
std::array<PeerCounts, PEER_CLASSES> peers{}; // the packets above split by Peer
uint64_t httpFromServers{}; // of httpRequests, those another server made (X-Darkflame-Server)
uint64_t httpFromServersBytesOut{}; // their response bodies
uint64_t httpOutRequests{}; // HTTP requests this server made to another server's web server
uint64_t httpOutBytesIn{}; // the bodies those answered with
void Merge(const Second& other); // adds the counts (keeps this one's time)
bool Idle() const { return packetsIn == 0 && packetsOut == 0 && httpRequests == 0; }
@@ -119,23 +146,60 @@ namespace TrafficStats {
uint32_t averagePingMs{}; // over the connections
};
/**
* One remote end of a server over a report: a RakNet connection (RakNet's own counts: datagrams, with
* acknowledgements and resends) or an HTTP client address (requests and body bytes). Personal data: the dashboard
* keeps these in memory only and shows addresses only with network_ips.
*/
struct Connection {
std::string address; // "203.0.113.5" or an IPv6 address; "" for the sum of the ones left out
uint16_t port{}; // 0 for HTTP clients (one entry per address)
Peer peer{};
bool http{};
uint64_t packetsIn{}, packetsOut{}; // datagrams, or HTTP requests and responses
uint64_t bytesIn{}, bytesOut{};
uint32_t resends{};
uint32_t pingMs{};
uint32_t accountId{}; // a logged-in player's (world servers), else 0
uint64_t characterId{};
std::string account, character;
uint64_t Bytes() const { return bytesIn + bytesOut; }
void Merge(const Connection& other); // adds the counts
};
struct Report {
std::vector<Second> seconds; // oldest first, one per second without gaps
std::vector<MessageCount> messages; // the busiest types per direction
std::vector<RouteStats> routes;
Link link;
std::vector<std::pair<std::string, double>> gauges; // e.g. workers_busy
bool peerSplit{}; // whether the seconds' `peers` are filled (reports of older servers don't have them)
std::vector<Connection> connections; // the busiest remote ends, busiest first
Connection otherConnections; // the rest summed
uint32_t otherConnectionCount{};
bool hasConnections{}; // false in reports of older servers
};
// Keeps the `limit` busiest connections of the report, sums the rest into otherConnections
void TrimConnections(Report& report, size_t limit);
class Recorder {
public:
static constexpr size_t TOP_MESSAGES = 24; // per direction, per report
static constexpr size_t MAX_ROUTES = 64; // distinct routes per report; more count as "other"
static constexpr int64_t MAX_GAP = 120; // seconds of silence a report fills in at most
static constexpr size_t MAX_HTTP_CLIENTS = 1024; // addresses per report; more count as one
static constexpr size_t TOP_CONNECTIONS = 32; // connections a report names; the rest are summed
// `fanout`: how many connections a broadcast went to
void Packet(int64_t now, const MessageKey& key, uint64_t bytes, uint32_t fanout = 1);
void Http(int64_t now, const std::string& route, uint16_t status, uint64_t microseconds, uint64_t bytesOut);
// `fanout`: how many connections a broadcast went to; `peer`: which side it went to or came from
void Packet(int64_t now, const MessageKey& key, uint64_t bytes, uint32_t fanout = 1, Peer peer = Peer::CLIENTS);
// `fromServer`: the request came from another server (it sent SERVER_HEADER), not a browser or game client
void Http(int64_t now, const std::string& route, uint16_t status, uint64_t microseconds, uint64_t bytesOut, bool fromServer = false);
// A request this server made to another server's web server (the dashboard to the UGC server); any thread
void HttpOut(int64_t now, uint64_t bytesIn);
// An HTTP client's request by its address (bytes of the request and of the answer's body)
void HttpClient(const std::string& address, bool fromServer, uint64_t bytesIn, uint64_t bytesOut);
// Evaluated when a report is taken (on the thread that takes it)
void SetGauge(const std::string& name, std::function<double()> source);
@@ -159,6 +223,7 @@ namespace TrafficStats {
int64_t m_LastReported{}; // last second a report covered
std::unordered_map<uint64_t, MessageCount> m_Messages;
std::map<std::string, RouteStats> m_Routes;
std::unordered_map<std::string, Connection> m_HttpClients;
std::vector<std::pair<std::string, std::function<double()>>> m_Gauges;
int64_t m_LastTake{};
};