The closing summary still named the old parser's variables, so every run wrote its row and then died on the print, and Sentry was told the night failed. It reads the shared parser's counts now. The check-ins become one at the end with the run's duration: a start and a finish seconds apart can reach Sentry in either order, and a start that lands second is never closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwFKMuQnPAEHJ5yA1q4orh
82 lines
4.4 KiB
Bash
Executable File
82 lines
4.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# Nightly rollup: one JSON line per day in /var/lib/waving-hands/rollup.jsonl:
|
|
# the day's traffic from Caddy's access log (people and bots apart, by page,
|
|
# with referrers and the campaign tags links carry), the rooms opened,
|
|
# started and finished, and the box's vitals. Counts only: no addresses are
|
|
# kept. The pulse reads it for trends the self-rotating access log loses.
|
|
# waving-hands-rollup.sh [YYYY-MM-DD] (default: yesterday, UTC)
|
|
# Cron: /etc/cron.d/waving-hands, installed by deploy.sh; SCHEDULE below
|
|
# must match it, because the Sentry monitor is told the same shape.
|
|
# Sentry Crons check-in: /root/.waving-hands-sentry-cron-rollup holds the
|
|
# check-in URL (absent = no check-ins).
|
|
set -u
|
|
OUT="/var/lib/waving-hands/rollup.jsonl"
|
|
LOG="/var/log/waving-hands/rollup.log"
|
|
DAY="${1:-$(date -u -d 'yesterday' +%Y-%m-%d)}"
|
|
SCHEDULE="12 0 * * *"
|
|
CRON_URL=$(cat /root/.waving-hands-sentry-cron-rollup 2>/dev/null || true)
|
|
# The check-in carries the schedule: Sentry keeps a monitor only once told its shape.
|
|
# One check-in, when the run ends, carrying its duration: a start and a
|
|
# finish sent seconds apart can reach Sentry in either order, and a start
|
|
# that lands second is never closed. A run that never ends sends nothing,
|
|
# and Sentry marks the night missed.
|
|
START=$SECONDS
|
|
checkin() {
|
|
[ -n "$CRON_URL" ] && curl -sf -o /dev/null -X POST -H "Content-Type: application/json" \
|
|
-d "{\"status\":\"$1\",\"duration\":$((SECONDS - START)),\"monitor_config\":{\"schedule\":{\"type\":\"crontab\",\"value\":\"$SCHEDULE\"},\"checkin_margin\":30,\"max_runtime\":10,\"timezone\":\"UTC\"}}" \
|
|
"$CRON_URL" || true
|
|
}
|
|
mkdir -p "$(dirname "$LOG")"
|
|
if python3 - "$DAY" "$OUT" <<'PY' >> "$LOG" 2>&1
|
|
import collections, datetime, glob, gzip, json, os, re, shutil, sys
|
|
day, out = sys.argv[1], sys.argv[2]
|
|
d0 = datetime.datetime.fromisoformat(day).replace(tzinfo=datetime.timezone.utc)
|
|
t0, t1 = d0.timestamp(), (d0 + datetime.timedelta(days=1)).timestamp()
|
|
sys.path.insert(0, "/usr/local/lib/waving-hands")
|
|
import traffic
|
|
t = traffic.scan(day)
|
|
|
|
created = started = finished = turned = turns = 0
|
|
humans, bots = set(), 0
|
|
for f in glob.glob("/var/lib/waving-hands/rooms/*.jsonl"):
|
|
try: lines = [json.loads(l) for l in open(f) if l.strip()]
|
|
except Exception: continue
|
|
if not lines or lines[0].get("t") != "room": continue
|
|
made = lines[0]["createdAt"] / 1000
|
|
if t0 <= made < t1:
|
|
created += 1
|
|
if any(x["t"] == "start" for x in lines): started += 1
|
|
if any(x["t"] == "turn" for x in lines): turned += 1
|
|
for x in lines:
|
|
if x["t"] == "seat":
|
|
if x["bot"]: bots += 1
|
|
else: humans.add(x["name"])
|
|
elif x["t"] == "unseat": bots -= 1
|
|
turns += sum(1 for x in lines if x["t"] == "turn" and t0 <= x["at"] / 1000 < t1)
|
|
# The over line is written with the turn that ends the game, so a finish counts on its own day.
|
|
if any(x["t"] == "over" and t0 <= x["at"] / 1000 < t1 for x in lines): finished += 1
|
|
|
|
disk = shutil.disk_usage("/")
|
|
mem = {}
|
|
for line in open("/proc/meminfo"):
|
|
k, v = line.split(":", 1); mem[k] = int(v.split()[0])
|
|
load = os.getloadavg()[1]
|
|
row = dict(day=day, requests=t["requests"], human=t["human"], bot=t["bot"], humanAddresses=len(t["addresses"]), botAddresses=len(t["botAddresses"]),
|
|
paths=dict(t["pages"]), roomLinks=sum(t["joins"].values()), roomsOpened=t["opened"], sharedOn=dict(t["shared"]),
|
|
scanners=t["scanners"], monitors=t["monitors"], referrers=dict(t["referrers"]), campaigns=dict(t["campaigns"]),
|
|
rooms=dict(created=created, started=started, withTurns=turned, finished=finished, humanNames=len(humans), botSeats=bots),
|
|
turns=turns, ledgers=len(glob.glob("/var/lib/waving-hands/rooms/*.jsonl")),
|
|
vitals=dict(diskUsedPct=round(disk.used * 100 / disk.total), memAvailableMb=mem.get("MemAvailable", 0) // 1024, load5=round(load, 2)),
|
|
at=datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"))
|
|
rows = []
|
|
if os.path.exists(out):
|
|
rows = [json.loads(l) for l in open(out) if l.strip()]
|
|
rows = [r for r in rows if r.get("day") != day] + [row]
|
|
rows.sort(key=lambda r: r["day"])
|
|
with open(out + ".tmp", "w") as fh:
|
|
for r in rows: fh.write(json.dumps(r) + "\n")
|
|
os.replace(out + ".tmp", out)
|
|
print("%s rolled: %d people, %d requests, %d rooms" % (day, len(t["addresses"]), t["human"], created))
|
|
PY
|
|
then checkin ok; else checkin error; fi
|