diff --git a/_primitives/log-ship.sh b/_primitives/log-ship.sh new file mode 100755 index 0000000..0c0867b --- /dev/null +++ b/_primitives/log-ship.sh @@ -0,0 +1,83 @@ +#!/bin/sh +# log-ship — tee structured JSON-line logs from stdin to stdout and optionally +# forward each line to Loki / Datadog / generic HTTP endpoint. +# Install path: $HOME/.claude/agents/_primitives/log-ship.sh +# POSIX sh. Deps: curl, awk. Optional: jq (for --validate). +# +# Usage: +# cat log.jsonl | log-ship --target stdout +# journalctl -o json | log-ship --target loki --endpoint http://loki:3100/loki/api/v1/push --label job=api +# tail -f app.log | log-ship --target datadog --endpoint https://http-intake.logs.datadoghq.com/api/v2/logs +# cat log.jsonl | log-ship --target http --endpoint https://my.collector/ingest +# cat log.jsonl | log-ship --target stdout --validate +# +# ENV overrides (avoid CLI token leak): +# LOG_SHIP_DD_API_KEY — Datadog API key (HTTP header DD-API-KEY) +# LOG_SHIP_BEARER — generic Bearer token for --target http +# +# Always tees to local stdout first, then forwards. Forwarding failure does NOT +# drop the local tee — observability MUST degrade gracefully. + +set -eu + +TARGET="stdout" +ENDPOINT="" +LABEL="" +VALIDATE=0 + +usage() { sed -n '2,17p' "$0" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage ;; + --target) TARGET="${2:-stdout}"; shift 2 ;; + --endpoint) ENDPOINT="${2:-}"; shift 2 ;; + --label) LABEL="${2:-}"; shift 2 ;; + --validate) VALIDATE=1; shift ;; + *) echo "[log-ship] unknown arg: $1" >&2; exit 2 ;; + esac +done + +case "$TARGET" in stdout|loki|datadog|http) ;; *) echo "[log-ship] bad target: $TARGET" >&2; exit 2 ;; esac +[ "$TARGET" != "stdout" ] && [ -z "$ENDPOINT" ] && { echo "[log-ship] --endpoint required for target=$TARGET" >&2; exit 2; } +[ "$VALIDATE" = 1 ] && ! command -v jq >/dev/null 2>&1 && { echo "[log-ship] jq required for --validate" >&2; exit 1; } +command -v curl >/dev/null 2>&1 || { echo "[log-ship] curl required" >&2; exit 1; } + +forward() { + LINE="$1" + case "$TARGET" in + stdout) : ;; + loki) + NS=$(awk 'BEGIN{srand(); printf "%d000000000", systime()}') + ESC=$(printf '%s' "$LINE" | awk '{ gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); print }') + curl -fsS --max-time 5 -H 'Content-Type: application/json' \ + -X POST "$ENDPOINT" -d "{\"streams\":[{\"stream\":{\"job\":\"${LABEL:-log-ship}\"},\"values\":[[\"$NS\",\"$ESC\"]]}]}" \ + >/dev/null 2>&1 || echo "[log-ship] loki push failed (tee OK)" >&2 + ;; + datadog) + KEY="${LOG_SHIP_DD_API_KEY:-}" + [ -z "$KEY" ] && { echo "[log-ship] LOG_SHIP_DD_API_KEY unset" >&2; return; } + curl -fsS --max-time 5 -H "DD-API-KEY: $KEY" -H 'Content-Type: application/json' \ + -X POST "$ENDPOINT" -d "[$LINE]" >/dev/null 2>&1 \ + || echo "[log-ship] datadog push failed (tee OK)" >&2 + ;; + http) + AUTH="" + [ -n "${LOG_SHIP_BEARER:-}" ] && AUTH="-H Authorization: Bearer $LOG_SHIP_BEARER" + # shellcheck disable=SC2086 + curl -fsS --max-time 5 $AUTH -H 'Content-Type: application/json' \ + -X POST "$ENDPOINT" -d "$LINE" >/dev/null 2>&1 \ + || echo "[log-ship] http push failed (tee OK)" >&2 + ;; + esac +} + +# Main loop: one JSON object per line. Tee first, validate optional, forward. +while IFS= read -r line; do + [ -z "$line" ] && continue + printf '%s\n' "$line" + if [ "$VALIDATE" = 1 ]; then + printf '%s' "$line" | jq -e . >/dev/null 2>&1 || { echo "[log-ship] WARN invalid JSON: $line" >&2; continue; } + fi + [ "$TARGET" = "stdout" ] || forward "$line" +done diff --git a/_primitives/metrics-scrape.sh b/_primitives/metrics-scrape.sh new file mode 100755 index 0000000..cd04b90 --- /dev/null +++ b/_primitives/metrics-scrape.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# metrics-scrape — scrape a Prometheus /metrics endpoint, parse and pretty-print. +# Install path: $HOME/.claude/agents/_primitives/metrics-scrape.sh +# POSIX sh. Deps: curl, awk. Optional: jq (for --format json). +# +# Usage: +# metrics-scrape # table (default) +# metrics-scrape --format json # JSON array, needs jq +# metrics-scrape --format table # aligned table +# metrics-scrape --format alert-check # non-zero exit if any filtered metric > threshold +# metrics-scrape --filter # only lines whose metric name matches +# metrics-scrape --format alert-check --filter '^http_requests_total' --threshold 1000 + +set -eu + +URL="" +FORMAT="table" +FILTER="" +THRESHOLD="" + +usage() { + sed -n '2,12p' "$0" >&2 + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) usage ;; + --format) FORMAT="${2:-table}"; shift 2 ;; + --filter) FILTER="${2:-}"; shift 2 ;; + --threshold) THRESHOLD="${2:-}"; shift 2 ;; + --*) echo "[metrics-scrape] unknown flag: $1" >&2; exit 2 ;; + *) [ -z "$URL" ] && URL="$1" || { echo "[metrics-scrape] extra arg: $1" >&2; exit 2; }; shift ;; + esac +done + +[ -z "$URL" ] && { echo "[metrics-scrape] missing URL" >&2; usage; } +command -v curl >/dev/null 2>&1 || { echo "[metrics-scrape] curl required" >&2; exit 1; } + +RAW=$(curl -fsS --max-time 10 "$URL") || { echo "[metrics-scrape] scrape failed: $URL" >&2; exit 3; } + +# Strip HELP/TYPE comments and blanks. Optionally filter by metric-name regex. +parse() { + printf '%s\n' "$RAW" | awk -v f="$FILTER" ' + /^[[:space:]]*$/ { next } + /^#/ { next } + { + name=$1; sub(/\{.*/, "", name) + if (f == "" || name ~ f) print $0 + }' +} + +case "$FORMAT" in + table) + parse | awk ' + BEGIN { printf "%-60s %s\n", "METRIC", "VALUE"; printf "%-60s %s\n", "------", "-----" } + { printf "%-60s %s\n", substr($0, 1, length($0)-length($NF)-1), $NF }' + ;; + json) + command -v jq >/dev/null 2>&1 || { echo "[metrics-scrape] jq required for --format json" >&2; exit 1; } + parse | awk ' + BEGIN { print "[" ; first=1 } + { + val=$NF; line=$0; sub(/[[:space:]]+[^[:space:]]+$/, "", line) + if (!first) printf ",\n"; first=0 + gsub(/"/, "\\\"", line) + printf " {\"metric\":\"%s\",\"value\":\"%s\"}", line, val + } + END { print "\n]" }' | jq '.' + ;; + alert-check) + [ -z "$THRESHOLD" ] && { echo "[metrics-scrape] --threshold required for alert-check" >&2; exit 2; } + OVER=$(parse | awk -v t="$THRESHOLD" '$NF+0 > t+0 { print $0 }') + if [ -n "$OVER" ]; then + echo "[metrics-scrape] ALERT — $(printf '%s\n' "$OVER" | wc -l | tr -d ' ') metrics over threshold=$THRESHOLD:" >&2 + printf '%s\n' "$OVER" >&2 + exit 4 + fi + echo "[metrics-scrape] OK — all filtered metrics <= $THRESHOLD" >&2 + ;; + *) echo "[metrics-scrape] unknown format: $FORMAT (table|json|alert-check)" >&2; exit 2 ;; +esac