master
sh 157 lines 4.81 KB
Raw
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