Files
core-vitals/docs/04_Metrics.md
T
2026-05-26 00:11:45 -04:00

3.9 KiB

Metrics

What you get

A MetricsService with three metric types and a Prometheus-compatible HTTP endpoint that scrapers (Prometheus, Grafana Agent, Vector) can read every N seconds.

Metric state lives in JVM memory only. It resets when the server restarts. Persistence is the scraper's job - see 09_Operations_And_Security.md for the rationale.

API

public interface MetricsService {
    Counter counter(String name, Map<String, String> labels);
    Gauge   gauge(String name, Map<String, String> labels);
    Timer   timer(String name, Map<String, String> labels);
}

All three are interned by (name, labels). Calling counter("matches_started", Map.of("game", "bridge_duel")) twice returns the same instance, so it's safe to call inside a hot path.

Counter

Monotonically increasing.

Counter started = metrics.counter("matches_started", Map.of("game", "bridge_duel"));
started.inc();
started.inc(5);
double v = started.value();

Gauge

Point-in-time value. Set explicitly or bind to a supplier that's called whenever Prometheus scrapes.

Gauge queueLen = metrics.gauge("queue_length", Map.of("game", "bridge_duel"));
queueLen.set(42);

Gauge memUsed = metrics.gauge("custom_memory_bytes", Map.of("pool", "off_heap"));
memUsed.bind(() -> myAllocator.currentBytes());

bind is preferred when the value is computed from somewhere else - it dodges write-paths and gets the current value at scrape time.

Timer

Records durations. Tracks count and total seconds.

Timer dbQuery = metrics.timer("db_query_seconds", Map.of("query", "find_player"));

long start = System.nanoTime();
runQuery();
dbQuery.recordNanos(System.nanoTime() - start);

The Prometheus output for a timer is two series: <name>_count and <name>_sum_seconds. To get the average in PromQL: rate(db_query_seconds_sum_seconds[5m]) / rate(db_query_seconds_count[5m]).

Naming

Metric names get sanitized to [a-zA-Z0-9_] (anything else becomes _). Stick to lowercase snake_case to avoid surprises. Don't include label values in the name - use labels for that. Example:

Good: matches_started{game="bridge_duel"} Bad: matches_started_bridge_duel

Labels with high cardinality (player UUIDs, match IDs, timestamps) will blow up Prometheus storage. Stick to bounded values: game id, world name, role.

Built-in metrics

NetworkCore registers these on its own. You'll see them in the output without doing anything.

Name Type Labels Source
players_online gauge none Universe.get().getPlayerCount()
jvm_memory_used_bytes gauge pool=<pool name> JVM MemoryPoolMXBean
messagebus_published_total counter none every MessageBus.publish call
messagebus_received_total counter none every accepted incoming message
messagebus_rejected_total counter none every message rejected due to bad auth token
rpc_request_total counter service, method, result every RPC completion (result is ok, error, timeout, or decode_error)
rpc_request_duration_seconds timer service, method every RPC completion

The endpoint

GET http://<bind>:<port><path>

Defaults: http://127.0.0.1:9100/metrics. Response is Prometheus text format 0.0.4. Bind defaults to loopback - you have to opt into exposing it externally (see 09_Operations_And_Security.md).

Prometheus scrape config

scrape_configs:
  - job_name: 'kweebec_servers'
    static_configs:
      - targets:
          - '10.0.0.10:9100'
          - '10.0.0.11:9100'
          - '10.0.0.12:9100'

When not to use this

Don't store anything you want to query later (like "what was Bob's match count yesterday"). That's a database, not a metrics system. Use MetricsService for things you want to graph or alert on - rates, latencies, queue depths, error counts.