Monitoring

This page covers what to watch on a running server: the numbers the System page shows and what each of them actually means, the HTTP endpoints an external monitoring system should poll, and a table of what is worth waking someone for.

ETTIX measures the machine itself by reading /proc, /sys and the filesystems directly. There is no Prometheus exporter, no agent to install and no third-party metrics library in the binary. Everything below is available from the administration interface and from two JSON endpoints, which is enough to drive a dashboard or an alerting system of your own.

Unavailable is never reported as zero. A container without /proc/diskstats, a kernel without an interface, a path that is not mounted: each is reported as not measurable, with the file that could not be read and the reason. Zero is a measurement. An operator who cannot tell the two apart will eventually act on a number that was never taken, so ETTIX refuses to make that confusion possible.

The System page

Open the administration interface and choose System in the navigation. On a default installation that is http://127.0.0.1:8081/#/system. The page is built from two endpoints, GET /api/v1/system for the machine and this process, and GET /api/v1/disks for the volumes ETTIX writes to.

The collector samples every two seconds. Percentages and per-second figures are the difference between two samples, so for the first interval after start-up there is nothing to divide and the page says so rather than drawing a row of zeroes. In the JSON that state is rates_available: false, and interval_seconds is the period the rates were averaged over.

CPU

The first meter is total CPU across all cores over the last interval, and underneath it is the split that explains where the time went.

FieldWhat it is
user_percentTime running application code — ETTIX and everything else on the machine. On a healthy media server this is most of the busy time.
system_percentTime in the kernel on this process's behalf, including hardware and software interrupt handling. Network-heavy servers spend real time here; it is not waste.
iowait_percentTime the cores were idle with at least one disk request outstanding — the CPU had nothing to do because storage had not answered yet.
steal_percentTime the hypervisor gave to another guest on the same physical host: this virtual machine was ready to run and was not allowed to.

Both of the last two are routinely misread, and in opposite directions. High iowait is not CPU load — the processor is idle. It is a storage symptom, and on a recording server it usually means the DVR volume, not the encoder. Steal is not your fault and you cannot tune it away — the machine is slow because a neighbour is busy or the instance is oversubscribed, and the fix is a different instance or a different host, not a configuration change here. Sustained steal above a few per cent on a VM is the first thing to look at when the server feels slow while every other number says idle.

The per-core panel below the CPU meter shows each logical CPU separately. It is worth a glance, because a single thread pinned at 100 % on one core of sixteen is a 6 % average — a bottleneck that is invisible in the total.

Load average is the 1, 5 and 15 minute figures from the kernel, and the bar beside them is the 1-minute load as a percentage of the core count. That comparison is the whole point: a load average of 8 is comfortable on a 16-core machine and severe on a 4-core one, so the number is meaningless until it is divided by cores. One unit of load per core means the machine is fully committed with nothing queued; above that, work is waiting.

Inside a container with a cgroup v2 CPU quota, the page adds a line naming the limit in cores, the raw cpu.max quota and period, and how much of the quota is being used. A process approaching 100 % of its quota is being throttled even though the host's CPU meter looks calm.

Memory

The Used meter is deliberately not "total minus free". It is MemTotal minus MemAvailable, which means reclaimable page cache is excluded from used. Linux fills otherwise idle memory with cached file contents; on a server writing DVR segments that cache can be most of the machine's RAM. Counting it as used would show 95 % memory pressure on a machine under no pressure at all, and would train you to ignore the number that matters. The cached_bytes figure is shown separately, as information rather than as consumption.

Swap is shown only when the machine has any. On a live media server, swap in use is a warning rather than a statistic: a page fault that has to reach the disk in the middle of a segment write is a delay measured in milliseconds, which viewers see. The interface starts colouring swap at 10 % used for that reason, far earlier than it colours anything else.

In a container, the totals are the cgroup's, not the host's, and source reads cgroup rather than meminfo. Reporting the host's 256 GB inside a 2 GB container would look healthy right up to the moment the kernel kills the process, so the limit wins and the host's figures are shown beside it as a separate line. Used inside a cgroup is memory.current minus the cgroup's own file cache, for the same reclaimability reason.

Storage and disk I/O

This panel has one row per volume ETTIX writes to — every configured DVR destination path, merged so that two streams recording to the same path are one volume with two destinations rather than two volumes each claiming the whole disk. Each row shows free space against capacity, a fill bar, the block device behind the path, what that device is reading and writing per second, its I/O busy percentage, and the rate this server's own recorders are writing there.

Two details are worth knowing:

For an extra path that is not a DVR destination, free space is the unprivileged figure — the space an ordinary process can actually use. The blocks a filesystem holds back as a root-only reserve are excluded, because a recording cannot be written into them.

Block devices

The Block devices table is the raw per-device view from /proc/diskstats: bytes read and written per second, completed reads and writes per second, requests currently in flight, and I/O busy. Partitions are listed and marked as such; the totals count whole disks only, since adding a partition to the disk that contains it would count every byte twice. RAM disks and loop devices are excluded because they are noise on every dashboard.

busy_percent is the fraction of wall-clock time the device had at least one request in flight. It is exactly the figure iostat prints as %util, and it is the number that tells you a DVR disk is saturated before writes start failing. A single spinning disk sitting near 100 % is doing all it can, and any further recording load will queue.

On a device that serves several requests at once — an NVMe drive, a RAID volume, a SAN LUN — 100 % busy means "never idle", not "at capacity". Read it alongside the throughput columns and the DVR destinations' max_write_latency_ms rather than on its own.

Network

One row per interface, in and out, in bits per second rather than bytes, because that is the unit a link is sold in and the unit an encoder is configured in. Errors and dropped counters are the interface's own lifetime totals; what matters is whether they move, not what they are. The totals at the top exclude loopback: traffic from this process to itself would otherwise be counted twice and would make a quiet server look busy.

The ETTIX process

The last panel is this process rather than the machine.

RowWhat it tells you
Resident memoryrss_bytes: the memory the kernel's OOM killer and any cgroup limit actually count. This is the figure to compare against a container limit, not heap.
CPUcpu_percent, where 100 means one core fully used — the convention top uses. On an 8-core machine, 400 is half the machine.
GoroutinesConcurrent tasks inside the server. It rises and falls with viewers and sources. A count that climbs steadily and never comes back down after streams end is a leak worth reporting.
OS threads, Open filesThread count and open descriptors. Descriptors climbing towards the process limit will eventually refuse connections.
Heap in useLive Go heap. Always smaller than resident memory; a large gap between the two is normal.
GC cyclesLifetime garbage collections. Only the rate of change is interesting.
GC pause p50 / p99 / maxStop-the-world pauses during the last sampling interval only, not since start-up. A percentile computed over the whole life of the process would be an hour old and would say nothing about a glitch a viewer saw a moment ago. These are normally well under a millisecond.
Scheduler latency p99How long a task waited for a turn on the CPU. This rises before anything else does when the machine is oversubscribed, and it is the most useful early warning on the page.

Below these, when anything could not be measured, the page lists the source, the file and the reason. The same condition is written to the log once, as SYSSTAT_SOURCE_UNAVAILABLE, when it first appears and once again when it clears — not every two seconds for the life of the process.

Endpoints for external monitoring

/health and /ready, on the playback listener

These two live on the playback listener — port 8080 by default — and neither needs a credential, so a load balancer can reach them without being given one. The difference between them is the part that matters.

EndpointAnswersMeaning
GET /healthalways 200 while the process is runningThe process is alive. It stays 200 even when the licence is restricted, because an operator has to be able to reach the administration interface of a server that has stopped serving. A server that failed its own liveness check when its licence lapsed would be restarted or destroyed by the platform, and the person trying to fix the licence would be chasing a moving target.
GET /ready200 when ready, 503 with a JSON error when notThis server should receive viewers. It goes not-ready when the licence is restricted, so a pool drains this node instead of sending viewers to a server that will refuse every playlist.

Health-check on /health; route on /ready. Using /health as the load balancer's pool check is the common mistake, and it sends viewers to a server that answers 503 to every playlist request. Using /ready as the liveness or restart check is the opposite mistake: it will restart a perfectly healthy process whose licence needs attention.

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health
curl -s http://127.0.0.1:8080/ready

Both endpoints are also present on the administration listener, which is convenient for a local check but is not where a load balancer should look.

The administration API

Everything else is on the administration listener, which defaults to 127.0.0.1:8081 and is loopback-only unless http.admin_allow_remote is set. All of these require a credential from http.admin_auth once one is configured — on the loopback default, where a fresh installation has none, they are open — presented either as an API key in an Authorization header or as a browser session cookie. All are read-only and all answer GET.

RouteCarries
/api/v1/statusVersion, uptime, hostname, stream counts by state, total viewers, HLS cache statistics, per-listener HTTP counters (requests, bytes out, 4xx, 5xx, open and rejected connections) and a short Go runtime summary.
/api/v1/streamsPer stream: state, bitrate, video and audio codecs, HLS output counters, and each configured source with its SRT statistics and MPEG-TS demultiplexer counters.
/api/v1/systemThe whole System page as JSON: CPU, memory, network, block devices, this process, and the list of anything not measurable.
/api/v1/disksPer volume: capacity, free space, used percentage, backing device and its throughput, and every DVR destination writing there with its queue depth, segments written and dropped, write errors and write latency.
/api/v1/licensestate, restricted, offline_seconds_remaining, lease_expires, failures, clock_rollback and the installation id.
/api/v1/certificatesIssuer, subject, hostnames, not_after, days_remaining, expiring_soon, last_error, and the ACME renewal state when ACME is in use.
/api/v1/eventsThe operational event ring, filterable by level, stream, code, component and since, with limit up to 1000 (200 by default).
/api/v1/events/streamThe same events as they happen, as Server-Sent Events. A new subscriber receives a snapshot first, then deltas; a client that falls too far behind is disconnected and told to resubscribe.
curl -s -H "Authorization: Bearer <id>.<secret>" \
  http://127.0.0.1:8081/api/v1/system

curl -s -H "Authorization: Bearer <id>.<secret>" \
  "http://127.0.0.1:8081/api/v1/events?level=warn&since=-15m&limit=100"

/api/v1/license and the routes beside it are never refused because of the licence they report on. An installation whose lease has lapsed has to stay diagnosable remotely, or fixing it would require someone standing next to the machine.

Wiring up an external check

Point the load balancer at /ready

Pool membership follows /ready on the playback listener. Every non-200 answer should drain the node, not restart it.

Point the platform's liveness check at /health

Under systemd, Kubernetes or a process supervisor, restart decisions belong to /health alone.

Give the monitoring system its own credential

Create a separate administration API key for polling rather than sharing an operator's. It can then be revoked without locking anyone out. If the monitoring system is not on this machine, the administration listener has to be reachable from it, which means binding it off loopback and accepting the exposure with http.admin_allow_remote — configure http.admin_auth first, and see the Security page before you do.

Poll, and compare against the previous poll

Most of the interesting values are counters, not gauges. Alert on their rate of change rather than their absolute value: connect_failures at 40 means nothing, connect_failures rising by 40 in five minutes means a source is flapping.

What to alert on

The thresholds below are the ones the administration interface itself colours at, which makes an alert and the screen an operator opens next agree with each other. Where a sensible universal threshold does not exist, the column says what to look for instead.

The machine

SignalWhereAlert when
cpu.percent/api/v1/systemSustained above 85 % (the interface warns at 70 %). Check the per-core figures before acting: one saturated core is a different problem from sixteen busy ones.
cpu.iowait_percentsameSustained above roughly 10 % on a recording server. Treat it as a storage alert and go straight to the block devices.
cpu.steal_percentsameSustained above 5 %. The remedy is a different host or instance size, not a change here.
cpu.load_average[0] ÷ cpu.coressameAbove 1.0 sustained. Below 0.7 is comfortable.
memory.used_percentsameAbove 92 % (warn at 80 %). Remember that cache is already excluded, so this figure is real pressure.
memory.swap_used_bytessameAbove 10 % of swap total. On a live server any sustained swapping deserves attention.
disk.devices[].busy_percentsameAbove 90 % sustained on a device carrying DVR writes (warn at 70 %).
interface errors and droppedsameAny increase. On a clean link these do not move at all.
unavailable is non-emptysameAnything appears that was measurable before — a metric source has gone away and the dashboard is now blind to it.
process.goroutinessameRising monotonically over hours while viewer and stream counts are flat.
process.sched_latency_p99_mssameRising while CPU is not — the machine is oversubscribed. No fixed threshold; alert on the trend against its own baseline.
process.gc_pause_p99_mssameAbove a few milliseconds sustained. These are last-interval figures, so a spike is current, not historical.

Streams and storage

SignalWhereAlert when
state/api/v1/streamsNot live on a stream that should be.
bitrate_bpssameFar below what the encoder is configured to send. That is usually loss on the path, not ETTIX.
srt.packets_droppedsameRising at all. Retransmissions are arriving too late for the configured latency budget; raise latency on the source.
srt.loss_rate_percentsameSustained above a few per cent.
srt.packets_undecryptedsameAny non-zero value: the passphrase matches on one side only.
demux.cc_errors, demux.discontinuitiessameAnything but zero on a healthy link. The source is producing a broken transport stream, and nothing on this server will repair it.
connect_failures, activationssame, per sourceClimbing: a source is flapping between connected and disconnected.
used_percent/api/v1/disksAbove 92 % (warn at 80 %). Retention deletes old recordings to stay under its own free-space rule, so a volume that keeps climbing means retention cannot keep up with what is being written.
destination statesameAnything other than active or idle. degraded, full and failed each mean recording is impaired; the live stream and other destinations are unaffected.
segments_dropped, write_errorssameAny increase. A dropped segment is a hole in the recording that will not be filled in later.
max_write_latency_mssameGrowing over time on a destination that used to be fast — the disk is degrading before it fails.
hls_cache.over_budget/api/v1/statusTrue: live playlist windows alone exceed the configured cache budget.
http.playback.errors_5xxsameRising. Compare against requests to get a rate rather than a count.

Licence and certificates

SignalWhereAlert when
restricted/api/v1/licenseTrue. Ingest and playback are being refused. Recorded media is untouched.
offline_seconds_remainingsameBelow 86400 (one day). At zero the server stops serving.
failuressameClimbing: the licence authority cannot be reached. This is a network alert long before it is a licensing one.
clock_rollbacksameTrue: the system clock moved backwards and licence timing can no longer be trusted. Fix NTP. Nothing is deleted and nothing stops because of it.
days_remaining/api/v1/certificatesBelow 14. expiring_soon turns true earlier — as soon as the certificate enters its renewal window, 30 days before expiry by default — which means renewal should be under way, not that anything has failed yet.
last_errorsameNon-empty, at either the top level or inside acme.

Events worth paging on

The event ring carries a code on the entries that matter, which makes /api/v1/events?code=… a precise alert source. These mean a customer is already affected or is about to be:

CodeMeaning
LICENSE_EXPIRED, LICENSE_RESTRICTED, LICENSE_REVOKEDIngest and playback are refused. Recorded media is untouched.
LICENSE_OFFLINEThe authority is unreachable. Still serving, on a clock.
CLOCK_ROLLBACKThe system clock moved backwards.
HLS_ENCRYPTION_UNLICENSEDA stream configured to encrypt is being served in the clear.
PLAYBACK_UNAUTHENTICATEDPlaylists, segments, content keys and DVR are being served to anyone who can reach the listener.
INGEST_UNAUTHENTICATEDAny publisher reaching the RTMP listener is accepted.
ADMIN_API_REMOTEThe administration listener is not on loopback.
EXTERNAL_AUTH_BREAKER_OPEN, VIEWER_AUTH_UNAVAILABLE, PUBLISHER_AUTH_UNAVAILABLEAn authorization decision could not be made.
HLS_CACHE_OVER_BUDGETLive playlist windows alone exceed the cache budget.
SYSSTAT_SOURCE_UNAVAILABLEA system metric can no longer be measured. Logged once when it starts, once when it clears.
TELEMETRY_OFFLINEUsage is not reaching Ettix.com. It is buffered, not lost.

Logs

The event ring is bounded and lives in memory, so it is the right place to look during an incident and the wrong place to look for last Tuesday. For that, read the service log.

journalctl -u ettix-stream -f
journalctl -u ettix-stream -p warning --since -1h

Setting log.format to "json" produces one object per line, which is what a log shipper wants. The log formatter and the event ring share the same redaction function rather than a copy of it, so stream keys, SRT passphrases and administration passwords cannot reach one sink by escaping the other.

The configuration file is not redacted, because the server has to read those secrets in the clear. API responses and logs are safe to send to support; /etc/ettix-stream/config.json is not.

What is not here

There is no Prometheus /metrics endpoint, no OpenTelemetry exporter and no StatsD output in this build. An external monitoring system consumes the JSON endpoints above, or subscribes to /api/v1/events/stream. Historical retention is likewise outside the server: the System page shows the present, and the event ring holds a bounded recent window. If you need trends over weeks, poll /api/v1/system into your own time-series store.

When something is wrong rather than merely worth watching, continue to Troubleshooting.