# 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 labels); Gauge gauge(String name, Map labels); Timer timer(String name, Map 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: `_count` and `_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=` | 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://: ``` 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.