From 09fe677f363ed58679f79c6415fdb9d639f23f98 Mon Sep 17 00:00:00 2001 From: Azat Date: Mon, 2 Feb 2026 22:29:24 +0100 Subject: [PATCH] Initial metrics skill with aggregation --- SKILL.md | 73 ++++++++++++++++++++++++++ scripts/autorun.sh | 124 +++++++++++++++++++++++++++++++++++++++++++++ scripts/run.sh | 107 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 SKILL.md create mode 100644 scripts/autorun.sh create mode 100644 scripts/run.sh diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..24a5907 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,73 @@ +--- +name: metrics +description: Metrics aggregator with system and skill metrics +metadata: + version: "1.0.0" + vibestack: + main: false + metrics-port: 9090 +--- + +# Metrics Skill + +Aggregates metrics from system (node_exporter) and other skills into a single `/metrics` endpoint. + +## Features + +- System metrics via node_exporter (CPU, memory, disk, network) +- Auto-discovers skills that expose metrics +- Single aggregated endpoint for Prometheus scraping +- Auto-registers with Caddy if present + +## How Skills Expose Metrics + +Skills can expose metrics by adding to their SKILL.md: + +```yaml +metadata: + vibestack: + metrics-port: 8080 +``` + +Or by setting environment variable: +```bash +METRICS_PORT_myskill=8080 +``` + +The aggregator will scrape `localhost:{port}/metrics` for each discovered skill. + +## Configuration + +| Variable | Description | Default | +|----------|-------------|---------| +| `METRICS_PORT` | Aggregator port | `9090` | +| `NODE_EXPORTER_PORT` | node_exporter port | `9100` | +| `METRICS_DOMAIN` | Domain for Caddy auto-config | (none) | +| `METRICS_PATH` | URL path for metrics | `/metrics` | + +## Collected Metrics + +### System (node_exporter) +- `node_cpu_*` - CPU usage +- `node_memory_*` - Memory usage +- `node_disk_*` - Disk I/O +- `node_network_*` - Network I/O +- `node_filesystem_*` - Filesystem usage + +### Per-Skill +Each skill's metrics are prefixed with the skill name: +``` +# From ttyd skill +ttyd_connections_active 5 +ttyd_sessions_total 123 +``` + +## Usage + +```bash +# Scrape aggregated metrics +curl http://localhost:9090/metrics + +# With Caddy proxy +curl https://metrics.example.com/metrics +``` diff --git a/scripts/autorun.sh b/scripts/autorun.sh new file mode 100644 index 0000000..7434f61 --- /dev/null +++ b/scripts/autorun.sh @@ -0,0 +1,124 @@ +#!/bin/bash +set -e + +NODE_EXPORTER_VERSION="${NODE_EXPORTER_VERSION:-1.7.0}" +SKILLS_DIR="${SKILLS_DIR:-/skills}" + +# Idempotent node_exporter installation +install_node_exporter() { + if command -v node_exporter &>/dev/null; then + echo "node_exporter already installed" + return 0 + fi + + echo "Installing node_exporter v${NODE_EXPORTER_VERSION}..." + + local arch + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + armv7l) arch="armv7" ;; + *) + echo "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + + local url="https://github.com/prometheus/node_exporter/releases/download/v${NODE_EXPORTER_VERSION}/node_exporter-${NODE_EXPORTER_VERSION}.linux-${arch}.tar.gz" + + curl -sSL "$url" -o /tmp/node_exporter.tar.gz + tar -xzf /tmp/node_exporter.tar.gz -C /tmp + mv /tmp/node_exporter-*/node_exporter /usr/local/bin/ + rm -rf /tmp/node_exporter* + + echo "node_exporter installed" +} + +# Discover skills with metrics ports +discover_metrics_targets() { + local targets_file="/tmp/metrics_targets.txt" + echo "# Auto-discovered metrics targets" > "$targets_file" + + for skill_dir in "$SKILLS_DIR"/*/; do + local skill_name=$(basename "$skill_dir") + local skill_md="$skill_dir/SKILL.md" + + # Skip self + [ "$skill_name" = "metrics" ] && continue + + # Check for metrics-port in SKILL.md + if [ -f "$skill_md" ]; then + local port=$(yq -r '.metadata.vibestack."metrics-port" // empty' < <(sed -n '/^---$/,/^---$/p' "$skill_md" | sed '1d;$d') 2>/dev/null) + if [ -n "$port" ] && [ "$port" != "null" ]; then + echo "$skill_name localhost:$port" >> "$targets_file" + echo " $skill_name: metrics on port $port" + fi + fi + + # Check for METRICS_PORT_skillname env var + local env_var="METRICS_PORT_${skill_name//-/_}" + local env_port="${!env_var}" + if [ -n "$env_port" ]; then + echo "$skill_name localhost:$env_port" >> "$targets_file" + echo " $skill_name: metrics on port $env_port (from env)" + fi + done + + echo "Targets written to $targets_file" +} + +# Configure Caddy if present +configure_caddy() { + local caddy_dir="$SKILLS_DIR/caddy" + local metrics_port="${METRICS_PORT:-9090}" + local metrics_domain="${METRICS_DOMAIN:-}" + + if [ ! -d "$caddy_dir" ]; then + echo "Caddy not found - metrics on port $metrics_port" + return 0 + fi + + echo "Caddy detected - configuring reverse proxy..." + mkdir -p "$caddy_dir/snippets.d" + + local snippet="$caddy_dir/snippets.d/metrics.caddy" + + if [ -n "$metrics_domain" ]; then + cat > "$snippet" << EOF +# Auto-generated by metrics skill +$metrics_domain { + reverse_proxy localhost:$metrics_port +} +EOF + echo "Caddy config: $metrics_domain -> localhost:$metrics_port" + else + cat > "$snippet" << EOF +# Auto-generated by metrics skill +# Add to your site block: +# handle /metrics { +# reverse_proxy localhost:$metrics_port +# } +EOF + echo "Caddy snippet created (manual config needed)" + fi +} + +# Install socat for HTTP server +install_socat() { + if command -v socat &>/dev/null; then + echo "socat already installed" + return 0 + fi + + echo "Installing socat..." + apt-get update + apt-get install -y socat + echo "socat installed" +} + +install_node_exporter +install_socat +discover_metrics_targets +configure_caddy + +echo "Metrics setup complete" diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..2f807c1 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,107 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(dirname "$0")" +METRICS_PORT="${METRICS_PORT:-9090}" +NODE_EXPORTER_PORT="${NODE_EXPORTER_PORT:-9100}" +TARGETS_FILE="/tmp/metrics_targets.txt" + +# Start node_exporter in background +start_node_exporter() { + echo "Starting node_exporter on port $NODE_EXPORTER_PORT..." + node_exporter --web.listen-address=":$NODE_EXPORTER_PORT" & + NODE_EXPORTER_PID=$! + sleep 2 + + if ! kill -0 $NODE_EXPORTER_PID 2>/dev/null; then + echo "Failed to start node_exporter" + exit 1 + fi + echo "node_exporter running (PID $NODE_EXPORTER_PID)" +} + +# Create the aggregator CGI script +create_aggregator_script() { + cat > /tmp/aggregate.sh << 'SCRIPT' +#!/bin/bash +NODE_EXPORTER_PORT="${NODE_EXPORTER_PORT:-9100}" +TARGETS_FILE="/tmp/metrics_targets.txt" + +# Collect all metrics +{ + # System metrics + echo "# HELP vibestack_up Whether the metrics aggregator is up" + echo "# TYPE vibestack_up gauge" + echo "vibestack_up 1" + echo "" + + # node_exporter metrics + curl -s "http://localhost:$NODE_EXPORTER_PORT/metrics" 2>/dev/null || true + + # Skill metrics + if [ -f "$TARGETS_FILE" ]; then + while IFS=' ' read -r skill_name target || [ -n "$skill_name" ]; do + [[ "$skill_name" =~ ^#.*$ ]] && continue + [ -z "$skill_name" ] && continue + [ -z "$target" ] && continue + + echo "" + echo "# Metrics from skill: $skill_name" + curl -s "http://${target}/metrics" 2>/dev/null || true + done < "$TARGETS_FILE" + fi +} +SCRIPT + chmod +x /tmp/aggregate.sh +} + +# Serve metrics using socat +serve_metrics() { + echo "Starting metrics aggregator on port $METRICS_PORT..." + + if ! command -v socat &>/dev/null; then + echo "Installing socat..." + apt-get update && apt-get install -y socat + fi + + # Use socat to handle HTTP requests + exec socat TCP-LISTEN:$METRICS_PORT,reuseaddr,fork EXEC:"/tmp/metrics_handler.sh" +} + +# Create HTTP handler script +create_handler_script() { + cat > /tmp/metrics_handler.sh << 'HANDLER' +#!/bin/bash +# Read HTTP request (we ignore it, always serve /metrics) +read -r request_line +while read -r header; do + [ "$header" = $'\r' ] && break +done + +# Get metrics +metrics=$(/tmp/aggregate.sh) +content_length=${#metrics} + +# Send HTTP response +printf "HTTP/1.1 200 OK\r\n" +printf "Content-Type: text/plain; charset=utf-8\r\n" +printf "Content-Length: %d\r\n" "$content_length" +printf "Connection: close\r\n" +printf "\r\n" +printf "%s" "$metrics" +HANDLER + chmod +x /tmp/metrics_handler.sh +} + +# Cleanup on exit +cleanup() { + echo "Shutting down..." + [ -n "$NODE_EXPORTER_PID" ] && kill $NODE_EXPORTER_PID 2>/dev/null + exit 0 +} +trap cleanup SIGTERM SIGINT + +start_node_exporter +create_aggregator_script +create_handler_script +serve_metrics