disinto/site/collect-metrics.sh

238 lines
10 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# =============================================================================
# collect-metrics.sh — Collect factory metrics and write JSON for the dashboard
#
# Queries forge API for PR/issue stats across all managed projects,
# counts vault decisions, and checks CI pass rates. Writes a JSON snapshot
# to the live site directory so the dashboard can fetch it.
#
# Usage:
# bash site/collect-metrics.sh
#
# Cron: 0 */6 * * * cd /home/debian/dark-factory && bash site/collect-metrics.sh
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
FACTORY_ROOT="$(dirname "$SCRIPT_DIR")"
# shellcheck source=../lib/env.sh
source "$FACTORY_ROOT/lib/env.sh"
# shellcheck source=../lib/ci-helpers.sh
source "$FACTORY_ROOT/lib/ci-helpers.sh" 2>/dev/null || true
LOGFILE="${DISINTO_LOG_DIR}/site/collect-metrics.log"
log() {
printf '[%s] collect-metrics: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
}
# Output path: write to live site root if deployed, else to site/data/
SITE_ROOT="${DISINTO_SITE_ROOT:-/home/debian/disinto-site}"
if [ -d "$SITE_ROOT" ] || [ -L "$SITE_ROOT" ]; then
OUTPUT_DIR="$(readlink -f "$SITE_ROOT")/data"
else
OUTPUT_DIR="${SCRIPT_DIR}/data"
fi
mkdir -p "$OUTPUT_DIR"
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%SZ)
WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
MONTH_AGO=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
# ── Per-project metrics ─────────────────────────────────────────────────────
collect_project_metrics() {
local project_toml="$1"
local repo repo_name
repo=$(grep '^repo ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
repo_name=$(grep '^name ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//')
local forge_url
forge_url=$(grep '^forge_url ' "$project_toml" | head -1 | sed 's/.*= *"//;s/"//') 2>/dev/null || true
forge_url="${forge_url:-${FORGE_URL:-http://localhost:3000}}"
local api_base="${forge_url}/api/v1/repos/${repo}"
# PRs merged (all time via state=closed + merged marker)
local prs_merged_week=0 prs_merged_month=0 prs_merged_total=0
local closed_prs
closed_prs=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/pulls?state=closed&sort=updated&limit=50" 2>/dev/null || echo "[]")
prs_merged_total=$(printf '%s' "$closed_prs" | jq '[.[] | select(.merged)] | length' 2>/dev/null || echo 0)
if [ -n "$WEEK_AGO" ]; then
prs_merged_week=$(printf '%s' "$closed_prs" | \
jq --arg since "$WEEK_AGO" '[.[] | select(.merged and .merged_at >= $since)] | length' 2>/dev/null || echo 0)
fi
if [ -n "$MONTH_AGO" ]; then
prs_merged_month=$(printf '%s' "$closed_prs" | \
jq --arg since "$MONTH_AGO" '[.[] | select(.merged and .merged_at >= $since)] | length' 2>/dev/null || echo 0)
fi
# Issues closed
local issues_closed_week=0 issues_closed_month=0
local closed_issues
closed_issues=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=closed&sort=updated&type=issues&limit=50" 2>/dev/null || echo "[]")
if [ -n "$WEEK_AGO" ]; then
issues_closed_week=$(printf '%s' "$closed_issues" | \
jq --arg since "$WEEK_AGO" '[.[] | select(.closed_at >= $since)] | length' 2>/dev/null || echo 0)
fi
if [ -n "$MONTH_AGO" ]; then
issues_closed_month=$(printf '%s' "$closed_issues" | \
jq --arg since "$MONTH_AGO" '[.[] | select(.closed_at >= $since)] | length' 2>/dev/null || echo 0)
fi
local total_closed_header
total_closed_header=$(curl -sf -I -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=closed&type=issues&limit=1" 2>/dev/null | grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
local issues_closed_total="${total_closed_header:-0}"
# Open issues by label
local backlog_count in_progress_count blocked_count
backlog_count=$(curl -sf -I -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=backlog&type=issues&limit=1" 2>/dev/null | \
grep -i 'x-total-count' | tr -d '\r' | awk '{print $2}' || echo "0")
in_progress_count=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=in-progress&type=issues&limit=50" 2>/dev/null | \
jq 'length' 2>/dev/null || echo 0)
blocked_count=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \
"${api_base}/issues?state=open&labels=blocked&type=issues&limit=50" 2>/dev/null | \
jq 'length' 2>/dev/null || echo 0)
jq -nc \
--arg name "$repo_name" \
--arg repo "$repo" \
--argjson prs_week "$prs_merged_week" \
--argjson prs_month "$prs_merged_month" \
--argjson prs_total "${prs_merged_total:-0}" \
--argjson issues_week "$issues_closed_week" \
--argjson issues_month "$issues_closed_month" \
--argjson issues_total "${issues_closed_total:-0}" \
--argjson backlog "${backlog_count:-0}" \
--argjson in_progress "${in_progress_count:-0}" \
--argjson blocked "${blocked_count:-0}" \
'{
name: $name,
repo: $repo,
prs_merged: { week: $prs_week, month: $prs_month, total: $prs_total },
issues_closed: { week: $issues_week, month: $issues_month, total: $issues_total },
backlog: { queued: $backlog, in_progress: $in_progress, blocked: $blocked }
}'
}
# ── Vault decisions ─────────────────────────────────────────────────────────
collect_vault_metrics() {
local vault_dir="${FACTORY_ROOT}/vault"
local approved=0 rejected=0 escalated=0 pending=0 fired=0
[ -d "$vault_dir/fired" ] && fired=$(find "$vault_dir/fired" -name '*.json' 2>/dev/null | wc -l)
[ -d "$vault_dir/approved" ] && approved=$(find "$vault_dir/approved" -name '*.json' 2>/dev/null | wc -l)
[ -d "$vault_dir/rejected" ] && rejected=$(find "$vault_dir/rejected" -name '*.json' 2>/dev/null | wc -l)
[ -d "$vault_dir/pending" ] && {
pending=$(find "$vault_dir/pending" -name '*.json' 2>/dev/null | wc -l)
escalated=$(find "$vault_dir/pending" -name '*.json' -exec grep -l '"escalated"' {} + 2>/dev/null | wc -l)
pending=$((pending - escalated))
}
jq -nc \
--argjson approved "$((approved + fired))" \
--argjson escalated "$escalated" \
--argjson rejected "$rejected" \
--argjson pending "$pending" \
'{ approved: $approved, escalated: $escalated, rejected: $rejected, pending: $pending }'
}
# ── CI pass rate ────────────────────────────────────────────────────────────
collect_ci_metrics() {
local total=0 passed=0 failed=0 rate=0
# Query Woodpecker DB for last 30 days across all repos
local ci_stats
ci_stats=$(wpdb -A -c "
SELECT status, count(*)
FROM pipelines
WHERE finished > 0
AND to_timestamp(finished) > now() - interval '30 days'
GROUP BY status;" 2>/dev/null || echo "")
if [ -n "$ci_stats" ]; then
passed=$(echo "$ci_stats" | awk '$1 == "success" {print $3}' | tr -d ' ' || echo 0)
failed=$(echo "$ci_stats" | awk '$1 == "failure" {print $3}' | tr -d ' ' || echo 0)
passed=${passed:-0}
failed=${failed:-0}
total=$((passed + failed))
if [ "$total" -gt 0 ]; then
rate=$(( (passed * 100) / total ))
fi
fi
jq -nc \
--argjson total "$total" \
--argjson passed "$passed" \
--argjson failed "$failed" \
--argjson rate "$rate" \
'{ total: $total, passed: $passed, failed: $failed, pass_rate: $rate }'
}
# ── Agent activity ──────────────────────────────────────────────────────────
collect_agent_metrics() {
local active_sessions=0
active_sessions=$(tmux list-sessions 2>/dev/null | wc -l || echo 0)
local agents='[]'
local agent_name log_path age_min last_active
for log_entry in dev/dev-agent.log review/review.log gardener/gardener.log \
planner/planner.log predictor/predictor.log supervisor/supervisor.log \
vault/vault.log; do
agent_name=$(basename "$(dirname "$log_entry")")
log_path="${FACTORY_ROOT}/${log_entry}"
if [ -f "$log_path" ]; then
age_min=$(( ($(date +%s) - $(stat -c %Y "$log_path" 2>/dev/null || echo 0)) / 60 ))
last_active=$(date -u -d "@$(stat -c %Y "$log_path" 2>/dev/null || echo 0)" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
agents=$(printf '%s' "$agents" | jq --arg n "$agent_name" --arg t "$last_active" --argjson a "$age_min" \
'. + [{ name: $n, last_active: $t, idle_minutes: $a }]')
fi
done
jq -nc --argjson sessions "$active_sessions" --argjson agents "$agents" \
'{ active_sessions: $sessions, agents: $agents }'
}
# ── Assemble ────────────────────────────────────────────────────────────────
log "Starting metrics collection"
PROJECTS_JSON="[]"
for toml in "$FACTORY_ROOT"/projects/*.toml; do
[ -f "$toml" ] || continue
log "Collecting metrics for $(basename "$toml")"
project_json=$(collect_project_metrics "$toml" 2>/dev/null || echo '{}')
PROJECTS_JSON=$(printf '%s\n%s' "$PROJECTS_JSON" "$project_json" | jq -s '.[0] + [.[1]]')
done
VAULT_JSON=$(collect_vault_metrics 2>/dev/null || echo '{}')
CI_JSON=$(collect_ci_metrics 2>/dev/null || echo '{}')
AGENTS_JSON=$(collect_agent_metrics 2>/dev/null || echo '{}')
# Build final output
jq -nc \
--arg ts "$NOW_ISO" \
--argjson projects "$PROJECTS_JSON" \
--argjson vault "$VAULT_JSON" \
--argjson ci "$CI_JSON" \
--argjson agents "$AGENTS_JSON" \
'{
generated_at: $ts,
projects: $projects,
vault: $vault,
ci: $ci,
agents: $agents
}' > "${OUTPUT_DIR}/metrics.json"
log "Metrics written to ${OUTPUT_DIR}/metrics.json"