Update For Docker Socket Support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} else if (!this._names.size) {
|
||||
log.warn('Docker socket reachable but no container names (check peardata ∈ docker group)', {
|
||||
socket: this.socketPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn('Docker name refresh failed', { error: err.message, socket: this.socketPath })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user