Operate

Monitoring

The health, stats and status endpoints, the relay's metrics, canaries, and what to alert on.

Four places say how the service is doing: the control API's endpoints, the relay's metrics port, the relay's admin port, and the logs. On the managed service you use the first only; on your own hosts you have all four.

The control API

CallKeyAnswers
GET /v1/healthnone200 {"ok":true} while the process is up. It checks nothing else: not the relay, not the gateway
GET /v1/statusnonethe fleet: overall state, regions, relays, recent incidents (Fleet). Without a fleet file: {"state":"up","regions":[],"incidents":[]}
GET /statusnonethe same as a page
GET /v1/streams/{id}/statssk_a stream's seats now
GET /v1/tenantsk_your account: limits, cap, this month's totals
GET /v1/usagesk_viewer-hours and bytes by day and stream (Usage)
GET /v1/fleetok_every node in detail, viewer_relays, the recent alerts

Stream stats

GET /v1/streams/{id}/stats is for a dashboard polling every few seconds. The player never calls it.

curl -s $API/v1/streams/$ID/stats -H "Authorization: Bearer $SK"

Illustrative example. The values below are made up to show the shape; they are not a measurement.

{ "viewers": { "seats": 1, "queued": 0, "cap": 100, "queue_max": 200, "peak": 1, "hls": 0 },
  "seats_with_tokens": 1, "turns": 0, "ingest_token_at": "2026-11-01T12:00:03Z",
  "ingest": { "live": true, "since": "2026-11-01T12:00:02Z", "width": 1920, "height": 1080,
              "fps": 60, "kbps": 5980, "reconnects": 0 } }
FieldMeans
viewers.seatsseats taken now
viewers.queuedpeople in the waiting line
viewers.cap, viewers.queue_maxthe stream's limits
viewers.peakthe most seats at once
viewers.hlsseats reading the LL-HLS fallback
seats_with_tokensseats that hold a live token
turnslive controller turns
ingest_token_atwhen the gateway last took a publisher token for this stream (UTC), or null. It stays set after the encoder stops: use ingest.live
ingest.livevideo is arriving: an encoder session is open on the gateway and a frame came in within the last 5 s. null: the API cannot tell (an SDK-only stream, or no gateway report for 15 s)
ingest.sincewhile live, when this encoder session began; otherwise when the last frame came in
ingest.width, height, fpsfrom the encoder's SPS (fps only when the encoder sends a fixed rate)
ingest.kbpswhile live, the encoder's media bitrate over the last 3 s, without container overhead
ingest.reconnectsencoder sessions after the first since the gateway started

Reading it also frees seats whose heartbeat stopped, so a stale count corrects itself.

The relay's metrics

Start the relay with --metrics-addr 127.0.0.1:9090 (the package's unit does). It then serves Prometheus text at /metrics (on any path, with no key; keep it on loopback or a private network and scrape it from there). Counters appear once they have fired.

MetricTypeMeans
moq_relay_active_connectionsgaugeclient connections now
moq_relay_active_publishersgaugepublishers now
moq_relay_active_subscriptions, moq_relay_active_tracksgaugesubscriptions and tracks being served
moq_relay_announced_namespacesgaugeregistered namespaces
moq_relay_seat_sessionsgaugelive token sessions
moq_relay_connections_total, _connections_closed_totalcounterconnections accepted, closed
moq_relay_publishers_total, _subscribers_totalcounterpublish and subscribe requests received
moq_relay_subscribe_latency_secondshistogramtime to resolve a subscription, by source
moq_relay_clock_synchronizedgauge1 when the kernel says the clock is synchronized. At 0 every token is refused
moq_relay_clock_unsynced_refusals_totalcounterrequests refused for that
moq_relay_token_issued_in_future_totalcountertokens refused because their issuer's clock is ahead of the relay's by more than the skew
moq_relay_auth_errors_total{stage}counterauthorization failures
moq_relay_seat_refused_total{reason}countersessions refused by the seat gate: scope_full (the account's session limit), token_id_in_use (a second session on one token), token_closed
moq_relay_seat_expired_totalcountersessions closed at their token's deadline
moq_relay_seat_admin_closed_totalcountersessions closed through the admin port (kicks, lost seats)
moq_relay_admin_denied_totalcounteradmin port calls without the right key
moq_relay_seat_log_errors_totalcountersession-log writes that failed: usage is being lost
moq_relay_setup_refused_total{limit}counterconnections refused before their handshake for the per-address or global rate (per_ip, global)
moq_relay_admission_busygaugebusy share (0 to 1) of the runtime serving viewers
moq_relay_admission_delay_msgaugetimer lateness, ms
moq_relay_admission_saturatedgauge1 while the relay refuses new viewers
moq_relay_admission_sessionsgaugesessions admitted under the admission rule
moq_relay_admission_admitted_total, _refused_total{why}counteradmitted, and refused (saturated, paced)

Watch moq_relay_admission_busy to see an edge approach its limit: it is the relay's own headroom, and 0.85 is where it starts to refuse.

The relay's admin port

--admin-addr 127.0.0.1:4480 --admin-key-file ... (loopback only). With the key as a bearer, GET /usage:

Illustrative example. The values below are made up to show the shape; they are not a measurement.

{ "scopes": { "/tn_...": { "sessions": 1, "publishers": 1, "egress_bps": 1702,
      "bytes_out_closed": 0, "sessions_closed": 0, "bytes_out_live": 7055 } },
  "admission": { "mode": "on", "saturated": false, "busy": 0.003, "delay_ms": 1.9,
      "sessions": 0, "budget": 50, "admitted": 0, "returned": 0, "places": 0,
      "refused_saturated": 0, "refused_paced": 0, "episodes": 0, "changed_at_s": 0,
      "enter_busy": 0.85, "leave_busy": 0.7, "retry_ms": 2000 } }

Per scope: live sessions and publishers, the send rate, and the bytes sent by closed and by live sessions since the relay started. It is the number to check usage against: bytes_out_closed on each relay should add up to the bytes the API's usage shows for the same period. Without the key the port answers 401. It also serves the node agent's heartbeat (Fleet).

The session log (--session-log) holds one JSON line per closed token session: scope, namespace, token_id, sub, role, start_ms, end_ms, bytes_out, token_exp and close (closed, expired, admin).

Logs

Services log to stdout, so systemd keeps them (journalctl -u nearglass-relay). The control API logs at info; set RUST_LOG (for example nearglass_control_api=debug) to change it. Lines worth searching for:

LineServiceMeans
session authorization failed, closing code=2 reason="unauthorized"relaya token was refused; the line does not say why (Troubleshooting)
system clock is not synchronized: token scopes refuse every token until it isrelayfix time sync
refusing a token issued in the futurerelaythe relay's clock is behind its issuer's
session log write failedrelayusage records are being lost
admin port failed; exitingrelaythe admin address is taken
alert event=...APIan alert was raised; alert delivered or alert webhook ... attempt=N follows
no relay admin port: a lost seat's open sessions run until the relay's watch grace endsAPINEARGLASS_API_RELAY_ADMIN_* is not set
control API unreachable for 120 s: every stream key droppedgatewayencoders are being refused until the API answers
shipped records=Nnearglass-meterusage reached the API

Canaries

A canary is a viewer that watches a stream all the time, so you know it works before a customer does. The repository has two, and both are our own tooling for our demo, not part of the packages:

  • tools/canary runs a headless browser against a page with the player for a window every hour, and records frames on screen, how far behind live the picture is (median and 99th percentile), reconnects and held pictures every second. It raises an alarm when a window's median is under 50 frames a second or it reconnects more than twice in 10 minutes.
  • tools/audio-canary is a native listener that subscribes to the audio track the way the player does, plays it into a null device, and reports every dropout and its cause every 10 s.

To canary your own deployment, publish a looping file to a stream of its own (nearglass publish --file ... --loop, with its own key), and watch it with a real browser on another network, on a schedule. Read the player's statechange events and stats() (Embed the player), and alert on: no live within a few seconds of load, a mode of ll-hls where you expect moq, or reconnects.

What to alert on

Alert onWhyWhere it shows
GET /v1/health not 200the API is down: nothing can be seated or tokenedan external probe, every 30 s
GET /v1/status state not up, or a relay.down eventa relay stopped answeringstatus, the operator channel
moq_relay_clock_synchronized is 0every token is refused; viewers see "Cannot play"relay metrics
moq_relay_admission_saturated is 1 for more than a few minutes, or edge.saturatedthe edge is full; new viewers go elsewhere or waitrelay metrics, operator channel
moq_relay_seat_log_errors_total risesusage and spend are undercountedrelay metrics
shipped records stops appearing in the agent's logthe same, from the other sideagent log
publisher.gone (or moq_relay_active_publishers is 0 while a stream should be live)the encoder droppedwebhook, relay metrics
viewers.queued above 0 for long, or seats near capthe cap is too low for the audience/stats
percent_of_cap at 80 or more, or spend.thresholdnew viewers will be refused at 100 %GET /v1/tenant, webhook
moq_relay_setup_refused_total risesa connection flood, or the rate limit is too low for your audience behind one addressrelay metrics
moq_relay_auth_errors_total jumpsa deploy changed a key, issuer or audience, or clocks driftedrelay metrics
the gateway logs every stream key droppedthe gateway cannot reach the APIgateway log

What was run, and what was not

Run on one host from the packages: GET /v1/health, /v1/status, /status, /v1/fleet, /v1/tenant, /v1/usage and the stream stats (including seats, a full stream and the line), the relay's /metrics with its metric names and the admin GET /usage, with and without the key, and the session log.

On this page