mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(dashboard): Network summary has each server's port and machine
The dashboard keeps master's server list endpoints and joins them into the Network summary: its listening port and its machine as a token, since the summary goes to every open page. The connection list maps the tokens to addresses for viewers with network_ips. The dashboard reports its web port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
#include "WorldView.h"
|
||||
#include "PrometheusMetrics.h"
|
||||
#include "Traffic.h"
|
||||
#include "TrafficHistory.h"
|
||||
#include "master/ServerTraffic.h"
|
||||
#include "master/Profiling.h"
|
||||
#include "Performance.h"
|
||||
@@ -135,6 +136,7 @@ namespace ServerState {
|
||||
uint32_t g_UgcPid{};
|
||||
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
||||
std::vector<WorldInstanceInfo> g_PendingWorlds{};
|
||||
std::map<std::string, ServerEndpoint> g_Endpoints{};
|
||||
std::mutex g_StatusMutex{};
|
||||
}
|
||||
|
||||
@@ -238,6 +240,12 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
ServerState::g_Endpoints.clear();
|
||||
for (const auto& endpoint : list.endpoints) {
|
||||
const auto key = TrafficHistory::KeyFor(static_cast<uint16_t>(endpoint.type), endpoint.zoneID, endpoint.instanceID);
|
||||
ServerState::g_Endpoints[key] = { endpoint.ip.string, endpoint.port, endpoint.host.string };
|
||||
}
|
||||
|
||||
LOG_DEBUG("Received server list: auth=%s chat=%s ugc=%s worlds=%u",
|
||||
list.authOnline ? "online" : "offline",
|
||||
list.chatOnline ? "online" : "offline",
|
||||
@@ -451,6 +459,8 @@ int main(int argc, char** argv) {
|
||||
&Game::lastSignal,
|
||||
masterPassword
|
||||
);
|
||||
// Master (and the Network page) show the port browsers use
|
||||
g_Server->SetAdvertisedPort(ourPort);
|
||||
Game::server = g_Server;
|
||||
|
||||
// What the worker threads read from the CDClient and the settings, read now on this thread: workers never query
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace NetworkView {
|
||||
return out;
|
||||
}
|
||||
|
||||
nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label) {
|
||||
nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label, const Endpoints& endpoints, uint64_t salt) {
|
||||
nlohmann::json servers = nlohmann::json::object();
|
||||
for (const auto& [key, server] : history.Servers()) {
|
||||
if (now - server.lastSeen > onlineSeconds || server.seconds.empty()) continue;
|
||||
@@ -62,6 +62,10 @@ namespace NetworkView {
|
||||
entry["type"] = std::string(magic_enum::enum_name(static_cast<ServiceType>(server.type)));
|
||||
entry["zone"] = server.zoneId;
|
||||
entry["instance"] = server.instanceId;
|
||||
const auto endpoint = endpoints.find(key);
|
||||
const bool known = endpoint != endpoints.end();
|
||||
entry["port"] = known && endpoint->second.port ? nlohmann::json(endpoint->second.port) : nlohmann::json(nullptr);
|
||||
entry["host"] = known && !endpoint->second.host.empty() ? nlohmann::json(MaskAddress(endpoint->second.host, salt)) : nlohmann::json(nullptr);
|
||||
entry["http"] = Rate(p.httpRequests, WINDOW);
|
||||
entry["http_bytes_out"] = Rate(p.httpBytesOut, WINDOW);
|
||||
entry["link"] = { {"connections", server.link.connections}, {"ping_ms", server.link.averagePingMs}, {"resends", server.link.resends},
|
||||
@@ -88,7 +92,8 @@ namespace NetworkView {
|
||||
return { {"time", now}, {"window", WINDOW}, {"servers", servers} };
|
||||
}
|
||||
|
||||
nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label) {
|
||||
nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label,
|
||||
const Endpoints& endpoints) {
|
||||
struct Group {
|
||||
std::string address;
|
||||
std::string kind = "server";
|
||||
@@ -173,6 +178,13 @@ namespace NetworkView {
|
||||
}
|
||||
list.push_back(std::move(entry));
|
||||
}
|
||||
return { {"time", now}, {"addresses_shown", showAddresses}, {"reported", anyReported}, {"peers", list}, {"others", others} };
|
||||
// The machines the Summary names by token
|
||||
nlohmann::json hosts = nlohmann::json::object();
|
||||
if (showAddresses) {
|
||||
for (const auto& [_, endpoint] : endpoints) {
|
||||
if (!endpoint.host.empty()) hosts[MaskAddress(endpoint.host, salt)] = endpoint.host;
|
||||
}
|
||||
}
|
||||
return { {"time", now}, {"addresses_shown", showAddresses}, {"reported", anyReported}, {"peers", list}, {"others", others}, {"hosts", hosts} };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "json.hpp"
|
||||
@@ -17,22 +18,34 @@ namespace NetworkView {
|
||||
// A server's name for people ("World 1200 Nimbus Station #3")
|
||||
using Labeler = std::function<std::string(const TrafficHistory::Server& server)>;
|
||||
|
||||
// Where a server listens and the machine it runs on, from master's server list
|
||||
struct Endpoint {
|
||||
uint32_t port{};
|
||||
std::string host; // the machine's address as master sees it (empty: unknown)
|
||||
};
|
||||
// By TrafficHistory key ("master", "auth", "world:1200:3")
|
||||
using Endpoints = std::map<std::string, Endpoint>;
|
||||
|
||||
/**
|
||||
* Every server that reported in the last `onlineSeconds`, by key: its packets and bytes per second in and out over
|
||||
* its last WINDOW reported seconds, HTTP requests, RakNet link statistics, gauges, and the split of the packets by
|
||||
* peer (clients, master, servers) with its HTTP requests from and to other servers, or split: null when its reports
|
||||
* don't have one (an older server). Never holds addresses.
|
||||
* don't have one (an older server). With `endpoints`, its listening `port` and its machine as `host`, a token
|
||||
* (MaskAddress with `salt`; null when not known): the summary goes to every viewer, so it never holds addresses.
|
||||
*/
|
||||
nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label);
|
||||
nlohmann::json Summary(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, const Labeler& label,
|
||||
const Endpoints& endpoints = {}, uint64_t salt = 0);
|
||||
|
||||
/**
|
||||
* The remote ends of every server (from each one's last report): game clients by player (else by RakNet connection),
|
||||
* HTTP clients of the dashboard and the UGC server by address and signed-in account (`user`, the dashboard account
|
||||
* the requests were signed in as), and the servers' own links. Rates per second. Account and user names are always
|
||||
* included; without `showAddresses` each address is replaced by a token that stays the same for the same address
|
||||
* and `salt`, and ports are left out.
|
||||
* and `salt`, and ports are left out. With `showAddresses`, `hosts` maps each machine's token in the Summary to its
|
||||
* address (empty otherwise).
|
||||
*/
|
||||
nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label);
|
||||
nlohmann::json Connections(const TrafficHistory& history, int64_t now, int64_t onlineSeconds, bool showAddresses, uint64_t salt, const Labeler& label,
|
||||
const Endpoints& endpoints = {});
|
||||
|
||||
// "peer-" and 8 hex digits: the same for the same address and salt, and says nothing about the address
|
||||
std::string MaskAddress(const std::string& address, uint64_t salt);
|
||||
|
||||
@@ -44,6 +44,13 @@ struct WorldInstanceInfo {
|
||||
std::string state{};
|
||||
};
|
||||
|
||||
// Where a server listens and the machine it runs on, from master's server list
|
||||
struct ServerEndpoint {
|
||||
std::string ip{};
|
||||
uint32_t port{0};
|
||||
std::string host{};
|
||||
};
|
||||
|
||||
namespace ServerState {
|
||||
extern ServerStatus g_AuthStatus;
|
||||
extern ServerStatus g_ChatStatus;
|
||||
@@ -54,6 +61,8 @@ namespace ServerState {
|
||||
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
||||
// Worlds master launched that aren't connected yet, or that are shutting down (kept apart: not running)
|
||||
extern std::vector<WorldInstanceInfo> g_PendingWorlds;
|
||||
// Every server master listed, by traffic key ("master", "auth", "world:1200:3")
|
||||
extern std::map<std::string, ServerEndpoint> g_Endpoints;
|
||||
extern std::mutex g_StatusMutex;
|
||||
|
||||
inline nlohmann::json GetServerStateJson() {
|
||||
|
||||
@@ -278,11 +278,6 @@ namespace {
|
||||
{"messages_since", since}, {"routes", routes} };
|
||||
}
|
||||
|
||||
// What open pages get every few seconds (the Network page draws it; Diagnostics reloads on it)
|
||||
nlohmann::json Summary(int64_t now) {
|
||||
return NetworkView::Summary(g_History, now, ONLINE_SECONDS, LabelOf);
|
||||
}
|
||||
|
||||
uint64_t AddressSalt() {
|
||||
static const uint64_t salt = [] {
|
||||
std::random_device device;
|
||||
@@ -291,6 +286,19 @@ namespace {
|
||||
return salt;
|
||||
}
|
||||
|
||||
// Each server's listening port and machine, from master's last server list
|
||||
NetworkView::Endpoints Endpoints() {
|
||||
NetworkView::Endpoints endpoints;
|
||||
std::lock_guard lock(ServerState::g_StatusMutex);
|
||||
for (const auto& [key, endpoint] : ServerState::g_Endpoints) endpoints[key] = { endpoint.port, endpoint.host };
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
// What open pages get every few seconds (the Network page draws it; Diagnostics reloads on it)
|
||||
nlohmann::json Summary(int64_t now) {
|
||||
return NetworkView::Summary(g_History, now, ONLINE_SECONDS, LabelOf, Endpoints(), AddressSalt());
|
||||
}
|
||||
|
||||
// One server for the Network page's detail panel: what ServerInfo says, its busiest message types each way over the
|
||||
// last 5 minutes, and its packets and bytes per second over the last 10 minutes (5 second steps)
|
||||
nlohmann::json NetworkServer(const std::string& key, int64_t now) {
|
||||
@@ -448,7 +456,7 @@ namespace Traffic {
|
||||
TrafficStats::Local().SetGauge("workers_threads", [] { return static_cast<double>(Workers::Pool().Threads()); });
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/diagnostics/network", Perm(PERMISSION),
|
||||
"The Network page's live view: every reporting server's packets and bytes per second over its last report, its split by peer (clients, master, other servers; null for servers too old to report one), HTTP requests, RakNet link statistics and gauges",
|
||||
"The Network page's live view: every reporting server's packets and bytes per second over its last report, its split by peer (clients, master, other servers; null for servers too old to report one), HTTP requests, RakNet link statistics, gauges, its listening port and its machine (host: a token; the connections route maps it to the address with network_ips)",
|
||||
[](HTTPReply& reply, const HTTPContext&) {
|
||||
JsonSuccess(reply, Summary(TrafficStats::Now()));
|
||||
});
|
||||
@@ -462,9 +470,9 @@ namespace Traffic {
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/diagnostics/network/connections", Perm(PERMISSION),
|
||||
"Every server's remote ends from its last report, grouped by address (game clients' RakNet connections, HTTP clients, server links), with rates, ping and the logged-in player. Addresses only with network_ips; otherwise a token per address",
|
||||
"Every server's remote ends from its last report, grouped by address (game clients' RakNet connections, HTTP clients, server links), with rates, ping and the logged-in player. Addresses and remote ports only with network_ips; otherwise a token per address. hosts: each machine token of the summary to its address (network_ips only)",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
JsonSuccess(reply, NetworkView::Connections(g_History, TrafficStats::Now(), ONLINE_SECONDS, Can(context, IPS_PERMISSION), AddressSalt(), LabelOf));
|
||||
JsonSuccess(reply, NetworkView::Connections(g_History, TrafficStats::Now(), ONLINE_SECONDS, Can(context, IPS_PERMISSION), AddressSalt(), LabelOf, Endpoints()));
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/diagnostics/traffic", Perm(PERMISSION),
|
||||
|
||||
@@ -183,3 +183,60 @@ TEST(NetworkViewTest, WebClientsBySignedInAccountWithTheirUserName) {
|
||||
EXPECT_EQ(hidden["peers"][1]["user"], "bob");
|
||||
for (const auto& p : hidden["peers"]) EXPECT_EQ(p["address"], NetworkView::MaskAddress("198.51.100.7", 5));
|
||||
}
|
||||
|
||||
TEST(NetworkViewTest, ListeningPortsAndMachinesFromTheServerList) {
|
||||
// Auth and a world on master's machine, a second world on another; chat is reporting but not in the list yet
|
||||
TrafficHistory history;
|
||||
history.Ingest(static_cast<uint16_t>(ServiceType::AUTH), 0, 0, WorldReport(true), NOW);
|
||||
history.Ingest(static_cast<uint16_t>(ServiceType::WORLD), 1200, 3, WorldReport(true), NOW);
|
||||
history.Ingest(static_cast<uint16_t>(ServiceType::WORLD), 1200, 4, WorldReport(true), NOW);
|
||||
history.Ingest(static_cast<uint16_t>(ServiceType::CHAT), 0, 0, WorldReport(true), NOW);
|
||||
const NetworkView::Endpoints endpoints{ { "auth", { 1001, "192.0.2.1" } }, { "world:1200:3", { 3015, "192.0.2.1" } },
|
||||
{ "world:1200:4", { 3016, "198.51.100.20" } }, { "master", { 2000, "192.0.2.1" } } };
|
||||
|
||||
const auto json = NetworkView::Summary(history, NOW, 20, Label, endpoints, 77);
|
||||
const auto& servers = json["servers"];
|
||||
EXPECT_EQ(servers["auth"]["port"], 1001);
|
||||
EXPECT_EQ(servers["world:1200:3"]["port"], 3015);
|
||||
EXPECT_EQ(servers["world:1200:4"]["port"], 3016);
|
||||
// The machine is a token, the same for servers on the same machine
|
||||
EXPECT_EQ(servers["auth"]["host"], NetworkView::MaskAddress("192.0.2.1", 77));
|
||||
EXPECT_EQ(servers["auth"]["host"], servers["world:1200:3"]["host"]);
|
||||
EXPECT_EQ(servers["world:1200:4"]["host"], NetworkView::MaskAddress("198.51.100.20", 77));
|
||||
EXPECT_NE(servers["auth"]["host"], servers["world:1200:4"]["host"]);
|
||||
// Not in the list: no port or machine
|
||||
EXPECT_TRUE(servers["chat"]["port"].is_null());
|
||||
EXPECT_TRUE(servers["chat"]["host"].is_null());
|
||||
// It goes to everyone: no addresses
|
||||
const auto dump = json.dump();
|
||||
EXPECT_EQ(dump.find("192.0.2.1"), std::string::npos);
|
||||
EXPECT_EQ(dump.find("198.51.100.20"), std::string::npos);
|
||||
|
||||
// The connection list names the machines only with network_ips
|
||||
const auto shown = NetworkView::Connections(history, NOW, 20, true, 77, Label, endpoints);
|
||||
ASSERT_EQ(shown["hosts"].size(), 2u);
|
||||
EXPECT_EQ(shown["hosts"][NetworkView::MaskAddress("192.0.2.1", 77)], "192.0.2.1");
|
||||
EXPECT_EQ(shown["hosts"][NetworkView::MaskAddress("198.51.100.20", 77)], "198.51.100.20");
|
||||
const auto hidden = NetworkView::Connections(history, NOW, 20, false, 77, Label, endpoints);
|
||||
EXPECT_TRUE(hidden["hosts"].empty());
|
||||
EXPECT_EQ(hidden.dump().find("192.0.2.1"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(NetworkViewTest, RemotePortsOnlyWithAddresses) {
|
||||
// A player's and a server link's remote port: shown with network_ips, left out without
|
||||
TrafficHistory history;
|
||||
auto world = WorldReport(true);
|
||||
world.connections = { Connection{ .address = "203.0.113.9", .port = 51234, .peer = Peer::CLIENTS, .bytesIn = 10, .accountId = 3, .account = "carol" },
|
||||
Connection{ .address = "192.0.2.1", .port = 2000, .peer = Peer::MASTER, .bytesIn = 10 } };
|
||||
world.hasConnections = true;
|
||||
history.Ingest(static_cast<uint16_t>(ServiceType::WORLD), 1200, 3, world, NOW);
|
||||
for (const bool show : { true, false }) {
|
||||
const auto json = NetworkView::Connections(history, NOW, 20, show, 1, Label);
|
||||
ASSERT_EQ(json["peers"].size(), 2u);
|
||||
for (const auto& p : json["peers"]) {
|
||||
const auto& port = p["servers"][0]["port"];
|
||||
if (!show) EXPECT_TRUE(port.is_null());
|
||||
else EXPECT_EQ(port, p["kind"] == "game" ? 51234 : 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user