Skip to content

Instantly share code, notes, and snippets.

@Alemiz112
Created March 17, 2025 13:26
Show Gist options
  • Save Alemiz112/e911bacb621d57fc62b9cdf8b30ed3e0 to your computer and use it in GitHub Desktop.
Save Alemiz112/e911bacb621d57fc62b9cdf8b30ed3e0 to your computer and use it in GitHub Desktop.
/*
* Copyright 2021 Alemiz
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cubemc.waterdog.proxycore.network;
import com.google.gson.JsonObject;
import dev.waterdog.waterdogpe.ProxyServer;
import dev.waterdog.waterdogpe.event.EventPriority;
import dev.waterdog.waterdogpe.event.defaults.InitialServerConnectedEvent;
import dev.waterdog.waterdogpe.event.defaults.PlayerDisconnectedEvent;
import dev.waterdog.waterdogpe.event.defaults.TransferCompleteEvent;
import dev.waterdog.waterdogpe.network.NetworkMetrics;
import dev.waterdog.waterdogpe.network.PacketDirection;
import dev.waterdog.waterdogpe.network.connection.client.ClientConnection;
import dev.waterdog.waterdogpe.player.ProxiedPlayer;
import io.prometheus.client.Counter;
import io.prometheus.client.Gauge;
import io.prometheus.client.exporter.HTTPServer;
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import it.unimi.dsi.fastutil.objects.ObjectSets;
import org.cloudburstmc.netty.channel.raknet.config.RakChannelMetrics;
import org.cloudburstmc.netty.channel.raknet.config.RakServerMetrics;
import java.io.IOException;
import java.net.InetSocketAddress;
public class CubeNetworkMetrics implements NetworkMetrics, RakServerMetrics, RakChannelMetrics {
// RakNet metrics
private static final Counter bytesIn = Counter.build()
.name("rak_bytes_in")
.help("RakNet inbound bytes")
.register();
private static final Counter bytesOut = Counter.build()
.name("rak_bytes_out")
.help("RakNet outbound bytes")
.register();
private static final Counter datagramsIn = Counter.build()
.name("rak_datagrams_in")
.help("RakNet inbound datagrams")
.register();
private static final Counter datagramsOut = Counter.build()
.name("rak_datagrams_out")
.help("RakNet outbound datagrams")
.register();
private static final Counter stateDatagrams = Counter.build()
.name("rak_stale_datagrams")
.help("RakNet sent stale datagrams counter")
.register();
private static final Counter ackIn = Counter.build()
.name("rak_ack_in")
.help("RakNet acknowledge inbound")
.register();
private static final Counter ackOut = Counter.build()
.name("rak_ack_out")
.help("RakNet acknowledge outbound")
.register();
private static final Counter nackIn = Counter.build()
.name("rak_nack_in")
.help("RakNet nacknowledge inbound")
.register();
private static final Counter nackOut = Counter.build()
.name("rak_nack_out")
.help("RakNet nacknowledge outbound")
.register();
// Bedrock metrics
private static final Counter compressedBytes = Counter.build().name("waterdog_compressed_bytes")
.labelNames("direction")
.help("Compressed bytes")
.register();
private static final Counter decompressedBytes = Counter.build().name("waterdog_decompressed_bytes")
.labelNames("direction")
.help("Decompressed bytes")
.register();
private static final Counter passThroughBytes = Counter.build().name("waterdog_pass_thought_bytes")
.labelNames("direction")
.help("Passed thought bytes")
.register();
private static final Counter encodedPackets = Counter.build().name("waterdog_encoded_packets")
.labelNames("direction")
.help("Encoded packets count")
.register();
private static final Counter passThroughPackets = Counter.build().name("waterdog_pass_thought_packets")
.labelNames("direction")
.help("Passed thought packets count")
.register();
// Player metrics
private static final Gauge playersCount = Gauge.build()
.name("waterdog_player_count")
.help("Waterdog player count")
.labelNames("server_address", "game_version")
.register();
private static final Counter playerLogins = Counter.build()
.name("waterdog_player_logins")
.help("Waterdog player login attempts")
.register();
private static final Counter playerTransfers = Counter.build()
.name("waterdog_player_transfer")
.help("Waterdog player transfers")
.register();
private static final Gauge playerAveragePing = Gauge.build()
.name("waterdog_player_avg_ping")
.help("Average player ping in mc").
register();
private static final Gauge downstreamAveragePing = Gauge.build()
.name("waterdog_server_avg_ping")
.help("Average server ping in mc").
register();
private final NetworkManager loader;
private HTTPServer server;
private final ObjectSet<String> trackedPlayers = ObjectSets.synchronize(new ObjectArraySet<>());
public CubeNetworkMetrics(NetworkManager loader, int port) {
this.loader = loader;
try {
this.server = new HTTPServer(port);
} catch (IOException e) {
this.loader.getLogger().error("Failed to start HTTP Prometheus metrics exporter on port " + port);
return;
}
ProxyServer proxy = this.loader.getLoader().getProxy();
proxy.getEventManager().subscribe(InitialServerConnectedEvent.class, this::onPostLogin, EventPriority.HIGH);
proxy.getEventManager().subscribe(PlayerDisconnectedEvent.class, this::onDisconnect, EventPriority.HIGH);
proxy.getEventManager().subscribe(TransferCompleteEvent.class, this::onTransfer);
proxy.getScheduler().scheduleRepeating(this::calculateAvgPing, 20);
}
public void shutdown() {
if (this.server != null) {
this.server.close();
}
}
private String getPlayerServerAddress(ProxiedPlayer player) {
JsonObject clientData = player.getLoginData().getClientData();
if (clientData.has("ServerAddress")) {
return clientData.get("ServerAddress").getAsString().split(":")[0].toLowerCase();
}
return "cubedmc.eu";
}
private String getPlayerVersion(ProxiedPlayer player) {
return player.getLoginData().getClientData().has("GameVersion") ?
player.getLoginData().getClientData().get("GameVersion").getAsString() :
player.getProtocol().getMinecraftVersion();
}
public void onLoginAttempt(InetSocketAddress address) {
playerLogins.inc();
}
private void onPostLogin(InitialServerConnectedEvent event) {
String address = this.getPlayerServerAddress(event.getPlayer());
String version = this.getPlayerVersion(event.getPlayer());
this.trackedPlayers.add(event.getPlayer().getName());
playersCount.labels(address, version).inc();
}
private void onDisconnect(PlayerDisconnectedEvent event) {
if (this.trackedPlayers.remove(event.getPlayer().getName())) {
String address = this.getPlayerServerAddress(event.getPlayer());
String version = this.getPlayerVersion(event.getPlayer());
playersCount.labels(address, version).dec();
}
}
private void onTransfer(TransferCompleteEvent event) {
playerTransfers.inc();
}
private void calculateAvgPing() {
ProxyServer proxy = this.loader.getLoader().getProxy();
int count = proxy.getPlayerManager().getPlayerCount();
int upstreamAvg = 0;
int downstreamAvg = 0;
for (ProxiedPlayer player : proxy.getPlayers().values()) {
upstreamAvg += player.getPing();
ClientConnection connection = player.getDownstreamConnection();
if (connection != null && connection.isConnected()) {
downstreamAvg += connection.getPing();
}
}
playerAveragePing.set((double) upstreamAvg / count);
downstreamAveragePing.set((double) downstreamAvg / count);
}
@Override
public void bytesIn(int count) {
bytesIn.inc(count);
}
@Override
public void bytesOut(int count) {
bytesOut.inc(count);
}
@Override
public void rakDatagramsIn(int count) {
datagramsIn.inc(count);
}
@Override
public void rakDatagramsOut(int count) {
datagramsOut.inc(count);
}
@Override
public void rakStaleDatagrams(int count) {
stateDatagrams.inc(count);
}
@Override
public void ackIn(int count) {
ackIn.inc(count);
}
@Override
public void ackOut(int count) {
ackOut.inc(count);
}
@Override
public void nackIn(int count) {
nackIn.inc(count);
}
@Override
public void nackOut(int count) {
nackOut.inc(count);
}
@Override
public void decompressedBytes(int count, PacketDirection direction) {
decompressedBytes.labels(direction.name()).inc(count);
}
@Override
public void compressedBytes(int count, PacketDirection direction) {
compressedBytes.labels(direction.name()).inc(count);
}
@Override
public void passedThroughBytes(int count, PacketDirection direction) {
passThroughBytes.labels(direction.name()).inc(count);
}
@Override
public void encodedPackets(int count, PacketDirection direction) {
encodedPackets.labels(direction.name()).inc(count);
}
@Override
public void passedThroughPackets(int count, PacketDirection direction) {
passThroughPackets.labels(direction.name()).inc(count);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment