Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WebSocket API Integration

Integrate with the socktop agent’s WebSocket API to build custom monitoring tools. If you’re writing Rust, prefer the socktop_connector library, which wraps all of this.

WebSocket Endpoint

ws://HOST:PORT/ws         # Without TLS
wss://HOST:PORT/ws        # With TLS

With authentication token (if configured):

ws://HOST:PORT/ws?token=YOUR_TOKEN
wss://HOST:PORT/ws?token=YOUR_TOKEN

The agent also serves GET /healthz over plain HTTP, returning 200 OK — useful for liveness probes.

Request Types

Requests are plain text WebSocket messages (not JSON). The agent replies with one message per request:

RequestResponse
get_metricsJSON — fast-changing metrics (CPU, memory, network, GPU)
get_disksJSON — array of disk/partition entries
get_processesBinary — protobuf process list, gzip-compressed above ~768 bytes
get_process_metrics:<PID>JSON — detailed metrics for one process
get_journal_entries:<PID>JSON — recent journal entries for one process

Unknown messages are ignored. The agent is fully request-driven: it collects nothing until you ask, and short TTL caches (metrics 250 ms, disks 1 s, processes 1.5 s) mean multiple clients share collection work.

Response Formats

get_metrics (JSON)

{
  "sampled_at_ms": 1755900000000,
  "cpu_total": 12.4,
  "cpu_per_core": [11.2, 15.7],
  "mem_total": 33554432,
  "mem_used": 18321408,
  "swap_total": 0,
  "swap_used": 0,
  "hostname": "myserver",
  "cpu_temp_c": 42.5,
  "disks": [],
  "networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
  "top_processes": [],
  "gpus": [{"name":"NVIDIA GeForce RTX 5080","utilization_gpu_pct":56,"mem_used_bytes":1073741824,"mem_total_bytes":8589934592}]
}

Notes:

  • sampled_at_ms (added in 1.60) is the epoch-milliseconds timestamp of when the snapshot was actually collected on the agent. Because responses can be served from the TTL cache, compute rates (e.g. network KB/s) from deltas of sampled_at_ms, not from your own receive times.
  • disks and top_processes are always empty here — request them separately with get_disks / get_processes.
  • cpu_temp_c is null when no sensor is available; gpus is null when there is no GPU (or GPU collection is disabled).
  • received/transmitted are cumulative byte counters since agent start.

get_disks (JSON)

[
  {"name":"nvme0n1","total":512000000000,"available":320000000000,"temperature":38.5,"is_partition":false},
  {"name":"nvme0n1p2","total":511000000000,"available":320000000000,"temperature":null,"is_partition":true}
]

is_partition distinguishes partitions from whole disks (exact on Linux via /sys/block).

get_processes (Protocol Buffers)

Returned as a binary WebSocket message. If the encoded payload exceeds ~768 bytes (nearly always), it is gzip-compressed. Schema:

syntax = "proto3";
package socktop;

message Processes {
  uint64 process_count = 1;   // total processes in the system
  repeated Process rows = 2;  // all processes (sorting is client-side)
}

message Process {
  uint32 pid = 1;
  string name = 2;
  float cpu_usage = 3;        // 0..100
  uint64 mem_bytes = 4;       // RSS bytes
}

To decode: check for the gzip magic bytes (0x1f 0x8b), decompress if present, then parse with any protobuf library.

get_process_metrics:<PID> and get_journal_entries:<PID> (JSON)

Added for the process-details view: per-process detail (command line, executable, working directory, per-thread CPU times in microseconds, and more) and recent journal entries. Journal entries carry both a display timestamp (RFC 3339 UTC) and a numeric timestamp_us (epoch microseconds, added in 1.60); the response’s notice field, when present, explains empty results caused by journal access restrictions rather than absence of logs. These responses are cached per PID for 250 ms / 1 s respectively.

Example: JavaScript/Node.js

const WebSocket = require('ws');

const ws = new WebSocket('ws://localhost:3000/ws');

ws.on('open', () => {
  console.log('Connected to socktop_agent');

  // Requests are plain text messages
  setInterval(() => ws.send('get_metrics'), 1000);
  setInterval(() => ws.send('get_processes'), 3000);
});

ws.on('message', (data, isBinary) => {
  if (isBinary) {
    // get_processes reply: gzip'd protobuf (see schema above)
    console.log('Binary process list, length:', data.length);
  } else {
    const metrics = JSON.parse(data.toString());
    console.log(`CPU: ${metrics.cpu_total}%`);
  }
});

Example: Python

import json
import asyncio
import websockets

async def monitor_system():
    uri = "ws://localhost:3000/ws"
    async with websockets.connect(uri) as websocket:
        print("Connected to socktop_agent")

        while True:
            await websocket.send("get_metrics")   # plain text request
            response = await websocket.recv()

            if isinstance(response, str):
                data = json.loads(response)
                print(f"CPU: {data['cpu_total']}%, "
                      f"Memory: {data['mem_used']/data['mem_total']*100:.1f}%")
            else:
                print(f"Binary response, length: {len(response)}")

            await asyncio.sleep(1)

asyncio.run(monitor_system())
  • Metrics: ≥ 500 ms
  • Processes: ≥ 2000 ms
  • Disks: ≥ 5000 ms

Polling faster than the agent’s TTL caches (250 ms / 1.5 s / 1 s) just returns cached snapshots.

Error Handling

Send each request and await its reply before sending the next of the same kind — replies carry no request ID and are matched by order. Wrap requests in a timeout and treat a timeout as a dead connection: reconnect rather than continuing on a stream that may now be misaligned.

function connect() {
  const ws = new WebSocket('ws://localhost:3000/ws');

  ws.on('open', () => {
    // Start polling
  });

  ws.on('close', () => {
    console.log('Connection lost, reconnecting...');
    setTimeout(connect, 1000);
  });
}

connect();

Compatibility

Wire changes are additive: new fields (like sampled_at_ms and timestamp_us) appear alongside old ones, so integrations built against older agents keep working against newer ones and vice versa.