#!/usr/bin/env bash # kandev-logs — fetch logs for a running kandev instance by backend port. # # Two modes: # --export (default) GET /api/v1/system/debug/export[?level=LEVEL], pretty # printed with jq. This is the structured in-memory log # buffer + runtime metadata. # --tail Tail the stderr log file written by dev-isolated # (/tmp/kandev-iso--*/backend.log), or a file given # with --file. # # Usage: # scripts/kandev-logs [--export] [--level LEVEL] # scripts/kandev-logs --tail [--lines N] [--file PATH] # # Options: # --export Hit the debug-export endpoint (default). # --level LEVEL Filter export by level. The export endpoint matches the # log entry's level string exactly (zap lowercase levels), # so LEVEL must be one of: debug|info|warn|error|dpanic| # panic|fatal. Passes ?level=LEVEL and prints only .logs. # --tail Tail the backend stderr log file instead of the endpoint. # --lines N Lines to show with --tail (default: 80). # --file PATH Explicit log file for --tail (overrides auto-resolve). # -h, --help Show this help. # # Examples: # scripts/kandev-logs 48429 --export # scripts/kandev-logs 48429 --export --level error # scripts/kandev-logs 48429 --tail --lines 120 # set -euo pipefail MODE="export" LEVEL="" LINES=80 FILE="" PORT="" # Levels the export endpoint can match (zapcore.Level.String(), lowercase). The # endpoint compares the entry's level string exactly, so any other value would # silently match nothing — we reject it up front instead. ALLOWED_LEVELS=(debug info warn error dpanic panic fatal) usage() { sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'; exit "${1:-2}"; } while [[ $# -gt 0 ]]; do case "$1" in --export) MODE="export"; shift ;; --tail) MODE="tail"; shift ;; --level) LEVEL="${2:?--level requires a value}"; shift 2 ;; --lines) LINES="${2:?--lines requires a number}"; shift 2 ;; --file) FILE="${2:?--file requires a path}"; shift 2 ;; -h|--help) usage 0 ;; --*) echo "kandev-logs: unknown flag '$1'" >&2; usage 2 ;; *) if [[ -z "$PORT" ]]; then PORT="$1"; else echo "kandev-logs: unexpected argument '$1'" >&2; usage 2 fi shift ;; esac done [[ -n "$PORT" ]] || { echo "kandev-logs: a backend port is required." >&2; usage 2; } [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "kandev-logs: port must be numeric, got '$PORT'" >&2; exit 2; } # Validate --level against the allowed set BEFORE it reaches the URL. An invalid # value would otherwise be interpolated into ?level= and silently return zero # matching logs, masking the typo. if [[ -n "$LEVEL" ]]; then level_ok=0 for lvl in "${ALLOWED_LEVELS[@]}"; do [[ "$LEVEL" == "$lvl" ]] && { level_ok=1; break; } done if [[ "$level_ok" -ne 1 ]]; then echo "kandev-logs: invalid --level '$LEVEL'. Allowed: ${ALLOWED_LEVELS[*]}" >&2 exit 2 fi fi if [[ "$MODE" == "export" ]]; then url="http://localhost:$PORT/api/v1/system/debug/export" # LEVEL is whitelisted above (alphabetic, no metacharacters), so it is safe to # interpolate directly; the query value needs no further encoding. [[ -n "$LEVEL" ]] && url="$url?level=$LEVEL" if ! body="$(curl -s --fail-with-body "$url" 2>/dev/null)"; then echo "kandev-logs: request to $url failed (is the instance on port $PORT up?)" >&2 exit 1 fi if command -v jq >/dev/null 2>&1; then if [[ -n "$LEVEL" ]]; then printf '%s' "$body" | jq '.logs' else printf '%s' "$body" | jq . fi else printf '%s\n' "$body" fi exit 0 fi # --- tail mode --- if [[ -z "$FILE" ]]; then # Resolve the dev-isolated log convention: /tmp/kandev-iso--*/backend.log # Pick the most recent match. FILE="$(ls -t /tmp/kandev-iso-"$PORT"-*/backend.log 2>/dev/null | head -1 || true)" fi [[ -n "$FILE" && -f "$FILE" ]] || { echo "kandev-logs: no log file found for port $PORT. Pass --file PATH." >&2 exit 1 } echo "kandev-logs: tail -n $LINES $FILE" >&2 tail -n "$LINES" "$FILE"