| 1 | #!/usr/bin/env bash |
| 2 | # analyze-events.sh -- group-by stats over a dump from get-events.sh. |
| 3 | # |
| 4 | # Reads either the response envelope (if a single payload) or |
| 5 | # raw rows (if pre-extracted). Emits a top-N counter table on |
| 6 | # the requested dimension. |
| 7 | |
| 8 | set -euo pipefail |
| 9 | |
| 10 | usage() { |
| 11 | cat <<'EOF' |
| 12 | analyze-events.sh --by <dim> [options] |
| 13 | |
| 14 | Required: |
| 15 | --by <dim> dimension to group by; one of: |
| 16 | signal, fatal_function, fatal_filename, version, |
| 17 | architecture, os_family, os_type, install_type, |
| 18 | db_mode, kubernetes, profile, aclk, health, |
| 19 | exit_cause, virtualization, chassis_type, host_cpus |
| 20 | |
| 21 | Options: |
| 22 | --input PATH path to the JSON dump (default: latest under |
| 23 | <repo>/.local/audits/query-agent-events/) |
| 24 | --top N top N values (default 20) |
| 25 | --filter "K=V" extra client-side filter (repeatable); |
| 26 | e.g. --filter "AE_OS_FAMILY=ubuntu" |
| 27 | --format text|json (default text) |
| 28 | -h, --help |
| 29 | |
| 30 | Tip: --by signal groups by AE_FATAL_SIGNAL_CODE (signal crashes). |
| 31 | For non-signal events, the value will be empty. |
| 32 | EOF |
| 33 | } |
| 34 | |
| 35 | # Map --by alias -> AE_* field name. |
| 36 | field_for_dim() { |
| 37 | local dim="$1" |
| 38 | case "$dim" in |
| 39 | signal) echo AE_FATAL_SIGNAL_CODE ;; |
| 40 | fatal_function) echo AE_FATAL_FUNCTION ;; |
| 41 | fatal_filename) echo AE_FATAL_FILENAME ;; |
| 42 | version) echo AE_AGENT_VERSION ;; |
| 43 | architecture) echo AE_HOST_ARCHITECTURE ;; |
| 44 | os_family) echo AE_OS_FAMILY ;; |
| 45 | os_type) echo AE_OS_TYPE ;; |
| 46 | install_type) echo AE_AGENT_INSTALL_TYPE ;; |
| 47 | db_mode) echo AE_AGENT_DB_MODE ;; |
| 48 | kubernetes) echo AE_AGENT_KUBERNETES ;; |
| 49 | profile) echo AE_AGENT_PROFILE_0 ;; |
| 50 | aclk) echo AE_AGENT_ACLK ;; |
| 51 | health) echo AE_AGENT_HEALTH ;; |
| 52 | exit_cause) echo AE_EXIT_CAUSE ;; |
| 53 | virtualization) echo AE_HOST_VIRTUALIZATION ;; |
| 54 | chassis_type) echo AE_HW_CHASSIS_TYPE ;; |
| 55 | host_cpus) echo AE_HOST_SYSTEM_CPUS ;; |
| 56 | *) echo "Unknown --by '$dim'" >&2; exit 2 ;; |
| 57 | esac |
| 58 | } |
| 59 | |
| 60 | BY= |
| 61 | INPUT= |
| 62 | TOP=20 |
| 63 | FORMAT=text |
| 64 | declare -a FILTERS=() |
| 65 | |
| 66 | while [ $# -gt 0 ]; do |
| 67 | case "$1" in |
| 68 | --by) BY="$2"; shift 2 ;; |
| 69 | --input) INPUT="$2"; shift 2 ;; |
| 70 | --top) TOP="$2"; shift 2 ;; |
| 71 | --filter) FILTERS+=("$2"); shift 2 ;; |
| 72 | --format) FORMAT="$2"; shift 2 ;; |
| 73 | -h|--help) usage; exit 0 ;; |
| 74 | *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; |
| 75 | esac |
| 76 | done |
| 77 | |
| 78 | [ -z "$BY" ] && { usage >&2; exit 2; } |
| 79 | |
| 80 | FIELD="$(field_for_dim "$BY")" |
| 81 | |
| 82 | # shellcheck source=SCRIPTDIR/_lib.sh disable=SC1091 |
| 83 | source "$(cd "$(dirname "$0")" && pwd)/_lib.sh" |
| 84 | |
| 85 | # Pick the most recent dump if none given. |
| 86 | if [ -z "$INPUT" ]; then |
| 87 | audit_dir="$(agentevents_audit_dir)" |
| 88 | # shellcheck disable=SC2012 # ls -1t is fine for *.json under audit_dir; find pipeline is overkill |
| 89 | INPUT="$(ls -1t "$audit_dir"/*.json 2>/dev/null | head -1 || true)" |
| 90 | [ -z "$INPUT" ] && { |
| 91 | echo "No input dump found under $audit_dir; pass --input PATH" >&2 |
| 92 | exit 2 |
| 93 | } |
| 94 | echo "[analyze-events] using $INPUT" >&2 |
| 95 | fi |
| 96 | |
| 97 | # The Function envelope has top-level `data` (rows of arrays) |
| 98 | # and `columns` (name -> {index, ...}). |
| 99 | # |
| 100 | # Two paths: |
| 101 | # - if the file looks like a Function envelope, project rows |
| 102 | # via the columns map to extract FIELD; |
| 103 | # - if it's a flat array of objects (pre-extracted), use jq |
| 104 | # directly. |
| 105 | |
| 106 | # Build filter expression. |
| 107 | filter_expr='true' |
| 108 | for f in "${FILTERS[@]}"; do |
| 109 | k="${f%%=*}" |
| 110 | v="${f#*=}" |
| 111 | filter_expr="$filter_expr and (.\"$k\" == \"$v\")" |
| 112 | done |
| 113 | |
| 114 | # Detect format and project. |
| 115 | records=$(jq -c --arg field "$FIELD" ' |
| 116 | if (type == "object" and has("columns") and has("data")) then |
| 117 | .columns as $c |
| 118 | | ($c | to_entries |
| 119 | | map({(.key): (.value.index)}) |
| 120 | | add) as $idx |
| 121 | | (.data // []) |
| 122 | | map( |
| 123 | . as $row |
| 124 | | reduce ($idx | keys_unsorted)[] as $k |
| 125 | ({}; .[$k] = $row[$idx[$k]]) |
| 126 | ) |
| 127 | elif (type == "array" and (.[0]? | type == "object")) then |
| 128 | . |
| 129 | else |
| 130 | [] |
| 131 | end |
| 132 | ' "$INPUT") |
| 133 | |
| 134 | # Apply filters and group. |
| 135 | result=$(printf '%s' "$records" | jq -c --arg field "$FIELD" --argjson top "$TOP" " |
| 136 | map(select($filter_expr)) |
| 137 | | group_by(.[\$field] // \"\") |
| 138 | | map({key: (.[0][\$field] // \"(empty)\"), count: length}) |
| 139 | | sort_by(-.count) |
| 140 | | .[:\$top] |
| 141 | ") |
| 142 | |
| 143 | case "$FORMAT" in |
| 144 | json) |
| 145 | echo "$result" | jq . |
| 146 | ;; |
| 147 | text|*) |
| 148 | echo |
| 149 | echo "Top $TOP by $BY ($FIELD):" |
| 150 | printf '%s\n' "----------------------------------------" |
| 151 | echo "$result" | jq -r '.[] | [.count, .key] | @tsv' \ |
| 152 | | awk -F'\t' '{ printf "%8d %s\n", $1, $2 }' |
| 153 | echo "----------------------------------------" |
| 154 | total=$(echo "$result" | jq '[.[].count] | add // 0') |
| 155 | echo " Total in top: $total" |
| 156 | ;; |
| 157 | esac |