Update For Docker Socket Support
CI / test (push) Successful in 1m1s
Release rolling / release (push) Successful in 7m11s

This commit is contained in:
Raven Scott
2026-07-19 13:26:52 -04:00
parent 1458915d78
commit 584e1c9c42
9 changed files with 152 additions and 15 deletions
+2
View File
@@ -34,7 +34,9 @@ PEARDATA_DEFAULT_ROLE=viewer
# PEARDATA_HYPERDB=1
# PEARDATA_SWARM=0
# PEARDATA_DOCKER=0
# Installer sets PEARDATA_DOCKER=1 when Docker is detected (names via socket).
# PEARDATA_DOCKER_SOCKET=/var/run/docker.sock
# PEARDATA_CGROUPS=1
# PEARDATA_CGROUPS_MAX=32
# PEARDATA_SENSORS=1
+6 -1
View File
@@ -3,6 +3,9 @@ Description=PearData PearMonitor agent (HyperDHT + REST metrics)
Documentation=https://git.ssh.surf/snxraven/peardata
After=network-online.target
Wants=network-online.target
# Installer adds these when Docker is detected:
# After=docker.service
# Wants=docker.service
[Service]
Type=simple
@@ -14,13 +17,15 @@ TimeoutStartSec=30
TimeoutStopSec=30
User=peardata
Group=peardata
# Host journal for Logs tab (journalctl). Installer also adds peardata to this group.
# Host journal for Logs tab. Installer also adds docker when Docker is present:
# SupplementaryGroups=systemd-journal docker
SupplementaryGroups=systemd-journal
NoNewPrivileges=true
PrivateTmp=true
Environment=NODE_ENV=production
# Journal on by default; override in .env with PEARDATA_JOURNAL=0 to disable
Environment=PEARDATA_JOURNAL=1
# Docker: installer sets PEARDATA_DOCKER=1 + PEARDATA_DOCKER_SOCKET in .env when detected
EnvironmentFile=-/opt/peardata/.env
# Identity + HyperDB / metric buffers
ReadWritePaths=/opt/peardata
+2 -2
View File
@@ -75,8 +75,8 @@ Live retention is edited in the desktop **Settings → Data** tab (admin) and pe
|----------|---------|-------------|
| `PEARDATA_HYPERDB` | on | `0` / `off` disables HyperDB |
| `PEARDATA_SWARM` | off | `1` enables Hyperswarm Corestore replication |
| `PEARDATA_DOCKER` | off | `1` enables Docker Engine collector (socket + per-container cpu/mem) |
| `PEARDATA_DOCKER_SOCKET` | `/var/run/docker.sock` | Unix socket for container name enrichment |
| `PEARDATA_DOCKER` | off* | `1` enables Docker Engine collector (socket + per-container cpu/mem). *Installer sets `1` when Docker is detected |
| `PEARDATA_DOCKER_SOCKET` | auto | Socket path for names + API (`/var/run/docker.sock`, `/run/docker.sock`, Podman). Installer writes the detected path |
| `PEARDATA_CGROUPS` | on (Linux) | `0` disables cgroup v2 CPU/mem/IO charts; `1` forces on |
| `PEARDATA_CGROUPS_MAX` | `32` | Max cgroup instances to chart |
| `PEARDATA_SENSORS` | on (Linux) | `0` disables hwmon/thermal charts |
+1 -1
View File
@@ -69,7 +69,7 @@ Have `pipeline.js` start each enabled collector; all emit `samples` batches into
1. Set `PEARDATA_DOCKER=1` on the agent.
2. Charts: `docker.containers`, `docker.cpu.<id>`, `docker.mem.<id>` (registered at runtime).
3. Discovery: cgroup v2 `docker-*.scope` / `libpod-*.scope`; names from `PEARDATA_DOCKER_SOCKET` when present.
3. Discovery: cgroup v2 `docker-*.scope` / `libpod-*.scope`; names from the Docker socket (`PEARDATA_DOCKER_SOCKET`). Installer adds `peardata` to the `docker` group and enables `PEARDATA_DOCKER=1` when Docker is detected.
4. Pick them in the desktop Explore chart dropdown (`docker.*`).
### Process top-N collector
+5 -1
View File
@@ -110,7 +110,7 @@ npm start
## systemd (Linux)
Prefer the [one-line installer](#one-line-installer-recommended) (`--server`). It installs the Bare binary, writes `peardata.service`, adds `peardata` to **`systemd-journal`**, and enables host journal for the Logs tab (`PEARDATA_JOURNAL=1`).
Prefer the [one-line installer](#one-line-installer-recommended) (`--server`). It installs the Bare binary, writes `peardata.service`, adds `peardata` to **`systemd-journal`**, and enables host journal for the Logs tab (`PEARDATA_JOURNAL=1`). When **Docker** is detected it also adds `peardata` to the **`docker`** group, sets `SupplementaryGroups=… docker`, and enables `PEARDATA_DOCKER=1` so charts show human names (e.g. `dozzle`) instead of hashes.
Manual (matches installer layout):
@@ -118,6 +118,10 @@ Manual (matches installer layout):
# After placing peardata-server + .env under /opt/peardata
sudo useradd --system --home /opt/peardata --shell /usr/sbin/nologin peardata # if needed
sudo usermod -aG systemd-journal peardata
# If Docker is installed:
sudo usermod -aG docker peardata
# In /opt/peardata/.env: PEARDATA_DOCKER=1 and PEARDATA_DOCKER_SOCKET=/var/run/docker.sock
# In the unit: SupplementaryGroups=systemd-journal docker
sudo cp deploy/peardata.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now peardata
+87 -3
View File
@@ -102,6 +102,23 @@ host_triple() {
echo "${os}-${arch}"
}
# First existing Docker Engine unix socket (empty if none).
detect_docker_socket() {
local p
for p in \
"${PEARDATA_DOCKER_SOCKET:-}" \
/var/run/docker.sock \
/run/docker.sock \
/var/run/podman/podman.sock \
/run/podman/podman.sock
do
[[ -n "$p" && -S "$p" ]] || continue
printf '%s\n' "$p"
return 0
done
return 1
}
# ─── args ───────────────────────────────────────────────────────────────────
parse_args() {
while [[ $# -gt 0 ]]; do
@@ -341,12 +358,71 @@ install_server() {
run_root sh -c "printf '\\n# Host journal for Logs tab (set to 0 to disable)\\nPEARDATA_JOURNAL=1\\n' >> '${SERVER_DIR}/.env'"
fi
# Docker socket access — container names (e.g. dozzle) + per-container metrics
local docker_detected=0
local docker_sock=""
local docker_group_name=""
docker_sock="$(detect_docker_socket || true)"
if [[ -n "$docker_sock" ]]; then
docker_detected=1
elif getent group docker >/dev/null 2>&1; then
docker_detected=1
docker_sock="/var/run/docker.sock"
elif command -v docker >/dev/null 2>&1; then
docker_detected=1
docker_sock="/var/run/docker.sock"
fi
if getent group docker >/dev/null 2>&1; then
docker_group_name="docker"
fi
if [[ "$docker_detected" -eq 1 ]]; then
ok "Docker detected${docker_sock:+ (socket ${docker_sock})}"
if [[ -n "$docker_group_name" ]]; then
log "Adding peardata to ${docker_group_name} for Docker socket / container names…"
if command -v usermod >/dev/null 2>&1; then
run_root usermod -aG "$docker_group_name" peardata || warn "usermod -aG ${docker_group_name} peardata failed"
elif command -v gpasswd >/dev/null 2>&1; then
run_root gpasswd -a peardata "$docker_group_name" || warn "gpasswd -a peardata ${docker_group_name} failed"
else
warn "Could not add peardata to ${docker_group_name} (no usermod/gpasswd)"
fi
else
warn "Docker socket present but no 'docker' group — grant peardata read access to ${docker_sock} manually"
fi
if ! run_root grep -qE '^PEARDATA_DOCKER=' "${SERVER_DIR}/.env" 2>/dev/null; then
log "Enabling PEARDATA_DOCKER=1 in ${SERVER_DIR}/.env (container metrics + names)"
run_root sh -c "printf '\\n# Docker collector (names via socket; set to 0 to disable)\\nPEARDATA_DOCKER=1\\nPEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'"
else
# Keep collector flag; refresh socket path if unset
if ! run_root grep -qE '^PEARDATA_DOCKER_SOCKET=' "${SERVER_DIR}/.env" 2>/dev/null; then
run_root sh -c "printf 'PEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'"
fi
fi
else
log "Docker not detected — skipping socket access (install Docker later, then re-run installer or: usermod -aG docker peardata && PEARDATA_DOCKER=1)"
fi
run_root chown -R peardata:peardata "$SERVER_DIR" 2>/dev/null || run_root chown -R peardata "$SERVER_DIR"
unit_path="/etc/systemd/system/peardata.service"
local journal_group_line=""
# systemd SupplementaryGroups replaces the user's supplementary set — list every group we need
local supp_groups=()
if getent group systemd-journal >/dev/null 2>&1; then
journal_group_line="SupplementaryGroups=systemd-journal"
supp_groups+=("systemd-journal")
fi
if [[ "$docker_detected" -eq 1 && -n "$docker_group_name" ]]; then
supp_groups+=("$docker_group_name")
fi
local supp_groups_line=""
if [[ ${#supp_groups[@]} -gt 0 ]]; then
supp_groups_line="SupplementaryGroups=${supp_groups[*]}"
fi
local docker_after_line=""
local docker_wants_line=""
if [[ "$docker_detected" -eq 1 ]]; then
docker_after_line="After=docker.service"
docker_wants_line="Wants=docker.service"
fi
log "Writing ${unit_path}"
run_root tee "$unit_path" >/dev/null <<EOF
@@ -355,6 +431,8 @@ Description=PearData PearMonitor agent (HyperDHT + REST metrics)
Documentation=${GITEA_URL}/${OWNER}/${REPO}
After=network-online.target
Wants=network-online.target
${docker_after_line}
${docker_wants_line}
[Service]
Type=simple
@@ -366,7 +444,7 @@ TimeoutStartSec=30
TimeoutStopSec=30
User=peardata
Group=peardata
${journal_group_line}
${supp_groups_line}
NoNewPrivileges=true
PrivateTmp=true
Environment=NODE_ENV=production
@@ -399,6 +477,11 @@ EOF
env_pk="$(run_root grep -E '^SERVER_PUBLIC_KEY=' "${SERVER_DIR}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]' || true)"
fi
local docker_summary="not detected (optional)"
if [[ "$docker_detected" -eq 1 ]]; then
docker_summary="enabled · peardata ∈ ${docker_group_name:-docker} · ${docker_sock} (human container names)"
fi
cat <<EOF
${C_BOLD}Server installed${C_RESET}
@@ -407,6 +490,7 @@ ${C_BOLD}Server installed${C_RESET}
Data: ${SERVER_DIR}/data
Service: peardata.service
Journal: peardata ∈ systemd-journal · PEARDATA_JOURNAL=1 (Logs tab)
Docker: ${docker_summary}
REST: http://127.0.0.1:18888/api/v3/info (localhost by default)
${C_BOLD}Keys (written on first successful start)${C_RESET}
+5 -3
View File
@@ -19,7 +19,7 @@ import {
makeCgroupThrottleChart,
} from '../../../shared/metrics.js'
import { resolveContainerLabel } from '../../../shared/container-names.js'
import { fetchDockerNames } from './docker.js'
import { fetchDockerNames, resolveDockerSocket } from './docker.js'
import logger from '../../utils/logger.js'
const log = logger.child('cgroups')
@@ -185,7 +185,7 @@ export class CgroupsCollector extends EventEmitter {
constructor(opts = {}) {
super()
this.intervalMs = opts.intervalMs ?? (Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS)
this.socketPath = opts.socketPath || process.env.PEARDATA_DOCKER_SOCKET || '/var/run/docker.sock'
this.socketPath = resolveDockerSocket(opts.socketPath)
this.timer = null
this.running = false
/** @type {Map<string, { usage: number, user: number, system: number, rbytes: number, wbytes: number, throttledUsec: number }>} */
@@ -220,8 +220,10 @@ export class CgroupsCollector extends EventEmitter {
if (now - this._nameRefreshAt < 30_000) return
this._nameRefreshAt = now
try {
this.socketPath = resolveDockerSocket(this.socketPath)
if (fs.existsSync(this.socketPath)) {
this._names = await fetchDockerNames(this.socketPath)
const map = await fetchDockerNames(this.socketPath)
if (map.size) this._names = map
}
} catch {
// ignore — titles fall back to humanized hashes
+37 -4
View File
@@ -32,6 +32,26 @@ export function isDockerCollectorEnabled() {
return v === '1' || v === 'on' || v === 'true'
}
/** Candidate Docker / Podman engine sockets (first existing wins). */
export function resolveDockerSocket(preferred) {
const candidates = [
preferred,
process.env.PEARDATA_DOCKER_SOCKET,
'/var/run/docker.sock',
'/run/docker.sock',
'/var/run/podman/podman.sock',
'/run/podman/podman.sock',
].filter(Boolean)
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p
} catch {
// ignore
}
}
return preferred || process.env.PEARDATA_DOCKER_SOCKET || '/var/run/docker.sock'
}
function readFile(p) {
try {
return fs.readFileSync(p, 'utf8')
@@ -222,7 +242,7 @@ export class DockerCollector extends EventEmitter {
constructor(opts = {}) {
super()
this.intervalMs = opts.intervalMs || Number(process.env.PEARDATA_SAMPLE_MS) || SAMPLE_INTERVAL_MS
this.socketPath = opts.socketPath || process.env.PEARDATA_DOCKER_SOCKET || '/var/run/docker.sock'
this.socketPath = resolveDockerSocket(opts.socketPath)
this._timer = null
/** @type {Map<string, { usageNs: number, wallMs: number }>} */
this._prevCpu = new Map()
@@ -233,6 +253,7 @@ export class DockerCollector extends EventEmitter {
start() {
if (this._timer) return
this.socketPath = resolveDockerSocket(this.socketPath)
registerChart(DOCKER_CONTAINERS_CHART)
log.info('Docker collector started', { socket: this.socketPath })
this._tick()
@@ -252,11 +273,23 @@ export class DockerCollector extends EventEmitter {
if (now - this._nameRefreshAt < 30_000) return
this._nameRefreshAt = now
try {
this.socketPath = resolveDockerSocket(this.socketPath)
if (fs.existsSync(this.socketPath)) {
this._names = await fetchDockerNames(this.socketPath)
const map = await fetchDockerNames(this.socketPath)
if (map.size) {
const prev = this._names.size
this._names = map
if (prev !== map.size) {
log.info('Docker container names loaded', { count: map.size, socket: this.socketPath })
}
} else if (!this._names.size) {
log.warn('Docker socket reachable but no container names (check peardata ∈ docker group)', {
socket: this.socketPath,
})
}
}
} catch {
// ignore
} catch (err) {
log.warn('Docker name refresh failed', { error: err.message, socket: this.socketPath })
}
}
+7
View File
@@ -7,6 +7,7 @@ import {
readCgroupCpu,
readCgroupMemory,
isDockerCollectorEnabled,
resolveDockerSocket,
} from '../server/services/collectors/docker.js'
import {
makeDockerCpuChart,
@@ -49,6 +50,12 @@ test('isDockerCollectorEnabled', (t) => {
else process.env.PEARDATA_DOCKER = prev
})
test('resolveDockerSocket prefers existing preferred path', (t) => {
const fallback = resolveDockerSocket('/nonexistent-docker-sock-peardata-test')
t.ok(typeof fallback === 'string')
t.ok(fallback.includes('docker.sock') || fallback.includes('podman.sock'))
})
test('readCgroupCpu / memory from fixture', (t) => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'peardata-cgroup-'))
const slice = path.join(tmp, 'system.slice')