specweave 1.0.28 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +39 -0
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/commands/check-hooks.md +43 -0
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
- package/plugins/specweave/hooks/lib/logging.sh +231 -0
- package/plugins/specweave/hooks/lib/metrics.sh +347 -0
- package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
- package/plugins/specweave/scripts/hook-health.sh +441 -0
- package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
- /package/plugins/specweave/hooks/{hooks.json.bak → hooks.json} +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# logging.sh - Structured logging for SpecWeave hooks
|
|
3
|
+
#
|
|
4
|
+
# FEATURES:
|
|
5
|
+
# - Structured JSON logs for machine parsing
|
|
6
|
+
# - Human-readable console output
|
|
7
|
+
# - Request ID tracing across hook chains
|
|
8
|
+
# - Log levels (DEBUG, INFO, WARN, ERROR)
|
|
9
|
+
# - Automatic log rotation
|
|
10
|
+
# - Performance timing
|
|
11
|
+
#
|
|
12
|
+
# USAGE:
|
|
13
|
+
# source logging.sh
|
|
14
|
+
# log_init "hook-name"
|
|
15
|
+
# log_info "Processing request"
|
|
16
|
+
# log_error "Something failed" "error_code=123"
|
|
17
|
+
# log_timing_start "operation"
|
|
18
|
+
# # ... do work ...
|
|
19
|
+
# log_timing_end "operation"
|
|
20
|
+
#
|
|
21
|
+
# v1.0.0 - Initial implementation (2025-12-17)
|
|
22
|
+
|
|
23
|
+
# === Configuration ===
|
|
24
|
+
LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
|
|
25
|
+
LOG_LEVEL="${LOG_LEVEL:-INFO}" # DEBUG, INFO, WARN, ERROR
|
|
26
|
+
LOG_FORMAT="${LOG_FORMAT:-json}" # json, text
|
|
27
|
+
LOG_MAX_SIZE_KB="${LOG_MAX_SIZE_KB:-1024}" # 1MB per file
|
|
28
|
+
LOG_ROTATION_COUNT="${LOG_ROTATION_COUNT:-5}"
|
|
29
|
+
|
|
30
|
+
# Log level numeric values
|
|
31
|
+
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
|
|
32
|
+
|
|
33
|
+
# Request context
|
|
34
|
+
_LOG_REQUEST_ID=""
|
|
35
|
+
_LOG_HOOK_NAME=""
|
|
36
|
+
_LOG_START_TIME=""
|
|
37
|
+
declare -A _LOG_TIMINGS
|
|
38
|
+
|
|
39
|
+
# === Initialization ===
|
|
40
|
+
log_init() {
|
|
41
|
+
local hook_name="$1"
|
|
42
|
+
|
|
43
|
+
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
44
|
+
|
|
45
|
+
_LOG_HOOK_NAME="$hook_name"
|
|
46
|
+
_LOG_REQUEST_ID="${REQUEST_ID:-$(generate_request_id)}"
|
|
47
|
+
_LOG_START_TIME=$(get_timestamp_ms)
|
|
48
|
+
|
|
49
|
+
export REQUEST_ID="$_LOG_REQUEST_ID"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# === Request ID Generation ===
|
|
53
|
+
generate_request_id() {
|
|
54
|
+
# Format: HHMMSS-RANDOM (compact but unique enough for tracing)
|
|
55
|
+
local time_part
|
|
56
|
+
time_part=$(date +%H%M%S)
|
|
57
|
+
local random_part
|
|
58
|
+
random_part=$(printf '%04x' $((RANDOM % 65536)))
|
|
59
|
+
echo "${time_part}-${random_part}"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# === Timestamp ===
|
|
63
|
+
get_timestamp_ms() {
|
|
64
|
+
if command -v gdate &>/dev/null; then
|
|
65
|
+
gdate +%s%3N
|
|
66
|
+
elif date +%s%N &>/dev/null 2>&1; then
|
|
67
|
+
echo $(($(date +%s%N) / 1000000))
|
|
68
|
+
else
|
|
69
|
+
echo "$(($(date +%s) * 1000))"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get_timestamp_iso() {
|
|
74
|
+
date -u +"%Y-%m-%dT%H:%M:%S.000Z" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# === Level Check ===
|
|
78
|
+
_should_log() {
|
|
79
|
+
local level="$1"
|
|
80
|
+
local level_num="${LOG_LEVELS[$level]:-1}"
|
|
81
|
+
local threshold_num="${LOG_LEVELS[$LOG_LEVEL]:-1}"
|
|
82
|
+
[[ "$level_num" -ge "$threshold_num" ]]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# === Log Rotation ===
|
|
86
|
+
_rotate_log_if_needed() {
|
|
87
|
+
local log_file="$1"
|
|
88
|
+
|
|
89
|
+
[[ ! -f "$log_file" ]] && return
|
|
90
|
+
|
|
91
|
+
local size_kb
|
|
92
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
93
|
+
size_kb=$(($(stat -f %z "$log_file" 2>/dev/null || echo 0) / 1024))
|
|
94
|
+
else
|
|
95
|
+
size_kb=$(($(stat -c %s "$log_file" 2>/dev/null || echo 0) / 1024))
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [[ "$size_kb" -gt "$LOG_MAX_SIZE_KB" ]]; then
|
|
99
|
+
# Rotate logs
|
|
100
|
+
for i in $(seq $((LOG_ROTATION_COUNT - 1)) -1 1); do
|
|
101
|
+
[[ -f "${log_file}.$i" ]] && mv "${log_file}.$i" "${log_file}.$((i + 1))" 2>/dev/null
|
|
102
|
+
done
|
|
103
|
+
mv "$log_file" "${log_file}.1" 2>/dev/null || true
|
|
104
|
+
fi
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# === Core Logging Function ===
|
|
108
|
+
_log() {
|
|
109
|
+
local level="$1"
|
|
110
|
+
local message="$2"
|
|
111
|
+
shift 2
|
|
112
|
+
local extra_fields=("$@")
|
|
113
|
+
|
|
114
|
+
_should_log "$level" || return 0
|
|
115
|
+
|
|
116
|
+
local timestamp
|
|
117
|
+
timestamp=$(get_timestamp_iso)
|
|
118
|
+
|
|
119
|
+
local log_file="$LOG_DIR/${_LOG_HOOK_NAME:-hooks}.log"
|
|
120
|
+
_rotate_log_if_needed "$log_file"
|
|
121
|
+
|
|
122
|
+
if [[ "$LOG_FORMAT" == "json" ]]; then
|
|
123
|
+
# Build JSON log entry
|
|
124
|
+
local json="{\"ts\":\"$timestamp\",\"level\":\"$level\",\"hook\":\"${_LOG_HOOK_NAME:-unknown}\",\"rid\":\"${_LOG_REQUEST_ID:-none}\",\"msg\":\"$message\""
|
|
125
|
+
|
|
126
|
+
# Add extra fields
|
|
127
|
+
for field in "${extra_fields[@]}"; do
|
|
128
|
+
local key="${field%%=*}"
|
|
129
|
+
local value="${field#*=}"
|
|
130
|
+
# Escape quotes in value
|
|
131
|
+
value="${value//\"/\\\"}"
|
|
132
|
+
json="$json,\"$key\":\"$value\""
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
json="$json}"
|
|
136
|
+
echo "$json" >> "$log_file" 2>/dev/null || true
|
|
137
|
+
else
|
|
138
|
+
# Text format
|
|
139
|
+
local prefix="[$timestamp] [$level] [${_LOG_HOOK_NAME:-unknown}] [${_LOG_REQUEST_ID:-none}]"
|
|
140
|
+
echo "$prefix $message ${extra_fields[*]}" >> "$log_file" 2>/dev/null || true
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Also output to stderr if DEBUG level and debug mode enabled
|
|
144
|
+
if [[ "$level" == "DEBUG" ]] && [[ "${HOOK_DEBUG:-0}" == "1" ]]; then
|
|
145
|
+
echo "[HOOK-$level] $_LOG_HOOK_NAME: $message" >&2
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# === Public Logging Functions ===
|
|
150
|
+
log_debug() {
|
|
151
|
+
_log "DEBUG" "$1" "${@:2}"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log_info() {
|
|
155
|
+
_log "INFO" "$1" "${@:2}"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log_warn() {
|
|
159
|
+
_log "WARN" "$1" "${@:2}"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
log_error() {
|
|
163
|
+
_log "ERROR" "$1" "${@:2}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# === Performance Timing ===
|
|
167
|
+
log_timing_start() {
|
|
168
|
+
local operation="$1"
|
|
169
|
+
_LOG_TIMINGS[$operation]=$(get_timestamp_ms)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log_timing_end() {
|
|
173
|
+
local operation="$1"
|
|
174
|
+
local start_time="${_LOG_TIMINGS[$operation]:-0}"
|
|
175
|
+
|
|
176
|
+
if [[ "$start_time" -gt 0 ]]; then
|
|
177
|
+
local end_time
|
|
178
|
+
end_time=$(get_timestamp_ms)
|
|
179
|
+
local duration_ms=$((end_time - start_time))
|
|
180
|
+
|
|
181
|
+
log_debug "Timing: $operation completed" "duration_ms=$duration_ms"
|
|
182
|
+
unset "_LOG_TIMINGS[$operation]"
|
|
183
|
+
|
|
184
|
+
echo "$duration_ms"
|
|
185
|
+
else
|
|
186
|
+
echo "0"
|
|
187
|
+
fi
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# === Request Summary ===
|
|
191
|
+
log_request_summary() {
|
|
192
|
+
local status="$1"
|
|
193
|
+
local details="$2"
|
|
194
|
+
|
|
195
|
+
local total_duration=0
|
|
196
|
+
if [[ -n "$_LOG_START_TIME" ]]; then
|
|
197
|
+
local end_time
|
|
198
|
+
end_time=$(get_timestamp_ms)
|
|
199
|
+
total_duration=$((end_time - _LOG_START_TIME))
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
log_info "Request completed" "status=$status" "total_ms=$total_duration" "details=$details"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# === Aggregate Log Stats ===
|
|
206
|
+
get_log_stats() {
|
|
207
|
+
local hook_name="${1:-$_LOG_HOOK_NAME}"
|
|
208
|
+
local log_file="$LOG_DIR/${hook_name}.log"
|
|
209
|
+
|
|
210
|
+
[[ ! -f "$log_file" ]] && echo '{"total":0,"errors":0,"warns":0}' && return
|
|
211
|
+
|
|
212
|
+
local total errors warns
|
|
213
|
+
total=$(wc -l < "$log_file" 2>/dev/null | tr -d ' ')
|
|
214
|
+
errors=$(grep -c '"level":"ERROR"' "$log_file" 2>/dev/null || echo 0)
|
|
215
|
+
warns=$(grep -c '"level":"WARN"' "$log_file" 2>/dev/null || echo 0)
|
|
216
|
+
|
|
217
|
+
echo "{\"total\":$total,\"errors\":$errors,\"warns\":$warns}"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# === Correlation ID Propagation ===
|
|
221
|
+
# Use this to pass request ID to child processes
|
|
222
|
+
export_request_context() {
|
|
223
|
+
export REQUEST_ID="$_LOG_REQUEST_ID"
|
|
224
|
+
export HOOK_NAME="$_LOG_HOOK_NAME"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Import context from parent
|
|
228
|
+
import_request_context() {
|
|
229
|
+
_LOG_REQUEST_ID="${REQUEST_ID:-$(generate_request_id)}"
|
|
230
|
+
_LOG_HOOK_NAME="${HOOK_NAME:-unknown}"
|
|
231
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# metrics.sh - Hook Metrics Collection and Health Monitoring
|
|
3
|
+
#
|
|
4
|
+
# FEATURES:
|
|
5
|
+
# - Execution time tracking (p50, p95, p99)
|
|
6
|
+
# - Success/failure rate tracking
|
|
7
|
+
# - Request throughput (requests/second)
|
|
8
|
+
# - Concurrent execution tracking
|
|
9
|
+
# - Health score calculation
|
|
10
|
+
# - Metrics aggregation and rollup
|
|
11
|
+
#
|
|
12
|
+
# STORAGE:
|
|
13
|
+
# - Ring buffer for recent metrics (last 1000 entries)
|
|
14
|
+
# - Hourly rollups for historical data
|
|
15
|
+
# - Compact binary-like format for efficiency
|
|
16
|
+
#
|
|
17
|
+
# USAGE:
|
|
18
|
+
# source metrics.sh
|
|
19
|
+
# metrics_init "hook-name"
|
|
20
|
+
# metrics_start_request
|
|
21
|
+
# # ... do work ...
|
|
22
|
+
# metrics_end_request "success" # or "failure", "timeout", "skipped"
|
|
23
|
+
#
|
|
24
|
+
# v1.0.0 - Initial implementation (2025-12-17)
|
|
25
|
+
|
|
26
|
+
set -o pipefail
|
|
27
|
+
|
|
28
|
+
# === Configuration ===
|
|
29
|
+
METRICS_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/metrics"
|
|
30
|
+
METRICS_BUFFER_SIZE="${METRICS_BUFFER_SIZE:-1000}" # Ring buffer entries
|
|
31
|
+
METRICS_DEBUG="${METRICS_DEBUG:-0}"
|
|
32
|
+
|
|
33
|
+
# Current request context
|
|
34
|
+
_METRICS_HOOK_NAME=""
|
|
35
|
+
_METRICS_START_TIME=""
|
|
36
|
+
|
|
37
|
+
# === Initialization ===
|
|
38
|
+
metrics_init() {
|
|
39
|
+
local hook_name="$1"
|
|
40
|
+
_METRICS_HOOK_NAME="$hook_name"
|
|
41
|
+
mkdir -p "$METRICS_DIR" 2>/dev/null || true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# === Timestamp ===
|
|
45
|
+
_metrics_now_ms() {
|
|
46
|
+
if command -v gdate &>/dev/null; then
|
|
47
|
+
gdate +%s%3N
|
|
48
|
+
elif date +%s%N &>/dev/null 2>&1; then
|
|
49
|
+
echo $(($(date +%s%N) / 1000000))
|
|
50
|
+
else
|
|
51
|
+
echo "$(($(date +%s) * 1000))"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_metrics_now_sec() {
|
|
56
|
+
date +%s
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# === File Paths ===
|
|
60
|
+
_metrics_buffer_file() {
|
|
61
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
62
|
+
echo "$METRICS_DIR/${hook_name}.buffer"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_metrics_stats_file() {
|
|
66
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
67
|
+
echo "$METRICS_DIR/${hook_name}.stats"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_metrics_hourly_file() {
|
|
71
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
72
|
+
local hour
|
|
73
|
+
hour=$(date +%Y%m%d%H)
|
|
74
|
+
echo "$METRICS_DIR/${hook_name}.hourly.${hour}"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# === Request Tracking ===
|
|
78
|
+
metrics_start_request() {
|
|
79
|
+
_METRICS_START_TIME=$(_metrics_now_ms)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
metrics_end_request() {
|
|
83
|
+
local status="${1:-success}" # success, failure, timeout, skipped
|
|
84
|
+
|
|
85
|
+
[[ -z "$_METRICS_START_TIME" ]] && return
|
|
86
|
+
|
|
87
|
+
local end_time
|
|
88
|
+
end_time=$(_metrics_now_ms)
|
|
89
|
+
local duration_ms=$((_METRICS_START_TIME > 0 ? end_time - _METRICS_START_TIME : 0))
|
|
90
|
+
|
|
91
|
+
# Record to buffer
|
|
92
|
+
_metrics_record_entry "$duration_ms" "$status"
|
|
93
|
+
|
|
94
|
+
# Update running stats
|
|
95
|
+
_metrics_update_stats "$duration_ms" "$status"
|
|
96
|
+
|
|
97
|
+
_METRICS_START_TIME=""
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# === Buffer Management (Ring Buffer) ===
|
|
101
|
+
_metrics_record_entry() {
|
|
102
|
+
local duration_ms="$1"
|
|
103
|
+
local status="$2"
|
|
104
|
+
local timestamp
|
|
105
|
+
timestamp=$(_metrics_now_sec)
|
|
106
|
+
|
|
107
|
+
local buffer_file
|
|
108
|
+
buffer_file=$(_metrics_buffer_file)
|
|
109
|
+
|
|
110
|
+
# Compact format: timestamp,duration_ms,status_code
|
|
111
|
+
# Status codes: 0=success, 1=failure, 2=timeout, 3=skipped
|
|
112
|
+
local status_code=0
|
|
113
|
+
case "$status" in
|
|
114
|
+
success) status_code=0 ;;
|
|
115
|
+
failure) status_code=1 ;;
|
|
116
|
+
timeout) status_code=2 ;;
|
|
117
|
+
skipped) status_code=3 ;;
|
|
118
|
+
esac
|
|
119
|
+
|
|
120
|
+
echo "${timestamp},${duration_ms},${status_code}" >> "$buffer_file" 2>/dev/null || true
|
|
121
|
+
|
|
122
|
+
# Trim buffer if too large
|
|
123
|
+
if [[ -f "$buffer_file" ]]; then
|
|
124
|
+
local line_count
|
|
125
|
+
line_count=$(wc -l < "$buffer_file" 2>/dev/null | tr -d ' ')
|
|
126
|
+
if [[ "$line_count" -gt "$METRICS_BUFFER_SIZE" ]]; then
|
|
127
|
+
tail -$((METRICS_BUFFER_SIZE / 2)) "$buffer_file" > "${buffer_file}.tmp" 2>/dev/null && \
|
|
128
|
+
mv "${buffer_file}.tmp" "$buffer_file" 2>/dev/null || true
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# === Running Statistics ===
|
|
134
|
+
_metrics_update_stats() {
|
|
135
|
+
local duration_ms="$1"
|
|
136
|
+
local status="$2"
|
|
137
|
+
|
|
138
|
+
local stats_file
|
|
139
|
+
stats_file=$(_metrics_stats_file)
|
|
140
|
+
|
|
141
|
+
# Read current stats
|
|
142
|
+
local total=0 success=0 failure=0 timeout=0 skipped=0
|
|
143
|
+
local sum_duration=0 min_duration=999999 max_duration=0
|
|
144
|
+
|
|
145
|
+
if [[ -f "$stats_file" ]]; then
|
|
146
|
+
# Format: total,success,failure,timeout,skipped,sum_duration,min,max
|
|
147
|
+
IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# Update counters
|
|
151
|
+
total=$((total + 1))
|
|
152
|
+
case "$status" in
|
|
153
|
+
success) success=$((success + 1)) ;;
|
|
154
|
+
failure) failure=$((failure + 1)) ;;
|
|
155
|
+
timeout) timeout=$((timeout + 1)) ;;
|
|
156
|
+
skipped) skipped=$((skipped + 1)) ;;
|
|
157
|
+
esac
|
|
158
|
+
|
|
159
|
+
# Update duration stats (only for non-skipped)
|
|
160
|
+
if [[ "$status" != "skipped" ]]; then
|
|
161
|
+
sum_duration=$((sum_duration + duration_ms))
|
|
162
|
+
[[ "$duration_ms" -lt "$min_duration" ]] && min_duration=$duration_ms
|
|
163
|
+
[[ "$duration_ms" -gt "$max_duration" ]] && max_duration=$duration_ms
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Write updated stats
|
|
167
|
+
echo "${total},${success},${failure},${timeout},${skipped},${sum_duration},${min_duration},${max_duration}" > "$stats_file" 2>/dev/null || true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# === Percentile Calculation ===
|
|
171
|
+
_metrics_calculate_percentile() {
|
|
172
|
+
local hook_name="$1"
|
|
173
|
+
local percentile="$2" # 50, 95, 99
|
|
174
|
+
|
|
175
|
+
local buffer_file
|
|
176
|
+
buffer_file=$(_metrics_buffer_file "$hook_name")
|
|
177
|
+
|
|
178
|
+
[[ ! -f "$buffer_file" ]] && echo "0" && return
|
|
179
|
+
|
|
180
|
+
# Extract durations (second field) and sort
|
|
181
|
+
local sorted_durations
|
|
182
|
+
sorted_durations=$(cut -d',' -f2 "$buffer_file" 2>/dev/null | sort -n)
|
|
183
|
+
|
|
184
|
+
local count
|
|
185
|
+
count=$(echo "$sorted_durations" | wc -l | tr -d ' ')
|
|
186
|
+
|
|
187
|
+
[[ "$count" -eq 0 ]] && echo "0" && return
|
|
188
|
+
|
|
189
|
+
# Calculate index for percentile
|
|
190
|
+
local index=$(( (count * percentile) / 100 ))
|
|
191
|
+
[[ "$index" -lt 1 ]] && index=1
|
|
192
|
+
|
|
193
|
+
# Get value at index
|
|
194
|
+
echo "$sorted_durations" | sed -n "${index}p"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# === Public API ===
|
|
198
|
+
|
|
199
|
+
# Get current metrics for a hook
|
|
200
|
+
metrics_get() {
|
|
201
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
202
|
+
local stats_file
|
|
203
|
+
stats_file=$(_metrics_stats_file "$hook_name")
|
|
204
|
+
|
|
205
|
+
local total=0 success=0 failure=0 timeout=0 skipped=0
|
|
206
|
+
local sum_duration=0 min_duration=0 max_duration=0
|
|
207
|
+
|
|
208
|
+
if [[ -f "$stats_file" ]]; then
|
|
209
|
+
IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Calculate rates
|
|
213
|
+
local success_rate=100
|
|
214
|
+
local executed=$((total - skipped))
|
|
215
|
+
if [[ "$executed" -gt 0 ]]; then
|
|
216
|
+
success_rate=$(( (success * 100) / executed ))
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# Calculate average duration
|
|
220
|
+
local avg_duration=0
|
|
221
|
+
if [[ "$executed" -gt 0 ]]; then
|
|
222
|
+
avg_duration=$((sum_duration / executed))
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# Get percentiles
|
|
226
|
+
local p50 p95 p99
|
|
227
|
+
p50=$(_metrics_calculate_percentile "$hook_name" 50)
|
|
228
|
+
p95=$(_metrics_calculate_percentile "$hook_name" 95)
|
|
229
|
+
p99=$(_metrics_calculate_percentile "$hook_name" 99)
|
|
230
|
+
|
|
231
|
+
echo "{\"hook\":\"$hook_name\",\"total\":$total,\"success\":$success,\"failure\":$failure,\"timeout\":$timeout,\"skipped\":$skipped,\"success_rate\":$success_rate,\"avg_ms\":$avg_duration,\"min_ms\":$min_duration,\"max_ms\":$max_duration,\"p50_ms\":$p50,\"p95_ms\":$p95,\"p99_ms\":$p99}"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Calculate health score (0-100)
|
|
235
|
+
metrics_health_score() {
|
|
236
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
237
|
+
|
|
238
|
+
local stats_file
|
|
239
|
+
stats_file=$(_metrics_stats_file "$hook_name")
|
|
240
|
+
|
|
241
|
+
[[ ! -f "$stats_file" ]] && echo "100" && return
|
|
242
|
+
|
|
243
|
+
local total success failure timeout skipped sum_duration min_duration max_duration
|
|
244
|
+
IFS=',' read -r total success failure timeout skipped sum_duration min_duration max_duration < "$stats_file" 2>/dev/null || true
|
|
245
|
+
|
|
246
|
+
local executed=$((total - skipped))
|
|
247
|
+
[[ "$executed" -eq 0 ]] && echo "100" && return
|
|
248
|
+
|
|
249
|
+
# Score components:
|
|
250
|
+
# - Success rate: 60% weight
|
|
251
|
+
# - No timeouts: 25% weight
|
|
252
|
+
# - Low latency: 15% weight
|
|
253
|
+
|
|
254
|
+
local success_rate=$(( (success * 100) / executed ))
|
|
255
|
+
local timeout_rate=$(( (timeout * 100) / executed ))
|
|
256
|
+
local avg_duration=$((sum_duration / executed))
|
|
257
|
+
|
|
258
|
+
# Success rate score (0-60)
|
|
259
|
+
local success_score=$((success_rate * 60 / 100))
|
|
260
|
+
|
|
261
|
+
# Timeout score (0-25, penalize timeouts heavily)
|
|
262
|
+
local timeout_score=$((25 - (timeout_rate * 25 / 100)))
|
|
263
|
+
[[ "$timeout_score" -lt 0 ]] && timeout_score=0
|
|
264
|
+
|
|
265
|
+
# Latency score (0-15, based on avg duration)
|
|
266
|
+
# <100ms = 15, 100-500ms = 10, 500-1000ms = 5, >1000ms = 0
|
|
267
|
+
local latency_score=0
|
|
268
|
+
if [[ "$avg_duration" -lt 100 ]]; then
|
|
269
|
+
latency_score=15
|
|
270
|
+
elif [[ "$avg_duration" -lt 500 ]]; then
|
|
271
|
+
latency_score=10
|
|
272
|
+
elif [[ "$avg_duration" -lt 1000 ]]; then
|
|
273
|
+
latency_score=5
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
local total_score=$((success_score + timeout_score + latency_score))
|
|
277
|
+
echo "$total_score"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Get all hooks metrics summary
|
|
281
|
+
metrics_get_all() {
|
|
282
|
+
local result="["
|
|
283
|
+
local first=true
|
|
284
|
+
|
|
285
|
+
for stats_file in "$METRICS_DIR"/*.stats; do
|
|
286
|
+
[[ ! -f "$stats_file" ]] && continue
|
|
287
|
+
|
|
288
|
+
local hook_name
|
|
289
|
+
hook_name=$(basename "$stats_file" .stats)
|
|
290
|
+
|
|
291
|
+
if [[ "$first" == "true" ]]; then
|
|
292
|
+
first=false
|
|
293
|
+
else
|
|
294
|
+
result="$result,"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
result="$result$(metrics_get "$hook_name")"
|
|
298
|
+
done
|
|
299
|
+
|
|
300
|
+
result="$result]"
|
|
301
|
+
echo "$result"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Reset metrics for a hook
|
|
305
|
+
metrics_reset() {
|
|
306
|
+
local hook_name="${1:-$_METRICS_HOOK_NAME}"
|
|
307
|
+
|
|
308
|
+
rm -f "$METRICS_DIR/${hook_name}.buffer" 2>/dev/null || true
|
|
309
|
+
rm -f "$METRICS_DIR/${hook_name}.stats" 2>/dev/null || true
|
|
310
|
+
rm -f "$METRICS_DIR/${hook_name}.hourly."* 2>/dev/null || true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Get system-wide health summary
|
|
314
|
+
metrics_system_health() {
|
|
315
|
+
local total_hooks=0
|
|
316
|
+
local healthy_hooks=0
|
|
317
|
+
local degraded_hooks=0
|
|
318
|
+
local unhealthy_hooks=0
|
|
319
|
+
|
|
320
|
+
for stats_file in "$METRICS_DIR"/*.stats; do
|
|
321
|
+
[[ ! -f "$stats_file" ]] && continue
|
|
322
|
+
|
|
323
|
+
local hook_name
|
|
324
|
+
hook_name=$(basename "$stats_file" .stats)
|
|
325
|
+
local health_score
|
|
326
|
+
health_score=$(metrics_health_score "$hook_name")
|
|
327
|
+
|
|
328
|
+
total_hooks=$((total_hooks + 1))
|
|
329
|
+
|
|
330
|
+
if [[ "$health_score" -ge 80 ]]; then
|
|
331
|
+
healthy_hooks=$((healthy_hooks + 1))
|
|
332
|
+
elif [[ "$health_score" -ge 50 ]]; then
|
|
333
|
+
degraded_hooks=$((degraded_hooks + 1))
|
|
334
|
+
else
|
|
335
|
+
unhealthy_hooks=$((unhealthy_hooks + 1))
|
|
336
|
+
fi
|
|
337
|
+
done
|
|
338
|
+
|
|
339
|
+
local overall_health="healthy"
|
|
340
|
+
if [[ "$unhealthy_hooks" -gt 0 ]]; then
|
|
341
|
+
overall_health="unhealthy"
|
|
342
|
+
elif [[ "$degraded_hooks" -gt 0 ]]; then
|
|
343
|
+
overall_health="degraded"
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
echo "{\"status\":\"$overall_health\",\"total_hooks\":$total_hooks,\"healthy\":$healthy_hooks,\"degraded\":$degraded_hooks,\"unhealthy\":$unhealthy_hooks}"
|
|
347
|
+
}
|