docs: 1.60 update — fix fabricated content, add kill/platform/known-issues docs
Build and Deploy to K3s / test (push) Successful in 1m55s
Build and Deploy to K3s / lint (push) Failing after 13m39s
Build and Deploy to K3s / build-and-push (push) Has been skipped
Build and Deploy to K3s / deploy (push) Failing after 14m58s

- Rewrite configuration.md from the real CLI/env surface (the page documented
  ~10 agent flags, /etc/default/socktop-agent, and env vars that don't exist)
- Fix token docs: SOCKTOP_TOKEN env + URL query param; client -t is --tls-ca,
  not a token flag
- Rewrite agent-integration.md: requests are plain text (get_metrics, ...),
  not JSON; correct proto schema, real GPU/metrics fields, 1.60 additive
  fields (sampled_at_ms, timestamp_us, journal notice)
- Fix connector.md: real ConnectorConfig builder API, error variants,
  examples list; bump to 1.60
- Document the 1.60 process kill feature (usage/general + keyboard-mouse)
- TLS: document exact-match pinning semantics, cert rotation, key perms,
  upgrade-clients-first note; genericize profile example
- Agent service: journal access setup (systemd-journal group), packaged-
  service cert path, drop fake positional-port form, fix broken fences
- New pages: Platform Notes (Windows/macOS/RISC-V/Pi kernel tip, ARMv7
  --no-default-features) and Known Issues
- Fedora build prereqs: libdrm-devel + libdrm-amdgpu (closes socktop#35)
- Freshen intro (1.60, new demo apng, correct crates.io names, GitHub
  Releases link), quick-start, upgrading (order notes, stale-binary tip),
  zellij (remove invalid pane_template example)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jasonwitty
2026-08-23 21:35:27 -07:00
parent 98f8b4a865
commit 5c03b92807
18 changed files with 454 additions and 438 deletions
+76 -76
View File
@@ -1,6 +1,6 @@
# WebSocket API Integration
Integrate with the socktop agent's WebSocket API to build custom monitoring tools.
Integrate with the socktop agent's WebSocket API to build custom monitoring tools. If you're writing Rust, prefer the [socktop_connector library](./connector.md), which wraps all of this.
## WebSocket Endpoint
@@ -15,22 +15,29 @@ 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
Send JSON messages to request specific metrics:
Requests are **plain text WebSocket messages** (not JSON). The agent replies with one message per request:
```json
{"type": "metrics"} // Fast-changing metrics (CPU, memory, network)
{"type": "disks"} // Disk information
{"type": "processes"} // Process list (returns protobuf)
```
| Request | Response |
|---|---|
| `get_metrics` | JSON — fast-changing metrics (CPU, memory, network, GPU) |
| `get_disks` | JSON — array of disk/partition entries |
| `get_processes` | Binary — 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
### Metrics (JSON)
### `get_metrics` (JSON)
```json
{
"sampled_at_ms": 1755900000000,
"cpu_total": 12.4,
"cpu_per_core": [11.2, 15.7],
"mem_total": 33554432,
@@ -39,40 +46,58 @@ Send JSON messages to request specific metrics:
"swap_used": 0,
"hostname": "myserver",
"cpu_temp_c": 42.5,
"disks": [],
"networks": [{"name":"eth0","received":12345678,"transmitted":87654321}],
"gpus": [{"name":"nvidia-0","usage":56.7,"memory_total":8589934592,"memory_used":1073741824,"temp_c":65.0}]
"top_processes": [],
"gpus": [{"name":"NVIDIA GeForce RTX 5080","utilization_gpu_pct":56,"mem_used_bytes":1073741824,"mem_total_bytes":8589934592}]
}
```
### Disks (JSON)
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)
```json
[
{"name":"nvme0n1p2","total":512000000000,"available":320000000000},
{"name":"sda1","total":1000000000000,"available":750000000000}
{"name":"nvme0n1","total":512000000000,"available":320000000000,"temperature":38.5,"is_partition":false},
{"name":"nvme0n1p2","total":511000000000,"available":320000000000,"temperature":null,"is_partition":true}
]
```
### Processes (Protocol Buffers)
`is_partition` distinguishes partitions from whole disks (exact on Linux via `/sys/block`).
Processes are returned in protobuf format, optionally gzip-compressed. Schema:
### `get_processes` (Protocol Buffers)
Returned as a binary WebSocket message. If the encoded payload exceeds ~768 bytes (nearly always), it is gzip-compressed. Schema:
```protobuf
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;
uint64 mem_bytes = 4;
}
message ProcessList {
uint32 process_count = 1;
repeated Process processes = 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
```javascript
@@ -80,36 +105,23 @@ const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', function open() {
ws.on('open', () => {
console.log('Connected to socktop_agent');
// Request metrics
ws.send(JSON.stringify({type: 'metrics'}));
// Poll every second
setInterval(() => {
ws.send(JSON.stringify({type: 'metrics'}));
}, 1000);
// Request processes every 3 seconds
setInterval(() => {
ws.send(JSON.stringify({type: 'processes'}));
}, 3000);
// Requests are plain text messages
setInterval(() => ws.send('get_metrics'), 1000);
setInterval(() => ws.send('get_processes'), 3000);
});
ws.on('message', function incoming(data) {
try {
const jsonData = JSON.parse(data);
console.log('Received JSON data:', jsonData);
} catch (e) {
console.log('Received binary data (protobuf), length:', data.length);
// Process binary protobuf data with protobufjs
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}%`);
}
});
ws.on('close', function close() {
console.log('Disconnected from socktop_agent');
});
```
## Example: Python
@@ -123,23 +135,18 @@ async def monitor_system():
uri = "ws://localhost:3000/ws"
async with websockets.connect(uri) as websocket:
print("Connected to socktop_agent")
# Request initial metrics
await websocket.send(json.dumps({"type": "metrics"}))
while True:
# Request metrics
await websocket.send(json.dumps({"type": "metrics"}))
# Receive response
await websocket.send("get_metrics") # plain text request
response = await websocket.recv()
try:
if isinstance(response, str):
data = json.loads(response)
print(f"CPU: {data['cpu_total']}%, Memory: {data['mem_used']/data['mem_total']*100:.1f}%")
except json.JSONDecodeError:
print(f"Received binary data, length: {len(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())
@@ -147,31 +154,24 @@ asyncio.run(monitor_system())
## Recommended Intervals
- Metrics: ≥ 500ms
- Processes: ≥ 2000ms
- Disks: ≥ 5000ms
- Metrics: ≥ 500 ms
- Processes: ≥ 2000 ms
- Disks: ≥ 5000 ms
## Handling Protocol Buffers
For processing binary process data:
1. Check if response starts with gzip magic bytes (`0x1f, 0x8b`)
2. Decompress if necessary
3. Parse with protobuf library using the schema above
Polling faster than the agent's TTL caches (250 ms / 1.5 s / 1 s) just returns cached snapshots.
## Error Handling
Implement reconnection logic with exponential backoff:
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.
```javascript
function connect() {
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', () => {
console.log('Connected');
// Start polling
});
ws.on('close', () => {
console.log('Connection lost, reconnecting...');
setTimeout(connect, 1000);
@@ -181,6 +181,6 @@ function connect() {
connect();
```
## More Info
## Compatibility
For detailed implementation, see the [socktop_agent README](https://github.com/jasonwitty/socktop/tree/master/socktop_agent).
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.
+32 -43
View File
@@ -17,7 +17,7 @@ Add to your `Cargo.toml`:
```toml
[dependencies]
socktop_connector = "1.50"
socktop_connector = "1.60"
tokio = { version = "1", features = ["full"] }
```
@@ -54,9 +54,9 @@ use socktop_connector::connect_to_socktop_agent_with_tls;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let connector = connect_to_socktop_agent_with_tls(
"wss://secure-host:3000/ws",
"/path/to/ca.pem",
false // Enable hostname verification
"wss://secure-host:8443/ws",
"/path/to/cert.pem",
false // verify_hostname: false = pin the certificate (default socktop behavior)
).await?;
// Use connector...
@@ -203,35 +203,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
### Custom Configuration
`ConnectorConfig` uses a builder pattern:
```rust
use socktop_connector::{ConnectorConfig, SocktopConnector};
let config = ConnectorConfig {
url: "ws://server:3000/ws".to_string(),
token: Some("secret-token".to_string()),
ca_cert_path: Some("/path/to/ca.pem".to_string()),
verify_tls: true,
};
let config = ConnectorConfig::new("wss://server:8443/ws?token=secret-token")
.with_tls_ca("/path/to/cert.pem")
.with_hostname_verification(false);
let connector = SocktopConnector::connect_with_config(config).await?;
let mut connector = SocktopConnector::new(config);
connector.connect().await?;
```
An authentication token is passed as a `token` query parameter in the URL (there is no separate token field).
### Error Handling
`ConnectorError` variants carry structured context:
```rust
use socktop_connector::{ConnectorError, Result};
use socktop_connector::{AgentRequest, ConnectorError, Result, connect_to_socktop_agent};
async fn monitor() -> Result<()> {
let mut connector = connect_to_socktop_agent("ws://server:3000/ws").await?;
match connector.request(AgentRequest::Metrics).await {
Ok(response) => {
Ok(_response) => {
// Handle response
Ok(())
}
Err(ConnectorError::ConnectionClosed) => {
Err(e @ ConnectorError::ConnectionClosed { .. }) => {
eprintln!("Connection closed, attempting reconnect...");
Err(ConnectorError::ConnectionClosed)
Err(e)
}
Err(e) => {
eprintln!("Error: {}", e);
@@ -247,7 +251,7 @@ The connector supports WebAssembly for browser usage:
```toml
[dependencies]
socktop_connector = { version = "1.50", features = ["wasm"] }
socktop_connector = { version = "1.60", default-features = false, features = ["wasm"] }
```
```rust
@@ -356,12 +360,12 @@ async fn check_alerts(mut connector: SocktopConnector) -> Result<(), Box<dyn std
Key types provided by the library:
- `Metrics` - System metrics (CPU, memory, network, GPU, etc.)
- `ProcessMetricsResponse` - Process information
- `DetailedProcessInfo` - Per-process detail (command, threads, ...)
- `DiskInfo` - Disk usage information
- `NetworkInfo` - Network interface statistics
- `GpuInfo` - GPU metrics
- `JournalEntry` - Systemd journal entries
- `AgentRequest` - Request types
- `AgentRequest` - Request types (`Metrics`, `Disks`, `Processes`, `ProcessMetrics { pid }`, `JournalEntries { pid }`)
- `AgentResponse` - Response types
See the [crate documentation](https://docs.rs/socktop_connector) for complete API reference.
@@ -386,12 +390,12 @@ Typical resource usage:
```rust
match connect_to_socktop_agent(url).await {
Err(ConnectorError::ConnectionFailed(e)) => {
eprintln!("Connection failed: {}", e);
Err(ConnectorError::ConnectionFailed { source }) => {
eprintln!("Connection failed: {}", source);
// Retry logic here
}
Err(ConnectorError::InvalidUrl) => {
eprintln!("Invalid URL format");
Err(ConnectorError::InvalidUrl { url, .. }) => {
eprintln!("Invalid URL: {}", url);
}
Err(e) => eprintln!("Other error: {}", e),
Ok(conn) => { /* Success */ }
@@ -400,25 +404,15 @@ match connect_to_socktop_agent(url).await {
### TLS Errors
```rust
// Disable TLS verification for testing (not recommended)
use socktop_connector::{ConnectorConfig, SocktopConnector};
let config = ConnectorConfig {
url: "wss://server:3000/ws".to_string(),
verify_tls: false,
..Default::default()
};
```
A `TlsError` or `CertificateError` usually means the pinned certificate doesn't match what the agent presented (or the PEM path is wrong). Re-copy `cert.pem` from the agent — see [TLS Configuration](../security/tls.md). Hostname verification is off by default (`with_hostname_verification(false)`), which pins the certificate rather than skipping checks.
## Examples Repository
More examples available in the socktop repository:
Working examples in the socktop repository:
- `examples/simple_monitor.rs` - Basic monitoring
- `examples/multi_server.rs` - Monitor multiple servers
- `examples/alert_system.rs` - Threshold-based alerts
- `examples/wasm_demo/` - Browser-based monitoring
- [`examples/wasm_example.rs`](https://github.com/jasonwitty/socktop/blob/master/examples/wasm_example.rs) - Connector usage from WASM
- [`socktop_wasm_test/`](https://github.com/jasonwitty/socktop/tree/master/socktop_wasm_test) - Browser-based test harness for the wasm feature
- [`socktop/`](https://github.com/jasonwitty/socktop/tree/master/socktop) - The TUI itself is the reference consumer of the connector
## API Reference
@@ -429,9 +423,4 @@ Full API documentation: [docs.rs/socktop_connector](https://docs.rs/socktop_conn
- [Agent Direct Integration](./agent-integration.md) - Embed agent in your app
- [General Usage](../usage/general.md) - Using the TUI client
- [Configuration](../usage/configuration.md) - Configuration options
<!-- TODO: Add more documentation -->
<!-- TODO: Add WebSocket reconnection examples -->
<!-- TODO: Add rate limiting guidance -->
<!-- TODO: Add batch request examples -->
<!-- TODO: Add Prometheus exporter example -->
+11 -28
View File
@@ -39,34 +39,17 @@ Run it:
zellij --layout socktop-layout.kdl
```
## Saved Layouts
Layouts placed in `~/.config/zellij/layouts/` can be launched by name:
```bash
cp socktop-layout.kdl ~/.config/zellij/layouts/socktop-monitoring.kdl
zellij --layout socktop-monitoring
```
The pane commands reference [connection profiles](../usage/connection-profiles.md) by name (`-P rpi-master`), so create the profiles first.
## More Info
For detailed Zellij documentation, see [Zellij](https://zellij.dev/).
Create `~/.config/zellij/layouts/socktop-monitoring.kdl`:
```kdl
layout {
pane_template name="socktop_pane" {
command "socktop"
args "-P" "{profile}"
}
pane split_direction="vertical" {
pane split_direction="horizontal" {
socktop_pane profile="production-web"
socktop_pane profile="production-db"
}
pane split_direction="horizontal" {
socktop_pane profile="staging-web"
socktop_pane profile="staging-db"
}
}
}
```
### Use the Layout
```bash
zellij --layout socktop-monitoring
```