KeiSeiKit-1.0/_primitives/log-ship.sh

83 lines
3.4 KiB
Bash
Executable file

#!/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