This commit is contained in:
2026-05-26 00:11:45 -04:00
commit 296c233157
41 changed files with 2829 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# 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](09_Operations_And_Security.md) for the rationale.
## API
```java
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.
```java
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.
```java
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.
```java
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](09_Operations_And_Security.md)).
## Prometheus scrape config
```yaml
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.