diff --git a/dot_config/private_k9s/plugins/executable_loki-logcli b/dot_config/private_k9s/plugins/executable_loki-logcli old mode 100644 new mode 100755 index 4348cc8..df0e362 --- a/dot_config/private_k9s/plugins/executable_loki-logcli +++ b/dot_config/private_k9s/plugins/executable_loki-logcli @@ -1,41 +1,134 @@ #!/bin/sh set -eu +usage() { + echo "usage: loki-logcli [selector args...] [helper flags...] [logcli args...]" >&2 + exit 2 +} + context="${1:-}" -query="${2:-}" +selector="${2:-}" +[ -n "$selector" ] || usage +shift 2 -if [ -z "$query" ]; then - echo "usage: loki-logcli [logcli args...]" >&2 - exit 2 +addr="http://localhost:3100" +format=false +pager=false +table=false +table_scope="query" + +case "$selector" in + --pod) + namespace="${1:-}" + pod="${2:-}" + [ -n "$namespace" ] && [ -n "$pod" ] || usage + shift 2 + query="{namespace=\"$namespace\",pod=\"$pod\"}" + table_scope="pod" + ;; + --app) + namespace="${1:-}" + app="${2:-}" + [ -n "$namespace" ] && [ -n "$app" ] || usage + shift 2 + query="{namespace=\"$namespace\",service_name=\"$app\"}" + table_scope="app" + ;; + *) + query="$selector" + ;; +esac + +while [ "$#" -gt 0 ]; do + case "$1" in + --k9s-format) format=true; shift ;; + --pager) pager=true; shift ;; + --table) table=true; shift ;; + *) break ;; + esac +done + +if [ "$table_scope" = "query" ]; then + case "$query" in + *'container="'*) table_scope="container" ;; + *'pod="'*) table_scope="pod" ;; + *'namespace="'*) table_scope="namespace" ;; + *'node_name="'*) table_scope="node" ;; + esac fi -shift 2 +if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ]; then + runtime_dir="$XDG_RUNTIME_DIR" +elif [ -d /dev/shm ]; then + runtime_dir=/dev/shm +else + runtime_dir=/tmp +fi -state_dir="${XDG_RUNTIME_DIR:-/tmp}/k9s-loki-port-forward" +state_dir="$runtime_dir/k9s-loki-port-forward" +session_dir="$state_dir/sessions" pid_file="$state_dir/pid" -log_file="$state_dir/kubectl.log" -addr="http://127.0.0.1:3100" +janitor_pid_file="$state_dir/janitor.pid" +port_forward_log="$state_dir/kubectl.log" +tail_log="$state_dir/logcli-tail.log" +session_file="$session_dir/$$" -mkdir -p "$state_dir" +mkdir -p "$session_dir" is_alive() { [ -n "${1:-}" ] && kill -0 "$1" 2>/dev/null } +kill_tree() { + pid="${1:-}" + [ -n "$pid" ] || return 0 + + for child in $(pgrep -P "$pid" 2>/dev/null || true); do + kill_tree "$child" + done + + kill "$pid" 2>/dev/null || true +} + is_loki_up() { - nc -z 127.0.0.1 3100 >/dev/null 2>&1 + nc -z localhost 3100 >/dev/null 2>&1 } -k9s_pid() { - pid="$PPID" - while [ -n "$pid" ] && [ "$pid" -gt 1 ] 2>/dev/null; do - comm="$(ps -p "$pid" -o comm= 2>/dev/null || true)" - if [ "$comm" = "k9s" ]; then - printf '%s\n' "$pid" - return 0 - fi - pid="$(ps -p "$pid" -o ppid= 2>/dev/null | tr -d ' ' || true)" - done +k9s_running() { + pgrep -x k9s >/dev/null 2>&1 +} + +register_session() { + : >"$session_file" +} + +release_session() { + rm -f "$session_file" +} + +start_janitor() { + k9s_running || return 0 + + if [ -f "$janitor_pid_file" ]; then + janitor_pid="$(cat "$janitor_pid_file" 2>/dev/null || true)" + is_alive "$janitor_pid" && return 0 + fi + + ( + while :; do + if ! pgrep -x k9s >/dev/null 2>&1; then + if [ -f "$pid_file" ]; then + pid="$(cat "$pid_file" 2>/dev/null || true)" + is_alive "$pid" && kill "$pid" 2>/dev/null || true + rm -f "$pid_file" + fi + rm -f "$janitor_pid_file" + exit 0 + fi + sleep 5 + done + ) >/dev/null 2>&1 & + printf '%s\n' "$!" >"$janitor_pid_file" } start_port_forward() { @@ -46,49 +139,197 @@ start_port_forward() { fi fi - if is_loki_up; then - return 0 - fi + is_loki_up && return 0 - : >"$log_file" + : >"$port_forward_log" if [ -n "$context" ]; then - kubectl --context "$context" port-forward -n loki svc/loki 3100:3100 --address 127.0.0.1 >>"$log_file" 2>&1 & + kubectl --context "$context" port-forward -n loki svc/loki 3100:3100 --address localhost >>"$port_forward_log" 2>&1 & else - kubectl port-forward -n loki svc/loki 3100:3100 --address 127.0.0.1 >>"$log_file" 2>&1 & + kubectl port-forward -n loki svc/loki 3100:3100 --address localhost >>"$port_forward_log" 2>&1 & fi pf_pid="$!" printf '%s\n' "$pf_pid" >"$pid_file" - owner_pid="$(k9s_pid || true)" - if [ -n "$owner_pid" ]; then - ( - while kill -0 "$owner_pid" 2>/dev/null; do - sleep 2 - done - if kill -0 "$pf_pid" 2>/dev/null; then - kill "$pf_pid" 2>/dev/null || true - fi - rm -f "$pid_file" - ) >/dev/null 2>&1 & - fi - i=0 while [ "$i" -lt 50 ]; do - if is_loki_up; then - return 0 - fi + is_loki_up && return 0 if ! is_alive "$pf_pid"; then - cat "$log_file" >&2 + cat "$port_forward_log" >&2 exit 1 fi i=$((i + 1)) sleep 0.1 done - cat "$log_file" >&2 + cat "$port_forward_log" >&2 exit 1 } +logcli_json() { + logcli query "$query" --addr "$addr" -o jsonl --include-common-labels "$@" +} + +format_logs() { + jq --unbuffered -r --arg color "${1:-true}" ' + def c($level): + if $color != "true" then "" + elif ["error", "fatal", "panic"] | index($level) then "\u001b[31m" + elif ["warn", "warning"] | index($level) then "\u001b[33m" + elif $level == "info" then "\u001b[36m" + elif ["debug", "trace"] | index($level) then "\u001b[90m" + else "\u001b[35m" + end; + def reset: if $color == "true" then "\u001b[0m" else "" end; + def app: (.line | fromjson?); + def message($app; $line): + if $app.message then + [ + $app.message, + $app.method, + $app.path, + (if $app.status != null then ($app.status | tostring) else empty end), + (if $app.duration_ms then (($app.duration_ms | tostring) + "ms") else empty end) + ] | map(select(. != null and . != "")) | join(" ") + else + $line + end; + . as $entry + | (app // {}) as $app + | (($app.level // .labels.level // .labels.detected_level // "unknown") | ascii_downcase) as $level + | (.timestamp[11:19] // "--:--:--") as $time + | (.labels.namespace // "-") as $namespace + | (.labels.pod // "-") as $pod + | (.labels.container // .labels.service_name // "-") as $container + | message($app; $entry.line) as $message + | "\($time) \(c($level))\($level | ascii_upcase)\(reset) \($namespace)/\($pod) \($container) \($message)" + ' +} + +format_table() { + jq --unbuffered -r --arg scope "$table_scope" ' + def app: (.line | fromjson?); + def clean: tostring | gsub("[|\t\r\n]"; " "); + def message($app; $line): + if $app.message then + [ + $app.message, + $app.method, + $app.path, + (if $app.status != null then ($app.status | tostring) else empty end), + (if $app.duration_ms then (($app.duration_ms | tostring) + "ms") else empty end) + ] | map(select(. != null and . != "")) | join(" ") + else + $line + end; + . as $entry + | (app // {}) as $app + | (($app.level // .labels.level // .labels.detected_level // "unknown") | ascii_upcase) as $level + | (.timestamp[11:19] // "--:--:--") as $time + | (.labels.namespace // "-") as $namespace + | (.labels.pod // "-") as $pod + | (.labels.container // .labels.service_name // "-") as $container + | message($app; $entry.line) as $message + | if $scope == "container" then + [$time, $level, $message] + elif $scope == "pod" then + [$time, $level, $container, $message] + elif $scope == "app" or $scope == "namespace" then + [$time, $level, $pod, $container, $message] + else + [$time, $level, $namespace, $pod, $container, $message] + end + | map(clean) + | join(" | ") + ' +} + +table_header() { + case "$table_scope" in + container) printf 'time | level | message\n' ;; + pod) printf 'time | level | container | message\n' ;; + app|namespace) printf 'time | level | pod | container | message\n' ;; + *) printf 'time | level | namespace | pod | container | message\n' ;; + esac +} + +preload_file() { + file="$1" + if [ "$table" = true ]; then + table_header >"$file" + logcli_json --quiet --limit 500 --since=24h | format_table >>"$file" + else + logcli_json --quiet --limit 500 --since=24h | format_logs false >"$file" + fi +} + +start_live_writer() { + file="$1" + shift + : >"$tail_log" + + if [ "$table" = true ]; then + (sleep 0.2; logcli_json "$@" 2>>"$tail_log" | format_table >>"$file") 2>>"$tail_log" & + else + (sleep 0.2; logcli_json "$@" 2>>"$tail_log" | format_logs false >>"$file") 2>>"$tail_log" & + fi + producer_pid="$!" +} + +run_ov() { + file="$1" + + if [ "$table" = true ]; then + ov --follow-mode --align --column-mode --column-rainbow --column-delimiter '|' --header 1 --multi-color 'ERROR,FATAL,PANIC,WARN,WARNING,INFO,DEBUG,TRACE,UNKNOWN' "$file" + else + ov --follow-mode --multi-color 'ERROR,FATAL,PANIC,WARN,WARNING,INFO,DEBUG,TRACE,UNKNOWN' "$file" + fi +} + +run_pager() { + if command -v ov >/dev/null 2>&1; then + tmp_file="$state_dir/logs.$$" + producer_pid="" + + cleanup_pager() { + if [ -n "${producer_pid:-}" ]; then + kill_tree "$producer_pid" + wait "$producer_pid" 2>/dev/null || true + fi + rm -f "$tmp_file" + } + + rm -f "$tmp_file" + trap 'cleanup_pager; release_session' EXIT INT TERM + preload_file "$tmp_file" + start_live_writer "$tmp_file" "$@" + run_ov "$tmp_file" + cleanup_pager + elif command -v less >/dev/null 2>&1; then + logcli_json "$@" | format_logs true | less -R +F + else + logcli_json "$@" | format_logs false | more + fi +} + +run_formatted() { + if [ "$pager" = true ]; then + run_pager "$@" + elif [ "$table" = true ]; then + table_header + logcli_json "$@" | format_table + else + logcli_json "$@" | format_logs true + fi +} + +register_session +trap release_session EXIT INT TERM start_port_forward -exec logcli query "$query" --addr "$addr" "$@" +start_janitor + +if [ "$format" = true ]; then + run_formatted "$@" +else + exec logcli query "$query" --addr "$addr" "$@" +fi diff --git a/dot_config/private_k9s/plugins/log-loki.yaml b/dot_config/private_k9s/plugins/log-loki.yaml index 7224756..bad2f1e 100644 --- a/dot_config/private_k9s/plugins/log-loki.yaml +++ b/dot_config/private_k9s/plugins/log-loki.yaml @@ -13,7 +13,11 @@ plugins: - -- - $CONTEXT - '{namespace="$NAMESPACE",pod="$POD",container="$NAME"}' + - --k9s-format + - --pager - --quiet + - --limit + - "500" - -f loki-container-raw: shortCut: Ctrl-E @@ -29,9 +33,31 @@ plugins: - $CONTEXT - '{namespace="$NAMESPACE",pod="$POD",container="$NAME"}' - --quiet + - --limit + - "500" - -f - -o - raw + loki-container-table: + shortCut: k + description: "loki table" + scopes: + - containers + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - '{namespace="$NAMESPACE",pod="$POD",container="$NAME"}' + - --k9s-format + - --pager + - --table + - --quiet + - --limit + - "500" + - -f loki-pods: shortCut: Shift-L description: "loki fmt" @@ -44,8 +70,14 @@ plugins: - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" - -- - $CONTEXT - - '{namespace="$NAMESPACE",pod="$NAME"}' + - --pod + - $NAMESPACE + - $NAME + - --k9s-format + - --pager - --quiet + - --limit + - "500" - -f loki-pods-raw: shortCut: Ctrl-L @@ -59,11 +91,101 @@ plugins: - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" - -- - $CONTEXT - - '{namespace="$NAMESPACE",pod="$NAME"}' + - --pod + - $NAMESPACE + - $NAME - --quiet + - --limit + - "500" - -f - -o - raw + loki-pods-table: + shortCut: k + description: "loki table" + scopes: + - po + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - --pod + - $NAMESPACE + - $NAME + - --k9s-format + - --pager + - --table + - --quiet + - --limit + - "500" + - -f + loki-deployments: + shortCut: Shift-L + description: "loki fmt" + scopes: + - deployments + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - --app + - $NAMESPACE + - $NAME + - --k9s-format + - --pager + - --quiet + - --limit + - "500" + - -f + loki-deployments-raw: + shortCut: Ctrl-L + description: "loki raw" + scopes: + - deployments + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - --app + - $NAMESPACE + - $NAME + - --quiet + - --limit + - "500" + - -f + - -o + - raw + loki-deployments-table: + shortCut: k + description: "loki table" + scopes: + - deployments + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - --app + - $NAMESPACE + - $NAME + - --k9s-format + - --pager + - --table + - --quiet + - --limit + - "500" + - -f loki-node: shortCut: Shift-L description: "loki fmt" @@ -77,7 +199,11 @@ plugins: - -- - $CONTEXT - '{node_name="$NAME"}' + - --k9s-format + - --pager - --quiet + - --limit + - "500" - -f loki-node-raw: shortCut: Ctrl-L @@ -93,9 +219,31 @@ plugins: - $CONTEXT - '{node_name="$NAME"}' - --quiet + - --limit + - "500" - -f - -o - raw + loki-node-table: + shortCut: k + description: "loki table" + scopes: + - node + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - '{node_name="$NAME"}' + - --k9s-format + - --pager + - --table + - --quiet + - --limit + - "500" + - -f loki-ns: shortCut: Shift-L description: "loki fmt" @@ -109,7 +257,11 @@ plugins: - -- - $CONTEXT - '{namespace="$NAME"}' + - --k9s-format + - --pager - --quiet + - --limit + - "500" - -f loki-ns-raw: shortCut: Ctrl-L @@ -125,6 +277,28 @@ plugins: - $CONTEXT - '{namespace="$NAME"}' - --quiet + - --limit + - "500" - -f - -o - raw + loki-ns-table: + shortCut: k + description: "loki table" + scopes: + - namespace + command: sh + background: false + args: + - -c + - exec "${XDG_CONFIG_HOME:-$HOME/.config}/k9s/plugins/loki-logcli" "$@" + - -- + - $CONTEXT + - '{namespace="$NAME"}' + - --k9s-format + - --pager + - --table + - --quiet + - --limit + - "500" + - -f diff --git a/dot_config/private_k9s/plugins/start-alpine.yaml b/dot_config/private_k9s/plugins/start-alpine.yaml index 59efe68..cbcc43c 100644 --- a/dot_config/private_k9s/plugins/start-alpine.yaml +++ b/dot_config/private_k9s/plugins/start-alpine.yaml @@ -2,7 +2,7 @@ plugins: start-alpine-pod: shortCut: Ctrl-T confirm: true - description: "Start an alpine:latest pod in current context/namespace" + description: "Start alpine:latest" scopes: - pods - deployments