feat(primitives): metrics-scrape + log-ship shell primitives
This commit is contained in:
parent
48cff91056
commit
e49660cd69
2 changed files with 165 additions and 0 deletions
83
_primitives/log-ship.sh
Executable file
83
_primitives/log-ship.sh
Executable file
|
|
@ -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
|
||||
82
_primitives/metrics-scrape.sh
Executable file
82
_primitives/metrics-scrape.sh
Executable file
|
|
@ -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 <url> # table (default)
|
||||
# metrics-scrape <url> --format json # JSON array, needs jq
|
||||
# metrics-scrape <url> --format table # aligned table
|
||||
# metrics-scrape <url> --format alert-check # non-zero exit if any filtered metric > threshold
|
||||
# metrics-scrape <url> --filter <regex> # only lines whose metric name matches
|
||||
# metrics-scrape <url> --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
|
||||
Loading…
Reference in a new issue