docs: 1.60 update — fix fabricated content, add kill/platform/known-issues docs
- 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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user