Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ JQ_TEMPLATE_REPO=Jebel-Quant/rhiza
# Comma-separated repo names to drop without removing them from repos.yml.
JQ_IGNORE=

# Drop private repos completely - their names, workflows, PR titles and local
# branches are all disclosure. Set this before serving the board publicly.
JQ_PUBLIC_ONLY=true
# Drop private repos completely - not just their details, their existence.
# The board binds to 127.0.0.1, so this is not needed to keep them off the
# network; it is for when you would rather the collector never fetched them.
JQ_PUBLIC_ONLY=false

# Refresh cadences, in seconds.
#
Expand Down
25 changes: 2 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,14 @@ jobs:
- name: Panel ids, link targets and grid overlaps
run: python3 scripts/check-dashboard.py

# The public copy is generated, so it must match what the generator makes
# from the current fleet.json - otherwise a dashboard edit silently leaves
# the public board stale.
- name: Public dashboard is regenerated from source
run: |
python3 scripts/make-public-dashboard.py
git diff --exit-code -- grafana/dashboards/fleet-public.json \
|| { echo "::error::fleet-public.json is stale - run scripts/make-public-dashboard.py"; exit 1; }

compose:
name: compose files parse
runs-on: ubuntu-latest
# The overlays only patch services, so they are not valid on their own -
# they are checked the way they are actually used. GITHUB_TOKEN and, for
# the server stack, JQ_REPOS are required variables; any value satisfies
# interpolation.
# they are checked the way they are actually used. GITHUB_TOKEN is a
# required variable; any value satisfies interpolation.
env:
GITHUB_TOKEN: dummy-value-for-interpolation-only
GF_ADMIN_PASSWORD: dummy-value-for-interpolation-only
JQ_REPOS: Jebel-Quant/rhiza
steps:
- uses: actions/checkout@v4

Expand All @@ -109,12 +97,3 @@ jobs:
run: docker compose -f docker-compose.yml -f docker-compose.repos.yml config --quiet
- name: Base + admin overlay
run: docker compose -f docker-compose.yml -f docker-compose.repos.yml -f docker-compose.admin.yml config --quiet
- name: Base + public overlay
run: docker compose -f docker-compose.yml -f docker-compose.repos.yml -f docker-compose.public.yml config --quiet
- name: Server stack
run: docker compose -f docker-compose.server.yml config --quiet
- name: Server + TLS overlay
env:
FLEET_DOMAIN: fleet.example.com
ACME_EMAIL: ci@example.com
run: docker compose -f docker-compose.server.yml -f docker-compose.tls.yml config --quiet
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ after the first API refresh. Both `repos.yml` and the generated
| Stop | `./scripts/down.sh` (add `--volumes` to discard the history too) |
| Edit the board | change `grafana/dashboards/fleet.json`; it reloads in 30s — [read the traps first](docs/dashboard.md#traps-worth-not-re-introducing) |
| Get notified | add a contact point under *Alerting → Contact points* — [why it is not provisioned](docs/dashboard.md#alerting) |
| Put it on the internet | [docs/serving.md](docs/serving.md) — do not skip the preflight |

| | |
|---|---|
Expand All @@ -76,7 +75,6 @@ Also published as a book: **<https://jebel-quant.github.io/monitoring/>**
| [Configuration](docs/configuration.md) | `repos.yml`, `.env`, the API budget |
| [What it watches](docs/metrics.md) | the four subjects, the metrics, and why each is shaped that way |
| [The dashboard](docs/dashboard.md) | reading it, editing it, alerting, and the query traps |
| [Serving it publicly](docs/serving.md) | public dashboards, the server stack, a VPS, TLS |
| [Day to day](docs/operations.md) | why panels say *No data*, and what the sign-in button is |

**If every panel says "No data", the machine was probably asleep.** Docker
Expand Down
22 changes: 0 additions & 22 deletions caddy/Caddyfile

This file was deleted.

16 changes: 9 additions & 7 deletions collector/jq_collector/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ class Config:

On a laptop the list is generated from ``repos.yml`` by
``scripts/gen-repos.py``, which also mounts each checkout at
``$JQ_REPO_ROOT/<owner>/<name>``; on a server it is set by hand and no
checkouts exist. Both halves read the same list, so the GitHub panels and
the working-copy panels can never disagree about who is in the fleet.
``$JQ_REPO_ROOT/<owner>/<name>``. Setting it by hand works too, and then
nothing mounts the checkouts. Both halves read the same list, so the GitHub
panels and the working-copy panels can never disagree about who is in the
fleet.
"""

repos: tuple[str, ...] = field(default_factory=lambda: _csv("JQ_REPOS"))
Expand All @@ -44,8 +45,8 @@ class Config:

# Where the checkouts are mounted (read-only) inside the container, one per
# repo at <repo_root>/<owner>/<name>. Set empty to skip local scanning
# entirely - the right setting on a server, where there are no working
# copies and the local panels do not apply.
# entirely, which is the right setting anywhere there are no working copies
# to report on - the local panels then simply have nothing to say.
repo_root: str = os.environ.get("JQ_REPO_ROOT", "/repos")

# owner/name of the repo whose releases define "up to date".
Expand Down Expand Up @@ -78,8 +79,9 @@ class Config:
include_archived: bool = os.environ.get("JQ_INCLUDE_ARCHIVED", "false").lower() == "true"

# Drop private repos entirely - not just their details, but their existence.
# For a board served world-readable, a private repo's name, its workflow
# names, its PR titles and its local branch names are all disclosure.
# The board binds to loopback, so this is not what keeps a private repo off
# the network; it is for when you would rather it were never fetched, since
# its name, workflow names, PR titles and branch names are all disclosure.
public_only: bool = os.environ.get("JQ_PUBLIC_ONLY", "false").lower() == "true"

def is_ignored(self, owner: str, name: str) -> bool:
Expand Down
9 changes: 5 additions & 4 deletions collector/jq_collector/localgit.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,9 @@ def scan(
"""
found: dict[str, LocalRepo] = {}
if not cfg.repo_root:
# Deliberate: on a server there are no working copies to report on, and
# an empty result is the honest answer rather than an error every minute.
# Deliberate: with no repo root there are no working copies to report
# on, and an empty result is the honest answer rather than an error
# every minute.
return found
if not os.path.isdir(cfg.repo_root):
log.error("repo root %s is not a directory", cfg.repo_root)
Expand All @@ -172,8 +173,8 @@ def scan(
path = os.path.join(cfg.repo_root, owner, repo_name)
# `.git` is a directory in a plain checkout and a file in a worktree.
if not os.path.exists(os.path.join(path, ".git")):
# Not mounted, or mounted somewhere else. Normal on a server, and
# normal for a repo you monitor but have not checked out.
# Not mounted, or mounted somewhere else. Normal for a repo you
# monitor but have not checked out.
log.debug("no working copy for %s at %s", key, path)
continue

Expand Down
25 changes: 0 additions & 25 deletions docker-compose.public.yml

This file was deleted.

83 changes: 0 additions & 83 deletions docker-compose.server.yml

This file was deleted.

38 changes: 0 additions & 38 deletions docker-compose.tls.yml

This file was deleted.

6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ better a refusal than a board that is quietly one repo short.
| `JQ_GITHUB_INTERVAL` | `300` | Seconds between GitHub refreshes. |
| `PROM_RETENTION` | `180d` | How much history to keep. |

On the server stack there are no checkouts and so no `repos.yml`: the fleet is
named directly in `JQ_REPOS`, a comma-separated list of `owner/name`.
`scripts/gen-repos.py` turns `repos.yml` into the `JQ_REPOS` list the
collector actually reads, so both halves see the same fleet. Setting `JQ_REPOS`
by hand works too, and skips `repos.yml` entirely — but then nothing mounts the
checkouts, so only the GitHub panels have anything to say.

## API budget

Expand Down
9 changes: 2 additions & 7 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ after the first API refresh.
Anonymous read access is on, so the board opens without signing in and
`admin` / `admin` is only for settings. Published on `0.0.0.0` the stack
would serve private repo names, PR titles and local branch names to anyone
on the network with no password. Putting it on the internet is a separate,
deliberate procedure — see [Serving it publicly](serving.md).
on the network with no password. This stack is built to run on one
machine; putting it on the internet is not a supported path.

## Where to go next

Expand All @@ -71,11 +71,6 @@ after the first API refresh.
Reading the tiles, editing the JSON, alerting, and four PromQL traps worth
not re-introducing.

- :material-web: **[Serving it publicly](serving.md)**

Public dashboards, the server stack, a VPS, TLS — and the preflight that
must pass before any of it is exposed.

- :material-calendar-check: **[Day to day](operations.md)**

Why panels say *No data*, and what the sign-in button actually is.
Expand Down
Loading
Loading