Where the numbers are

zone-server serves its metrics as Prometheus text on a port of their own:

  curl -s 127.0.0.1:8092/metrics | grep ^zone_
  

The port binds to loopback only (metrics.address and metrics.port in application.yml), apart from the game socket (8090). The zone runs no Spring web server, so this is the Prometheus client’s own exporter (PrometheusEndpoint), a small JDK HTTP server over the meter registry. Besides /metrics it answers /-/healthy for a liveness probe, and nothing else.

What is measured

MeterTypeTagsWhat it says
zone_tick_duration_secondstimerpart = total, systems, syncTime per tick; buckets at half, one and two tick budgets
zone_tick_overruns_totalcounterTicks longer than their budget (50 ms at 20 Hz)
zone_tick_dropped_steps_totalcounterTicks skipped because the loop fell too far behind
zone_system_duration_secondsfunctionsystem, phaseTime each system spent in its updates, and how often it ran
zone_system_failures_totalcountersystemUpdates of a system that threw
zone_systems_disabledgaugeSystems switched off after five failures in a row
zone_tick_posted_pendinggaugeTick-lane messages and leases waiting for the tick thread
zone_entitiesgaugeEntities in the world
zone_inbox_pendinggaugeClient messages that arrived and have not started
zone_inbox_io_queuedgaugeIO-lane messages waiting for a free IO thread
zone_inbox_overflows_totalcounterConnections closed because their inbox was full
zone_db_jobs_pendinggaugeworkerDB jobs queued on one worker
zone_db_jobs_rejected_totalcounterDB jobs dropped because their queue was full
zone_db_jobs_failed_totalcounterDB jobs that threw
zone_db_job_duration_secondsfunctionstage = wait, runHow long DB jobs waited in their queue, and how long they ran
zone_players_connectedgaugeAccounts with a logged-in connection
zone_players_sessionsgaugeAccounts holding a session
zone_socket_channelsgaugeOpen game connections, logged in or not
zone_socket_written_bytes_totalcounterBytes flushed to clients
zone_socket_read_bytes_totalcounterBytes received from clients
zone_socket_flushes_totalcounterFlushes to clients; each is at least one write call
zone_socket_flush_size_bytessummaryBytes per flush, with buckets from 1 KiB to 256 KiB
zone_socket_dropped_totalcounterreason = backlog, unwritableClients closed because they stopped reading
zone_chunk_inbox_pendinggaugekind = requests, carves, teleportsChunk work waiting for the tick
zone_map_renders_queuedgaugeMap tiles waiting for a render thread

Spring Boot adds its own meters on top: the JVM (jvm_gc_pause_seconds lines up with slow ticks), the database pool (hikaricp_connections_pending, hikaricp_connections_acquire_seconds), repository calls (spring_data_repository_invocations_seconds) and the tile requests (http_server_requests_seconds).

Every series also carries application="zone-server" and shard, the zone’s shard id.

No meter is tagged with an account or an entity. One series per player would grow without end. To compare clients, divide: bytes written by connected players gives bytes per player, flushes by players gives flushes per player.

The slow-tick line

Prometheus shows how ticks are spread over time, not what one late tick was made of. For that the zone still logs one warning when a tick overruns, at most once a second:

  Zone tick took 87 ms against a 50 ms budget (systems 71 ms, component sync 16 ms): ChunkStreamSystem 40.1ms, ...
  

It names the five slowest systems of that tick. The line and zone_tick_duration_seconds come from the same measurement.

The account in a log line

While a client message runs, its account id is in the logging context. A log line written during that time shows account=<id> right after the level:

  2026-10-06T14:02:11.512+02:00  WARN account=42 81234 --- [zone-io-lane-1] n.b.z.item.UseItemHandler : ...
  

Lines that run for no account (the tick’s systems, boot, DB jobs) have no such field.

Adding a meter

  1. Keep a plain counter where the thing happens: an AtomicLong, a LongAdder, or a size you can read. Code under ecs/core must not import Micrometer.
  2. Bind it in zone/metrics/ (ZoneMeters for a gauge or a count, TickMetrics for something the engine records per tick). Gauges and function meters are read when the metrics are scraped, so nothing on the tick pays for them.
  3. Name it zone.<area>.<what>; Prometheus turns dots into underscores and adds the unit.
  4. Use tags only with a small, fixed set of values.
  5. Add it to the table above.

Last updated 09 Nov 2026, 23:57 +0100 . history