Skip to content

easymonitordev/probe-node

Repository files navigation

EasyMonitor Probe Node

A lightweight Go binary that executes monitoring checks on behalf of an EasyMonitor server. Probes pull work from the server's Redis Streams, run HTTP/ICMP checks, and publish results back.

Deploy one or more probes across different regions/networks. The server applies cross-probe quorum so alerts only fire when a majority of probes agree.

License Docker pulls

Pre-built images are published for every release:

  • Docker Hub: easymonitor/probe-node:latest
  • GitHub Container Registry: ghcr.io/easymonitordev/probe-node:latest

Both are multi-arch (linux/amd64, linux/arm64) and built from the same source.


What you need

  • An EasyMonitor server already running (main repo)
  • A probe JWT token generated on that server
  • The server's Redis URL and password (from its .env)
  • A network path from this host to the server's Redis — typically via Tailscale or Cloudflare Tunnel. Never expose Redis over plaintext on the public internet.

Generate a token on the EasyMonitor server:

docker compose exec php php artisan probe:generate-token \
  --node-id=us-east-1 \
  --tags=us-east-1,production \
  --expires=365

Copy the printed token.


Quick start — Tailscale (recommended)

1. Install Tailscale

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Join the same tailnet your EasyMonitor server is on. Verify:

ping -c 2 <server-tailscale-ip>

2. Run the probe

docker run -d \
  --name easymonitor-probe \
  --restart unless-stopped \
  --network host \
  -e NODE_ID="us-east-1" \
  -e REDIS_URL="redis://<server-tailscale-ip>:6379/0" \
  -e REDIS_PASSWORD="<redis password from server .env>" \
  -e JWT_TOKEN="<probe token from step above>" \
  easymonitor/probe-node:latest

--network host is required so the container shares the host's tailscale0 interface — without it, the container's own network namespace can't reach the tailnet. Port 8080 is automatically exposed on the host, no -p needed.

3. Verify

curl -s http://localhost:8080/health
# {"status":"healthy","node_id":"us-east-1"}

On the EasyMonitor server, confirm the probe registered:

docker compose exec redis redis-cli -a "$REDIS_PASSWORD" --no-auth-warning \
  XINFO GROUPS checks

You should see a consumer group named probe-us-east-1.


Other tunnel options

Cloudflare Tunnel

  • Server side: cloudflared tunnel create + ingress config exposing tcp://localhost:6379 under a hostname
  • Probe side: cloudflared access tcp --hostname=... --url=127.0.0.1:6379 + Cloudflare service token
  • Run probe with --network host and REDIS_URL=redis://127.0.0.1:6379/0

Full instructions are in the main repo's PROBE_NODE_SETUP.md.

SSH tunnel / WireGuard / own VPN

Any private network path works. Point REDIS_URL at the appropriate private address.

ssh -L 6379:127.0.0.1:6379 -N user@server &

docker run -d \
  --name easymonitor-probe \
  --restart unless-stopped \
  --network host \
  -e NODE_ID="eu-west-1" \
  -e REDIS_URL="redis://127.0.0.1:6379/0" \
  -e REDIS_PASSWORD="..." \
  -e JWT_TOKEN="..." \
  easymonitor/probe-node:latest

Configuration reference

Variable Required Default Purpose
NODE_ID yes Unique identifier for this probe (e.g. us-east-1)
REDIS_URL yes redis://host:port/db
REDIS_PASSWORD yes (if server has one) Must match the server's REDIS_PASSWORD
JWT_TOKEN yes Probe auth token generated by the server
DEFAULT_TIMEOUT no 30s Default per-check timeout
BATCH_SIZE no 10 Max checks pulled per XREADGROUP
MAX_CONCURRENCY no 10 Concurrent in-flight checks
HEALTH_CHECK_PORT no 8080 HTTP port for /health, /ready, /version
REDIS_DB no 0 Redis database number

Health endpoints

  • GET /health — 200 if the probe is consuming, 503 otherwise
  • GET /ready — same semantics as /health
  • GET /version — build version and timestamp

Architecture

                ┌──────────────────────────┐
                │  EasyMonitor server      │
                │  (Laravel + Redis)       │
                └──────────┬───────────────┘
                           │
        private tunnel     │  (Tailscale / Cloudflare / VPN)
                           │
       ┌───────────────────┼───────────────────┐
       ▼                   ▼                   ▼
┌──────────┐         ┌──────────┐         ┌──────────┐
│ probe    │         │ probe    │         │ probe    │
│ us-east  │         │ eu-west  │         │ ap-south │
└──────────┘         └──────────┘         └──────────┘

Each probe uses a unique Redis Streams consumer group (probe-<NODE_ID>) so every check is delivered to every probe. The server groups per-probe results by a round_id and decides monitor status by majority vote.


Building from source

Requires Go 1.24+.

git clone https://github.com/easymonitordev/probe-node.git
cd probe-node
make build
./bin/probe-node

Cross-compile

make build-linux         # linux/amd64
make build-linux-arm     # linux/arm64

Run tests

make test                # -race -coverprofile=coverage.out
make test-coverage       # generate coverage.html

Build Docker image

make docker-build                 # single-arch
make docker-build-multiarch       # amd64 + arm64 via buildx

Deployment patterns

systemd unit (bare metal / VM)

Create /etc/systemd/system/easymonitor-probe.service:

[Unit]
Description=EasyMonitor Probe
After=network.target

[Service]
Type=simple
User=probe
EnvironmentFile=/etc/easymonitor/probe.env
ExecStart=/usr/local/bin/probe-node
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

/etc/easymonitor/probe.env:

NODE_ID=us-east-1
REDIS_URL=redis://100.x.y.z:6379/0
REDIS_PASSWORD=...
JWT_TOKEN=...
sudo systemctl enable --now easymonitor-probe

Kubernetes (StatefulSet)

A minimal manifest:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: easymonitor-probe
spec:
  serviceName: probe
  replicas: 1
  selector:
    matchLabels:
      app: easymonitor-probe
  template:
    metadata:
      labels:
        app: easymonitor-probe
    spec:
      containers:
        - name: probe
          image: easymonitor/probe-node:latest
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: REDIS_URL
              valueFrom:
                secretKeyRef:
                  name: easymonitor-probe
                  key: redis_url
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: easymonitor-probe
                  key: redis_password
            - name: JWT_TOKEN
              valueFrom:
                secretKeyRef:
                  name: easymonitor-probe
                  key: jwt_token
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080

Token rotation

Tokens should be rotated periodically (every 90–365 days recommended). On the server:

docker compose exec php php artisan probe:generate-token \
  --node-id=us-east-1 \
  --expires=365

Update the token on the probe and restart the container.


Troubleshooting

"AUTH failed" in logsREDIS_PASSWORD doesn't match the server. Check it's copied exactly from the server's .env.

"failed to validate token" — token expired or was generated with a different JWT_SECRET. Regenerate on the server.

Probe starts but no checks run — confirm Horizon is running on the server and that there are active monitors. Also verify the probe shows up: docker compose exec redis redis-cli -a "$REDIS_PASSWORD" XINFO GROUPS checks.

Can't reach Redis — verify the tunnel: tailscale status / systemctl status cloudflared. Test connectivity: redis-cli -h <host> -a "$REDIS_PASSWORD" ping should return PONG.


Contributing

See CONTRIBUTING.md for the development setup and PR guidelines.

License

MIT — see LICENSE.


Part of the EasyMonitor project.

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors