shipwright-cli 1.10.0 → 2.0.0
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/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright otel — OpenTelemetry Observability ║
|
|
4
|
+
# ║ Prometheus metrics, traces, OTLP export, webhook forwarding, dashboard ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="2.0.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── State Directories ──────────────────────────────────────────────────────
|
|
38
|
+
OTEL_DIR="${HOME}/.shipwright/otel"
|
|
39
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
40
|
+
DAEMON_STATE="${HOME}/.shipwright/daemon-state.json"
|
|
41
|
+
OTEL_CONFIG="${REPO_DIR}/.claude/otel-config.json"
|
|
42
|
+
|
|
43
|
+
ensure_otel_dir() {
|
|
44
|
+
mkdir -p "$OTEL_DIR"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ─── Prometheus Metrics ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
cmd_metrics() {
|
|
50
|
+
local format="${1:-text}"
|
|
51
|
+
|
|
52
|
+
ensure_otel_dir
|
|
53
|
+
|
|
54
|
+
# Initialize counters
|
|
55
|
+
local total_pipelines=0
|
|
56
|
+
local active_pipelines=0
|
|
57
|
+
local failed_pipelines=0
|
|
58
|
+
local succeeded_pipelines=0
|
|
59
|
+
local total_cost=0
|
|
60
|
+
|
|
61
|
+
# Status breakdown
|
|
62
|
+
local status_success=0
|
|
63
|
+
local status_failed=0
|
|
64
|
+
local status_running=0
|
|
65
|
+
|
|
66
|
+
# Template counts
|
|
67
|
+
declare -a templates
|
|
68
|
+
declare -a template_counts
|
|
69
|
+
|
|
70
|
+
# Stage timing
|
|
71
|
+
declare -a stages
|
|
72
|
+
declare -a stage_durations
|
|
73
|
+
|
|
74
|
+
# Model costs
|
|
75
|
+
declare -a models
|
|
76
|
+
declare -a model_costs
|
|
77
|
+
|
|
78
|
+
# Parse events.jsonl
|
|
79
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
80
|
+
while IFS= read -r line; do
|
|
81
|
+
[[ -z "$line" ]] && continue
|
|
82
|
+
|
|
83
|
+
local event_type
|
|
84
|
+
event_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null || true)
|
|
85
|
+
|
|
86
|
+
case "$event_type" in
|
|
87
|
+
pipeline_start)
|
|
88
|
+
((total_pipelines++))
|
|
89
|
+
((active_pipelines++))
|
|
90
|
+
;;
|
|
91
|
+
pipeline_complete)
|
|
92
|
+
((active_pipelines--))
|
|
93
|
+
((succeeded_pipelines++))
|
|
94
|
+
((status_success++))
|
|
95
|
+
;;
|
|
96
|
+
pipeline_failed)
|
|
97
|
+
((active_pipelines--))
|
|
98
|
+
((failed_pipelines++))
|
|
99
|
+
((status_failed++))
|
|
100
|
+
;;
|
|
101
|
+
stage_complete)
|
|
102
|
+
local stage duration
|
|
103
|
+
stage=$(echo "$line" | jq -r '.stage // "unknown"' 2>/dev/null || true)
|
|
104
|
+
duration=$(echo "$line" | jq -r '.duration_seconds // 0' 2>/dev/null || true)
|
|
105
|
+
if [[ -n "$stage" && "$duration" != "0" ]]; then
|
|
106
|
+
stage_durations+=("$stage:$duration")
|
|
107
|
+
fi
|
|
108
|
+
;;
|
|
109
|
+
cost_recorded)
|
|
110
|
+
local cost model
|
|
111
|
+
cost=$(echo "$line" | jq -r '.cost_usd // 0' 2>/dev/null || true)
|
|
112
|
+
model=$(echo "$line" | jq -r '.model // "unknown"' 2>/dev/null || true)
|
|
113
|
+
total_cost=$(awk -v t="$total_cost" -v c="$cost" 'BEGIN { printf "%.4f", t + c }')
|
|
114
|
+
model_costs+=("$model:$cost")
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
done < "$EVENTS_FILE"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Parse daemon state for queue depth
|
|
121
|
+
local queue_depth=0
|
|
122
|
+
if [[ -f "$DAEMON_STATE" ]]; then
|
|
123
|
+
queue_depth=$(jq -r '.active_jobs | length // 0' "$DAEMON_STATE" 2>/dev/null || echo "0")
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Calculate histogram buckets for stage durations
|
|
127
|
+
local stage_p50=0 stage_p99=0
|
|
128
|
+
if [[ ${#stage_durations[@]} -gt 0 ]]; then
|
|
129
|
+
# Simple approximation for percentiles
|
|
130
|
+
stage_p50=$(printf '%s\n' "${stage_durations[@]}" | cut -d: -f2 | sort -n | head -n $((${#stage_durations[@]}/2)) | tail -n1 || echo "0")
|
|
131
|
+
stage_p99=$(printf '%s\n' "${stage_durations[@]}" | cut -d: -f2 | sort -n | tail -n1 || echo "0")
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
if [[ "$format" == "json" ]]; then
|
|
135
|
+
cat << EOF
|
|
136
|
+
{
|
|
137
|
+
"metrics": {
|
|
138
|
+
"pipelines_total": {
|
|
139
|
+
"value": $total_pipelines,
|
|
140
|
+
"type": "counter"
|
|
141
|
+
},
|
|
142
|
+
"active_pipelines": {
|
|
143
|
+
"value": $active_pipelines,
|
|
144
|
+
"type": "gauge"
|
|
145
|
+
},
|
|
146
|
+
"pipelines_succeeded": {
|
|
147
|
+
"value": $succeeded_pipelines,
|
|
148
|
+
"type": "counter"
|
|
149
|
+
},
|
|
150
|
+
"pipelines_failed": {
|
|
151
|
+
"value": $failed_pipelines,
|
|
152
|
+
"type": "counter"
|
|
153
|
+
},
|
|
154
|
+
"cost_total_usd": {
|
|
155
|
+
"value": $total_cost,
|
|
156
|
+
"type": "counter"
|
|
157
|
+
},
|
|
158
|
+
"queue_depth": {
|
|
159
|
+
"value": $queue_depth,
|
|
160
|
+
"type": "gauge"
|
|
161
|
+
},
|
|
162
|
+
"stage_duration_p50_seconds": {
|
|
163
|
+
"value": $stage_p50,
|
|
164
|
+
"type": "histogram"
|
|
165
|
+
},
|
|
166
|
+
"stage_duration_p99_seconds": {
|
|
167
|
+
"value": $stage_p99,
|
|
168
|
+
"type": "histogram"
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"timestamp": "$(now_iso)"
|
|
172
|
+
}
|
|
173
|
+
EOF
|
|
174
|
+
else
|
|
175
|
+
# Prometheus text format
|
|
176
|
+
cat << EOF
|
|
177
|
+
# HELP shipwright_pipelines_total Total number of pipeline runs
|
|
178
|
+
# TYPE shipwright_pipelines_total counter
|
|
179
|
+
shipwright_pipelines_total $total_pipelines
|
|
180
|
+
|
|
181
|
+
# HELP shipwright_active_pipelines Currently running pipelines
|
|
182
|
+
# TYPE shipwright_active_pipelines gauge
|
|
183
|
+
shipwright_active_pipelines $active_pipelines
|
|
184
|
+
|
|
185
|
+
# HELP shipwright_pipelines_succeeded Successfully completed pipelines
|
|
186
|
+
# TYPE shipwright_pipelines_succeeded counter
|
|
187
|
+
shipwright_pipelines_succeeded $succeeded_pipelines
|
|
188
|
+
|
|
189
|
+
# HELP shipwright_pipelines_failed Failed pipelines
|
|
190
|
+
# TYPE shipwright_pipelines_failed counter
|
|
191
|
+
shipwright_pipelines_failed $failed_pipelines
|
|
192
|
+
|
|
193
|
+
# HELP shipwright_cost_total_usd Total cost in USD
|
|
194
|
+
# TYPE shipwright_cost_total_usd counter
|
|
195
|
+
shipwright_cost_total_usd $total_cost
|
|
196
|
+
|
|
197
|
+
# HELP shipwright_queue_depth Number of queued jobs
|
|
198
|
+
# TYPE shipwright_queue_depth gauge
|
|
199
|
+
shipwright_queue_depth $queue_depth
|
|
200
|
+
|
|
201
|
+
# HELP shipwright_stage_duration_seconds Stage duration histogram
|
|
202
|
+
# TYPE shipwright_stage_duration_seconds histogram
|
|
203
|
+
shipwright_stage_duration_seconds_bucket{le="1"} 0
|
|
204
|
+
shipwright_stage_duration_seconds_bucket{le="5"} 0
|
|
205
|
+
shipwright_stage_duration_seconds_bucket{le="10"} 0
|
|
206
|
+
shipwright_stage_duration_seconds_bucket{le="30"} 0
|
|
207
|
+
shipwright_stage_duration_seconds_bucket{le="60"} 0
|
|
208
|
+
shipwright_stage_duration_seconds_bucket{le="300"} 0
|
|
209
|
+
shipwright_stage_duration_seconds_bucket{le="+Inf"} $total_pipelines
|
|
210
|
+
shipwright_stage_duration_seconds_sum 0
|
|
211
|
+
shipwright_stage_duration_seconds_count $total_pipelines
|
|
212
|
+
EOF
|
|
213
|
+
fi
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# ─── OpenTelemetry Traces ────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
cmd_trace() {
|
|
219
|
+
local pipeline_id="${1:-latest}"
|
|
220
|
+
|
|
221
|
+
ensure_otel_dir
|
|
222
|
+
|
|
223
|
+
# Build trace from events
|
|
224
|
+
local traces='[]'
|
|
225
|
+
local spans='[]'
|
|
226
|
+
local root_span=""
|
|
227
|
+
|
|
228
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
229
|
+
while IFS= read -r line; do
|
|
230
|
+
[[ -z "$line" ]] && continue
|
|
231
|
+
|
|
232
|
+
local event_type ts stage pipeline
|
|
233
|
+
event_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null || true)
|
|
234
|
+
ts=$(echo "$line" | jq -r '.ts // empty' 2>/dev/null || true)
|
|
235
|
+
pipeline=$(echo "$line" | jq -r '.pipeline_id // empty' 2>/dev/null || true)
|
|
236
|
+
|
|
237
|
+
[[ -z "$event_type" ]] && continue
|
|
238
|
+
|
|
239
|
+
case "$event_type" in
|
|
240
|
+
pipeline_start)
|
|
241
|
+
root_span=$(cat << EOF
|
|
242
|
+
{
|
|
243
|
+
"traceId": "${pipeline:0:16}",
|
|
244
|
+
"spanId": "${pipeline:0:16}",
|
|
245
|
+
"parentSpanId": "",
|
|
246
|
+
"name": "pipeline",
|
|
247
|
+
"kind": "SPAN_KIND_INTERNAL",
|
|
248
|
+
"startTime": "${ts}",
|
|
249
|
+
"endTime": "",
|
|
250
|
+
"status": {
|
|
251
|
+
"code": "STATUS_CODE_UNSET",
|
|
252
|
+
"message": ""
|
|
253
|
+
},
|
|
254
|
+
"attributes": {
|
|
255
|
+
"pipeline.id": "$pipeline",
|
|
256
|
+
"pipeline.status": "running"
|
|
257
|
+
},
|
|
258
|
+
"events": []
|
|
259
|
+
}
|
|
260
|
+
EOF
|
|
261
|
+
)
|
|
262
|
+
;;
|
|
263
|
+
stage_start)
|
|
264
|
+
stage=$(echo "$line" | jq -r '.stage // "unknown"' 2>/dev/null || true)
|
|
265
|
+
local span_id="${stage:0:8}$(printf '%08x' $((RANDOM * 256 + RANDOM)))"
|
|
266
|
+
spans=$(echo "$spans" | jq --arg span_id "$span_id" --arg stage "$stage" --arg ts "$ts" \
|
|
267
|
+
'. += [{
|
|
268
|
+
"traceId": "'${pipeline:0:16}'",
|
|
269
|
+
"spanId": "'$span_id'",
|
|
270
|
+
"parentSpanId": "'${root_span:0:16}'",
|
|
271
|
+
"name": "stage_'$stage'",
|
|
272
|
+
"kind": "SPAN_KIND_INTERNAL",
|
|
273
|
+
"startTime": "'$ts'",
|
|
274
|
+
"attributes": { "stage.name": "'$stage'" }
|
|
275
|
+
}]')
|
|
276
|
+
;;
|
|
277
|
+
esac
|
|
278
|
+
done < "$EVENTS_FILE"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Output OTel trace JSON
|
|
282
|
+
cat << EOF
|
|
283
|
+
{
|
|
284
|
+
"resourceSpans": [
|
|
285
|
+
{
|
|
286
|
+
"resource": {
|
|
287
|
+
"attributes": {
|
|
288
|
+
"service.name": "shipwright",
|
|
289
|
+
"service.version": "$VERSION"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"scopeSpans": [
|
|
293
|
+
{
|
|
294
|
+
"scope": {
|
|
295
|
+
"name": "shipwright-tracer",
|
|
296
|
+
"version": "$VERSION"
|
|
297
|
+
},
|
|
298
|
+
"spans": $spans
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"exportedAt": "$(now_iso)"
|
|
304
|
+
}
|
|
305
|
+
EOF
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# ─── OTLP Export ────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
cmd_export() {
|
|
311
|
+
local format="${1:-prometheus}"
|
|
312
|
+
|
|
313
|
+
ensure_otel_dir
|
|
314
|
+
|
|
315
|
+
local endpoint="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}"
|
|
316
|
+
local auth_header=""
|
|
317
|
+
|
|
318
|
+
if [[ -n "${OTEL_EXPORTER_OTLP_HEADERS:-}" ]]; then
|
|
319
|
+
auth_header="-H '${OTEL_EXPORTER_OTLP_HEADERS}'"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
info "Exporting $format metrics to $endpoint"
|
|
323
|
+
|
|
324
|
+
local payload
|
|
325
|
+
if [[ "$format" == "trace" ]]; then
|
|
326
|
+
payload=$(cmd_trace)
|
|
327
|
+
local response
|
|
328
|
+
response=$(curl -s -X POST \
|
|
329
|
+
"$endpoint/v1/traces" \
|
|
330
|
+
-H "Content-Type: application/json" \
|
|
331
|
+
$auth_header \
|
|
332
|
+
-d "$payload" 2>&1 || echo "{\"error\": \"export failed\"}")
|
|
333
|
+
|
|
334
|
+
if echo "$response" | jq . >/dev/null 2>&1; then
|
|
335
|
+
success "Traces exported successfully"
|
|
336
|
+
else
|
|
337
|
+
error "Failed to export traces: $response"
|
|
338
|
+
return 1
|
|
339
|
+
fi
|
|
340
|
+
else
|
|
341
|
+
payload=$(cmd_metrics text)
|
|
342
|
+
local response
|
|
343
|
+
response=$(curl -s -X POST \
|
|
344
|
+
"$endpoint/metrics" \
|
|
345
|
+
-H "Content-Type: text/plain" \
|
|
346
|
+
$auth_header \
|
|
347
|
+
--data-binary "$payload" 2>&1 || echo "error")
|
|
348
|
+
|
|
349
|
+
if [[ "$response" == "200" ]] || [[ "$response" == "" ]]; then
|
|
350
|
+
success "Metrics exported successfully"
|
|
351
|
+
else
|
|
352
|
+
error "Failed to export metrics: $response"
|
|
353
|
+
return 1
|
|
354
|
+
fi
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Record export event
|
|
358
|
+
mkdir -p "${HOME}/.shipwright"
|
|
359
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"otel_export\",\"format\":\"$format\",\"endpoint\":\"$endpoint\"}" \
|
|
360
|
+
>> "${HOME}/.shipwright/events.jsonl"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# ─── Webhook Forwarding ──────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
cmd_webhook() {
|
|
366
|
+
local action="${1:-send}"
|
|
367
|
+
local webhook_url="${OTEL_WEBHOOK_URL:-}"
|
|
368
|
+
|
|
369
|
+
if [[ -z "$webhook_url" ]]; then
|
|
370
|
+
error "OTEL_WEBHOOK_URL environment variable not set"
|
|
371
|
+
return 1
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
if [[ "$action" == "send" ]]; then
|
|
375
|
+
info "Forwarding events to webhook: $webhook_url"
|
|
376
|
+
|
|
377
|
+
# Get latest unforwarded events
|
|
378
|
+
local payload
|
|
379
|
+
payload=$(cmd_metrics json)
|
|
380
|
+
|
|
381
|
+
local max_retries=3
|
|
382
|
+
local retry=0
|
|
383
|
+
local backoff=1
|
|
384
|
+
|
|
385
|
+
while [[ $retry -lt $max_retries ]]; do
|
|
386
|
+
local response
|
|
387
|
+
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
388
|
+
"$webhook_url" \
|
|
389
|
+
-H "Content-Type: application/json" \
|
|
390
|
+
-d "$payload" 2>&1 || echo "000")
|
|
391
|
+
|
|
392
|
+
local http_code
|
|
393
|
+
http_code=$(echo "$response" | tail -n1)
|
|
394
|
+
|
|
395
|
+
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]] || [[ "$http_code" == "204" ]]; then
|
|
396
|
+
success "Webhook delivered (HTTP $http_code)"
|
|
397
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"webhook_sent\",\"url\":\"$webhook_url\",\"http_code\":$http_code}" \
|
|
398
|
+
>> "${HOME}/.shipwright/events.jsonl"
|
|
399
|
+
return 0
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
((retry++))
|
|
403
|
+
if [[ $retry -lt $max_retries ]]; then
|
|
404
|
+
warn "Webhook failed (HTTP $http_code), retrying in ${backoff}s..."
|
|
405
|
+
sleep "$backoff"
|
|
406
|
+
backoff=$((backoff * 2))
|
|
407
|
+
fi
|
|
408
|
+
done
|
|
409
|
+
|
|
410
|
+
error "Webhook delivery failed after $max_retries attempts"
|
|
411
|
+
return 1
|
|
412
|
+
elif [[ "$action" == "config" ]]; then
|
|
413
|
+
info "Webhook configuration:"
|
|
414
|
+
echo " URL: $webhook_url"
|
|
415
|
+
echo " Status: enabled"
|
|
416
|
+
else
|
|
417
|
+
error "Unknown webhook action: $action"
|
|
418
|
+
return 1
|
|
419
|
+
fi
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# ─── Dashboard Metrics ───────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
cmd_dashboard() {
|
|
425
|
+
ensure_otel_dir
|
|
426
|
+
|
|
427
|
+
# Aggregate metrics for dashboard
|
|
428
|
+
local metrics
|
|
429
|
+
metrics=$(cmd_metrics json)
|
|
430
|
+
|
|
431
|
+
# Enhance with additional dashboard fields
|
|
432
|
+
echo "$metrics" | jq '
|
|
433
|
+
.dashboard = {
|
|
434
|
+
"pipelines": {
|
|
435
|
+
"total": .metrics.pipelines_total.value,
|
|
436
|
+
"active": .metrics.active_pipelines.value,
|
|
437
|
+
"success_rate": ((.metrics.pipelines_succeeded.value / (.metrics.pipelines_total.value + 0.001)) * 100 | floor)
|
|
438
|
+
},
|
|
439
|
+
"costs": {
|
|
440
|
+
"total_usd": .metrics.cost_total_usd.value,
|
|
441
|
+
"daily_avg": (.metrics.cost_total_usd.value / 30)
|
|
442
|
+
},
|
|
443
|
+
"queue": {
|
|
444
|
+
"depth": .metrics.queue_depth.value
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
'
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# ─── Observability Report ───────────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
cmd_report() {
|
|
453
|
+
ensure_otel_dir
|
|
454
|
+
|
|
455
|
+
info "Shipwright Observability Health Report"
|
|
456
|
+
echo ""
|
|
457
|
+
|
|
458
|
+
# Event volume
|
|
459
|
+
local event_count=0
|
|
460
|
+
local export_count=0
|
|
461
|
+
local webhook_count=0
|
|
462
|
+
local last_event_ts=""
|
|
463
|
+
|
|
464
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
465
|
+
event_count=$(wc -l < "$EVENTS_FILE" || echo "0")
|
|
466
|
+
export_count=$(grep -c '"type":"otel_export"' "$EVENTS_FILE" 2>/dev/null || echo "0")
|
|
467
|
+
webhook_count=$(grep -c '"type":"webhook_sent"' "$EVENTS_FILE" 2>/dev/null || echo "0")
|
|
468
|
+
last_event_ts=$(tail -n1 "$EVENTS_FILE" | jq -r '.ts // "unknown"' 2>/dev/null || echo "unknown")
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
echo -e "${BOLD}Events:${RESET}"
|
|
472
|
+
echo " Total events: $event_count"
|
|
473
|
+
echo " OTLP exports: $export_count"
|
|
474
|
+
echo " Webhook sends: $webhook_count"
|
|
475
|
+
echo " Last event: $last_event_ts"
|
|
476
|
+
echo ""
|
|
477
|
+
|
|
478
|
+
# Metrics summary
|
|
479
|
+
local metrics
|
|
480
|
+
metrics=$(cmd_metrics json)
|
|
481
|
+
|
|
482
|
+
local active_pipelines succeeded failed cost
|
|
483
|
+
active_pipelines=$(echo "$metrics" | jq -r '.metrics.active_pipelines.value')
|
|
484
|
+
succeeded=$(echo "$metrics" | jq -r '.metrics.pipelines_succeeded.value')
|
|
485
|
+
failed=$(echo "$metrics" | jq -r '.metrics.pipelines_failed.value')
|
|
486
|
+
cost=$(echo "$metrics" | jq -r '.metrics.cost_total_usd.value')
|
|
487
|
+
|
|
488
|
+
echo -e "${BOLD}Pipeline Metrics:${RESET}"
|
|
489
|
+
echo " Active: $active_pipelines"
|
|
490
|
+
echo " Succeeded: $succeeded"
|
|
491
|
+
echo " Failed: $failed"
|
|
492
|
+
echo " Total cost: \$$(printf '%.2f' "$cost")"
|
|
493
|
+
echo ""
|
|
494
|
+
|
|
495
|
+
# Export health
|
|
496
|
+
local export_success_rate=0
|
|
497
|
+
if [[ $export_count -gt 0 ]]; then
|
|
498
|
+
export_success_rate=$((succeeded * 100 / (succeeded + failed + 1)))
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
echo -e "${BOLD}Export Health:${RESET}"
|
|
502
|
+
echo " Success rate: ${export_success_rate}%"
|
|
503
|
+
echo " Configuration: $(test -f "$OTEL_CONFIG" && echo "present" || echo "not found")"
|
|
504
|
+
|
|
505
|
+
# Recommendations
|
|
506
|
+
echo ""
|
|
507
|
+
echo -e "${BOLD}${CYAN}Recommendations:${RESET}"
|
|
508
|
+
if [[ $active_pipelines -gt 10 ]]; then
|
|
509
|
+
echo " ⚠ High queue depth ($active_pipelines) — consider scaling"
|
|
510
|
+
fi
|
|
511
|
+
if [[ $export_count -eq 0 ]]; then
|
|
512
|
+
echo " ⚠ No exports configured — set OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
513
|
+
fi
|
|
514
|
+
if [[ $webhook_count -eq 0 ]]; then
|
|
515
|
+
echo " ℹ No webhooks configured — set OTEL_WEBHOOK_URL for event forwarding"
|
|
516
|
+
fi
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# ─── Help ────────────────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
show_help() {
|
|
522
|
+
cat << EOF
|
|
523
|
+
${BOLD}${CYAN}shipwright otel${RESET} — OpenTelemetry Observability
|
|
524
|
+
|
|
525
|
+
${BOLD}USAGE${RESET}
|
|
526
|
+
${CYAN}shipwright otel${RESET} <subcommand> [options]
|
|
527
|
+
|
|
528
|
+
${BOLD}SUBCOMMANDS${RESET}
|
|
529
|
+
${CYAN}metrics${RESET} [format] Prometheus-format metrics (text or json)
|
|
530
|
+
${CYAN}trace${RESET} [pipeline-id] OpenTelemetry trace for a pipeline run
|
|
531
|
+
${CYAN}export${RESET} [format] Export metrics/traces to OTLP endpoint
|
|
532
|
+
${CYAN}webhook${RESET} <action> Webhook operations (send, config)
|
|
533
|
+
${CYAN}dashboard${RESET} Dashboard-ready JSON metrics
|
|
534
|
+
${CYAN}report${RESET} Observability health report
|
|
535
|
+
${CYAN}help${RESET} Show this help message
|
|
536
|
+
|
|
537
|
+
${BOLD}FORMATS${RESET}
|
|
538
|
+
${DIM}prometheus, text${RESET} Prometheus text format (default)
|
|
539
|
+
${DIM}json${RESET} JSON format
|
|
540
|
+
${DIM}trace${RESET} OpenTelemetry trace format
|
|
541
|
+
|
|
542
|
+
${BOLD}ENVIRONMENT VARIABLES${RESET}
|
|
543
|
+
${DIM}OTEL_EXPORTER_OTLP_ENDPOINT${RESET} OTLP collector endpoint (default: http://localhost:4318)
|
|
544
|
+
${DIM}OTEL_EXPORTER_OTLP_HEADERS${RESET} Authorization header for OTLP
|
|
545
|
+
${DIM}OTEL_WEBHOOK_URL${RESET} Webhook endpoint for event forwarding
|
|
546
|
+
|
|
547
|
+
${BOLD}EXAMPLES${RESET}
|
|
548
|
+
${DIM}shipwright otel metrics${RESET} # Show Prometheus metrics
|
|
549
|
+
${DIM}shipwright otel metrics json${RESET} # JSON format
|
|
550
|
+
${DIM}shipwright otel export prometheus${RESET} # Export to OTLP endpoint
|
|
551
|
+
${DIM}OTEL_WEBHOOK_URL=https://api.example.com shipwright otel webhook send${RESET}
|
|
552
|
+
${DIM}shipwright otel report${RESET} # Health status
|
|
553
|
+
|
|
554
|
+
EOF
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
main() {
|
|
560
|
+
local cmd="${1:-help}"
|
|
561
|
+
shift 2>/dev/null || true
|
|
562
|
+
|
|
563
|
+
case "$cmd" in
|
|
564
|
+
metrics)
|
|
565
|
+
cmd_metrics "$@"
|
|
566
|
+
;;
|
|
567
|
+
trace)
|
|
568
|
+
cmd_trace "$@"
|
|
569
|
+
;;
|
|
570
|
+
export)
|
|
571
|
+
cmd_export "$@"
|
|
572
|
+
;;
|
|
573
|
+
webhook)
|
|
574
|
+
cmd_webhook "$@"
|
|
575
|
+
;;
|
|
576
|
+
dashboard)
|
|
577
|
+
cmd_dashboard "$@"
|
|
578
|
+
;;
|
|
579
|
+
report)
|
|
580
|
+
cmd_report "$@"
|
|
581
|
+
;;
|
|
582
|
+
help|--help|-h)
|
|
583
|
+
show_help
|
|
584
|
+
;;
|
|
585
|
+
*)
|
|
586
|
+
error "Unknown command: $cmd"
|
|
587
|
+
echo ""
|
|
588
|
+
show_help
|
|
589
|
+
exit 1
|
|
590
|
+
;;
|
|
591
|
+
esac
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
595
|
+
main "$@"
|
|
596
|
+
fi
|