Skip to content

feat(metrics): publish which platform Bedrock players are on - #44

Merged
hbrombeer merged 1 commit into
mainfrom
feat/bedrock-platform
Aug 23, 2026
Merged

feat(metrics): publish which platform Bedrock players are on#44
hbrombeer merged 1 commit into
mainfrom
feat/bedrock-platform

Conversation

@hbrombeer

Copy link
Copy Markdown
Member

Nothing in the estate could answer "are these players on a phone or a console". The device travels in a Bedrock client's login chain, Geyser hands it to Floodgate, and by the time Velocity sees the player they are an ordinary Java-protocol connection with the platform stripped off — the game servers cannot tell a Switch from a phone, and the Bedrock proxy is the last place that still knows.

What

On a proxy that also runs Floodgate — only velocity-bedrock does — the endpoint now publishes:

velocity_bedrock_players{device_os="ANDROID"}

by Floodgate's DeviceOs enum name: ANDROID, IOS, OSX, XBOX, NX, PS4, UWP, … Low cardinality, one series per platform per proxy.

Floodgate is read reflectively, not depended on

This plugin loads on every proxy and only one of them has Floodgate, so the dependency would be compileOnly anyway. And GeyserMC publishes the API as a SNAPSHOT only (2.2.5-SNAPSHOT, no releases) — the only thing a build dependency would buy is type safety against a moving artifact that could break the build of a plugin which has nothing to do with Bedrock.

Three calls do not justify that. The shape was checked against the published API jar, not assumed:

FloodgateApi.getInstance()            static
  .getPlayers()                       Collection<FloodgatePlayer>
FloodgatePlayer.getDeviceOs()         enum

The enum itself lives in yet another artifact, so it is read as a name and never resolved.

Four decisions worth arguing with

  • Probed on every read, not once at startup. Velocity's plugin classloaders can see each other but load order is not guaranteed, and this declares no dependency on Floodgate. Probing once would let a proxy that happened to start this plugin first conclude "no Floodgate" and stay wrong for the life of the pod. The successful lookup is cached, so it costs one field read after that — asserted by a test.
  • The platform is the enum's name, not its toString. Floodgate renders NX as "Nintendo Switch". A label that changes case and spacing between releases silently splits one series into two, and neither half is the whole truth afterwards.
  • A proxy without Floodgate publishes no series at all, rather than zero. "No Bedrock players" and "cannot see Bedrock players" are different states, and only one belongs on a graph — this also keeps the Java proxies out of a Bedrock panel entirely.
  • A platform that empties keeps its row and reports 0. Dropping it would make the series stale, which Grafana draws as a gap — indistinguishable from the endpoint being down, and exactly wrong for the number that says "nobody is playing on a Switch".

Rows are refreshed inside the HTTP handler rather than on a timer: the endpoint is the only reader, so a scrape sees the count as it is at that moment and an unscraped proxy pays nothing.

Verification

./gradlew build green — spotless + 94 tests, 12 new.

The reflection runs against a Floodgate stand-in handed in through the class lookup, so what is tested is the code that ships rather than a copy of it written twice. Covered: counting by platform, a player whose device Floodgate does not know being skipped rather than labelled, the enum-name-not-display-string rule, the empty-map cases (no players / Floodgate loaded but not started), a Floodgate that changed shape degrading instead of throwing — a NoSuchMethodError escaping here would take the whole scrape with it — the class being resolved once, and at the endpoint level: the series appearing with the right values, re-read on every scrape, going to 0 rather than vanishing, and being absent entirely on a proxy without Floodgate (which is the real absence, since Floodgate is not on the test classpath).

Follow-ups

  1. deploy — bump the plugin-floodgate… no: bump the plugin-proxy image tag once this releases, so velocity-bedrock fetches the new jar. Same jar-fetch ordering caveat as last time: the proxy pulls the plugin at startup from the sibling Service, so the jar server has to be new before the proxy restarts.
  2. grounds-pulumi — a platform breakdown panel on the Bedrock dashboard.

Nothing in the estate could answer "are these players on a phone or a
console". The device travels in a Bedrock client's login chain, Geyser hands
it to Floodgate, and by the time Velocity sees the player they are an ordinary
Java-protocol connection with the platform stripped off — the game servers
cannot tell a Switch from a phone, and the Bedrock proxy was the last place
that still knew.

On a proxy that also runs Floodgate — only velocity-bedrock does — the
endpoint now publishes:

  velocity_bedrock_players{device_os="ANDROID"}   by Floodgate's DeviceOs enum
                                                  name: ANDROID, IOS, OSX,
                                                  XBOX, NX, PS4, UWP, …

Floodgate is read reflectively rather than depended on. This plugin loads on
every proxy and only one of them has Floodgate, so the dependency would be
compileOnly anyway — and GeyserMC publishes the API as a SNAPSHOT only, so the
only thing it would buy is type safety against a moving artifact that could
break the build of a plugin which has nothing to do with Bedrock. Three calls
do not justify that. The shape was checked against the published API jar
rather than assumed.

Four decisions worth arguing with:

  - The class is probed on every read, not once at startup. Velocity's plugin
    classloaders can see each other but load order is not guaranteed, and this
    declares no dependency on Floodgate — probing once would let a proxy that
    happened to start this plugin first conclude "no Floodgate" and stay wrong
    for the life of the pod.
  - The platform is the enum's `name`, not its `toString`. Floodgate renders
    NX as "Nintendo Switch"; a label that changes case and spacing between
    releases splits one series into two.
  - A proxy without Floodgate publishes no series at all rather than zero.
    "No Bedrock players" and "cannot see Bedrock players" are different
    states, and only one of them belongs on a graph.
  - A platform that empties keeps its row and reports 0. Dropping it would
    make the series stale, which Grafana draws as a gap — indistinguishable
    from the endpoint being down.

Rows are refreshed inside the HTTP handler rather than on a timer: the
endpoint is the only reader, so a scrape sees the count as it is at that
moment and an unscraped proxy pays nothing.

94 tests, 12 of them new. The reflection runs against a Floodgate stand-in
handed in through the class lookup, so what is tested is the code that ships
rather than a copy of it; the not-installed path runs against Floodgate's real
absence from the test classpath.
@hbrombeer
hbrombeer merged commit 9e2ba6a into main Aug 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant