Files
blackroad/scripts/blackroad-cost-tracker.sh
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

460 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# BlackRoad Cost Tracker
# Track resource usage and costs across the cluster
set -eo pipefail
# Source centralized config
NODES_CONFIG="$HOME/.blackroad/config/nodes.sh"
if [[ -f "$NODES_CONFIG" ]]; then
source "$NODES_CONFIG"
else
PINK='\033[38;5;205m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'
YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; RESET='\033[0m'
fi
# Dependency check
for dep in sqlite3 bc; do
command -v "$dep" &>/dev/null || { echo "ERROR: $dep required" >&2; exit 1; }
done
# SQL-safe string escaping
_sql_escape() { echo "$1" | sed "s/'/''/g"; }
COST_DIR="$HOME/.blackroad/costs"
COST_DB="$COST_DIR/costs.db"
# Default rates (can be customized)
RATE_CPU_HOUR=0.001 # $ per CPU-hour
RATE_MEM_GB_HOUR=0.0005 # $ per GB-hour
RATE_GPU_HOUR=0.01 # $ per GPU-hour (Hailo)
RATE_INFERENCE=0.0001 # $ per inference request
RATE_TOKEN=0.000001 # $ per token
# Initialize
init() {
mkdir -p "$COST_DIR"/{reports,budgets}
sqlite3 "$COST_DB" << 'SQL'
CREATE TABLE IF NOT EXISTS usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
node TEXT,
project TEXT DEFAULT 'default',
resource_type TEXT,
quantity REAL,
unit TEXT,
cost REAL
);
CREATE TABLE IF NOT EXISTS rates (
resource_type TEXT PRIMARY KEY,
rate REAL,
unit TEXT,
description TEXT
);
CREATE TABLE IF NOT EXISTS budgets (
project TEXT PRIMARY KEY,
monthly_limit REAL,
alert_threshold REAL DEFAULT 0.8,
current_spend REAL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS invoices (
id TEXT PRIMARY KEY,
project TEXT,
period_start DATE,
period_end DATE,
total REAL,
status TEXT DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_project ON usage(project);
CREATE INDEX IF NOT EXISTS idx_timestamp ON usage(timestamp);
SQL
# Seed default rates
seed_rates
echo -e "${GREEN}Cost tracker initialized${RESET}"
}
# Seed default rates
seed_rates() {
sqlite3 "$COST_DB" << SQL
INSERT OR IGNORE INTO rates (resource_type, rate, unit, description) VALUES
('cpu', $RATE_CPU_HOUR, 'cpu-hour', 'CPU compute time'),
('memory', $RATE_MEM_GB_HOUR, 'gb-hour', 'Memory usage'),
('gpu', $RATE_GPU_HOUR, 'gpu-hour', 'Hailo accelerator time'),
('inference', $RATE_INFERENCE, 'request', 'LLM inference request'),
('tokens', $RATE_TOKEN, 'token', 'Input/output tokens'),
('storage', 0.00001, 'gb-hour', 'Disk storage'),
('network', 0.00001, 'gb', 'Network transfer');
SQL
}
# Record usage
record() {
local resource="$1"
local quantity="$2"
local project="${3:-default}"
local node="${4:-$(hostname)}"
local rate=$(sqlite3 "$COST_DB" "SELECT rate FROM rates WHERE resource_type = '$resource'")
local cost=$(echo "scale=6; $quantity * $rate" | bc)
sqlite3 "$COST_DB" "
INSERT INTO usage (node, project, resource_type, quantity, unit, cost)
VALUES ('$node', '$project', '$resource', $quantity, (SELECT unit FROM rates WHERE resource_type = '$resource'), $cost)
"
# Update budget
sqlite3 "$COST_DB" "
UPDATE budgets SET current_spend = current_spend + $cost WHERE project = '$project'
"
echo -e "${GREEN}Recorded: $quantity $resource = \$$cost${RESET}"
}
# Record inference usage
record_inference() {
local project="${1:-default}"
local tokens_in="${2:-0}"
local tokens_out="${3:-0}"
local node="${4:-$(hostname)}"
record "inference" 1 "$project" "$node"
record "tokens" "$((tokens_in + tokens_out))" "$project" "$node"
}
# Set rate
set_rate() {
local resource="$1"
local rate="$2"
local unit="${3:-unit}"
sqlite3 "$COST_DB" "
INSERT OR REPLACE INTO rates (resource_type, rate, unit)
VALUES ('$resource', $rate, '$unit')
"
echo -e "${GREEN}Rate set: $resource = \$$rate per $unit${RESET}"
}
# List rates
rates() {
echo -e "${PINK}=== RESOURCE RATES ===${RESET}"
echo
sqlite3 "$COST_DB" "SELECT resource_type, rate, unit, description FROM rates ORDER BY resource_type" | \
while IFS='|' read -r resource rate unit desc; do
printf " %-15s \$%-10.6f per %-10s %s\n" "$resource" "$rate" "$unit" "$desc"
done
}
# Create budget
budget_create() {
local project="$1"
local limit="$2"
local threshold="${3:-0.8}"
sqlite3 "$COST_DB" "
INSERT OR REPLACE INTO budgets (project, monthly_limit, alert_threshold, current_spend)
VALUES ('$project', $limit, $threshold, 0)
"
echo -e "${GREEN}Budget created: $project = \$$limit/month${RESET}"
}
# Check budgets
budget_check() {
echo -e "${PINK}=== BUDGET STATUS ===${RESET}"
echo
sqlite3 "$COST_DB" "SELECT project, monthly_limit, current_spend, alert_threshold FROM budgets" | \
while IFS='|' read -r project limit spend threshold; do
local pct=$(echo "scale=1; $spend * 100 / $limit" | bc 2>/dev/null || echo 0)
local threshold_pct=$(echo "scale=0; $threshold * 100" | bc)
local color=$GREEN
local alert_val=$(echo "$spend / $limit" | bc -l)
if [ "$(echo "$alert_val > $threshold" | bc -l)" = "1" ]; then
color=$YELLOW
fi
if [ "$(echo "$alert_val > 1" | bc -l)" = "1" ]; then
color=$RED
fi
printf " %-15s ${color}\$%.2f / \$%.2f (%.1f%%)${RESET}\n" "$project" "$spend" "$limit" "$pct"
done
}
# Current period costs
current() {
local project="${1:-all}"
local period="${2:-month}"
echo -e "${PINK}=== CURRENT $period COSTS ===${RESET}"
echo
local where=""
[ "$project" != "all" ] && where="AND project = '$project'"
local period_filter
case "$period" in
day) period_filter="date(timestamp) = date('now')" ;;
week) period_filter="datetime(timestamp, '+7 days') > datetime('now')" ;;
month) period_filter="datetime(timestamp, '+1 month') > datetime('now')" ;;
esac
echo "By resource:"
sqlite3 "$COST_DB" "
SELECT resource_type, SUM(quantity), unit, SUM(cost)
FROM usage
WHERE $period_filter $where
GROUP BY resource_type
ORDER BY SUM(cost) DESC
" | while IFS='|' read -r resource qty unit cost; do
printf " %-15s %10.2f %-10s \$%.4f\n" "$resource" "$qty" "$unit" "$cost"
done
echo
echo "By project:"
sqlite3 "$COST_DB" "
SELECT project, SUM(cost)
FROM usage
WHERE $period_filter $where
GROUP BY project
ORDER BY SUM(cost) DESC
" | while IFS='|' read -r proj cost; do
printf " %-15s \$%.4f\n" "$proj" "$cost"
done
echo
echo "By node:"
sqlite3 "$COST_DB" "
SELECT node, SUM(cost)
FROM usage
WHERE $period_filter $where
GROUP BY node
ORDER BY SUM(cost) DESC
" | while IFS='|' read -r node cost; do
printf " %-15s \$%.4f\n" "$node" "$cost"
done
echo
local total=$(sqlite3 "$COST_DB" "SELECT SUM(cost) FROM usage WHERE $period_filter $where")
echo -e "Total: ${GREEN}\$${total:-0}${RESET}"
}
# Generate invoice
invoice() {
local project="$1"
local start_date="${2:-$(date -d 'first day of this month' +%Y-%m-%d 2>/dev/null || date -v1d +%Y-%m-%d)}"
local end_date="${3:-$(date +%Y-%m-%d)}"
local invoice_id="inv_$(date +%Y%m)_${project}"
echo -e "${PINK}=== INVOICE: $invoice_id ===${RESET}"
echo
echo "Project: $project"
echo "Period: $start_date to $end_date"
echo
echo "─────────────────────────────────────────────────────────────────"
printf "%-20s %15s %12s %12s\n" "Resource" "Quantity" "Rate" "Cost"
echo "─────────────────────────────────────────────────────────────────"
local total=0
sqlite3 "$COST_DB" "
SELECT u.resource_type, SUM(u.quantity), u.unit, r.rate, SUM(u.cost)
FROM usage u
JOIN rates r ON u.resource_type = r.resource_type
WHERE u.project = '$project'
AND date(u.timestamp) BETWEEN '$start_date' AND '$end_date'
GROUP BY u.resource_type
" | while IFS='|' read -r resource qty unit rate cost; do
printf "%-20s %12.2f %-3s \$%-8.6f \$%.4f\n" "$resource" "$qty" "$unit" "$rate" "$cost"
total=$(echo "$total + $cost" | bc)
done
echo "─────────────────────────────────────────────────────────────────"
total=$(sqlite3 "$COST_DB" "
SELECT SUM(cost) FROM usage
WHERE project = '$project'
AND date(timestamp) BETWEEN '$start_date' AND '$end_date'
")
printf "%48s \$%.4f\n" "TOTAL:" "$total"
echo
# Save invoice
sqlite3 "$COST_DB" "
INSERT OR REPLACE INTO invoices (id, project, period_start, period_end, total)
VALUES ('$invoice_id', '$project', '$start_date', '$end_date', $total)
"
# Export to file
local invoice_file="$COST_DIR/reports/${invoice_id}.txt"
{
echo "INVOICE: $invoice_id"
echo "Project: $project"
echo "Period: $start_date to $end_date"
echo "Generated: $(date)"
echo ""
echo "Total: \$$total"
} > "$invoice_file"
echo "Saved to: $invoice_file"
}
# Cost forecast
forecast() {
local project="${1:-all}"
local days="${2:-30}"
echo -e "${PINK}=== COST FORECAST ===${RESET}"
echo "Based on last 7 days, projecting $days days"
echo
local where=""
[ "$project" != "all" ] && where="WHERE project = '$project'"
local daily_avg=$(sqlite3 "$COST_DB" "
SELECT SUM(cost) / 7 FROM usage
WHERE datetime(timestamp, '+7 days') > datetime('now')
$where
")
local projected=$(echo "scale=2; $daily_avg * $days" | bc)
echo "Daily average: \$${daily_avg:-0}"
echo "Projected ${days}-day cost: \$$projected"
if [ "$project" != "all" ]; then
local limit=$(sqlite3 "$COST_DB" "SELECT monthly_limit FROM budgets WHERE project = '$project'")
if [ -n "$limit" ]; then
local pct=$(echo "scale=1; $projected * 100 / $limit" | bc)
echo "Budget utilization: ${pct}%"
fi
fi
}
# Collect usage from nodes
collect() {
echo -e "${PINK}=== COLLECTING USAGE ===${RESET}"
echo
for node in "${ALL_NODES[@]}"; do
echo -n " $node: "
if ! ssh -o ConnectTimeout=3 "$node" "echo ok" >/dev/null 2>&1; then
echo "(offline)"
continue
fi
# Get resource usage
local metrics=$(ssh "$node" "
cpu_hours=\$(cat /proc/stat | awk '/^cpu / {print (\$2+\$3+\$4)/100/3600}')
mem_gb=\$(free -g | awk '/Mem:/ {print \$3}')
disk_gb=\$(df / | awk 'NR==2 {print \$3/1024/1024}')
echo \"\$cpu_hours|\$mem_gb|\$disk_gb\"
" 2>/dev/null)
if [ -n "$metrics" ]; then
local cpu=$(echo "$metrics" | cut -d'|' -f1)
local mem=$(echo "$metrics" | cut -d'|' -f2)
local disk=$(echo "$metrics" | cut -d'|' -f3)
record "cpu" "$cpu" "default" "$node" >/dev/null
record "memory" "$mem" "default" "$node" >/dev/null
record "storage" "$disk" "default" "$node" >/dev/null
echo "collected"
else
echo "failed"
fi
done
}
# Reset monthly budgets
reset_budgets() {
sqlite3 "$COST_DB" "UPDATE budgets SET current_spend = 0"
echo -e "${GREEN}Reset all budget counters${RESET}"
}
# Help
help() {
echo -e "${PINK}BlackRoad Cost Tracker${RESET}"
echo
echo "Track resource usage and costs"
echo
echo "Usage Recording:"
echo " record <resource> <qty> [proj] Record usage"
echo " record-inference [proj] [in] [out] Record inference"
echo " collect Collect from nodes"
echo
echo "Rates & Budgets:"
echo " rates List rates"
echo " set-rate <res> <rate> [unit] Set rate"
echo " budget-create <proj> <limit> Create budget"
echo " budget-check Check budgets"
echo
echo "Reports:"
echo " current [proj] [day|week|month] Current costs"
echo " invoice <proj> [start] [end] Generate invoice"
echo " forecast [proj] [days] Cost forecast"
echo
echo "Examples:"
echo " $0 record inference 100 myproject"
echo " $0 budget-create myproject 50"
echo " $0 invoice myproject 2024-01-01"
}
# Ensure initialized
[ -f "$COST_DB" ] || init >/dev/null
case "${1:-help}" in
init)
init
;;
record)
record "$2" "$3" "$4" "$5"
;;
record-inference)
record_inference "$2" "$3" "$4" "$5"
;;
collect)
collect
;;
rates)
rates
;;
set-rate)
set_rate "$2" "$3" "$4"
;;
budget-create|budget)
budget_create "$2" "$3" "$4"
;;
budget-check|budgets)
budget_check
;;
current|costs)
current "$2" "$3"
;;
invoice)
invoice "$2" "$3" "$4"
;;
forecast)
forecast "$2" "$3"
;;
reset-budgets)
reset_budgets
;;
*)
help
;;
esac