# CoreCI — deploy-stage timing helper (P11 — IDEATE-39). # # Sourced (not executed) by the deploy orchestrators # (proxy-deploy.sh, lxc-deploy.sh) to emit structured slog-style # JSON timing lines for each deploy stage to stderr, where a log # aggregator (or `2>>timing.log`) can pick them up. # # Usage: # . /path/to/timing.sh # timing_start clone # ... clone work ... # timing_end clone # # Emits one JSON line per timing_end to stderr: # {"event":"praxis_deploy_timing","stage":"clone","duration_s":3} # # Optional node_exporter textfile collector: if the env var # NODE_TEXTFILE_COLLECTOR_DIR points to a writable directory, the # latest per-stage duration is ALSO written there as # `praxis_deploy_timing_.prom` so a node_exporter textfile # collector scrapes it. If the dir is unset or unwritable, only the # JSON log is emitted (the structured-log-first decision, PLAN v3.6 # P11 Wave 2). # # Dependencies: date (POSIX epoch via +%s). jq is NOT required (the # JSON line is constructed with printf so there is no external dep # on the slow path). Idempotent: re-sourcing is harmless (the # _TIMING_STARTS associative state is reset on source, but the # orchestrator sources exactly once at startup). # # Adapted from coreci for praxis: metric/event prefixes renamed from # `coreci_deploy_timing` → `praxis_deploy_timing` (TASK-03-07). # # shellcheck shell=sh # _TIMING_STARTS is a flat file-backed map (stage → epoch seconds). # POSIX sh has no associative arrays, so we use a single newline- # separated string of "stage=epoch" records and scan it. Stages are # short identifiers (clone/config/start/health/smoke) so the linear # scan is trivially cheap. _TIMING_STARTS="" # timing_start — record the current epoch for . # Overwrites a prior start for the same stage (idempotent re-entry). timing_start() { _stage="$1" _now=$(date +%s) # Drop any prior record for this stage, then append the fresh one. _TIMING_STARTS="$(printf '%s\n' "$_TIMING_STARTS" \ | while IFS= read -r _line; do case "$_line" in "${_stage}="*) ;; *) [ -n "$_line" ] && printf '%s\n' "$_line" ;; esac done)" _TIMING_STARTS="${_TIMING_STARTS:+${_TIMING_STARTS} }${_stage}=${_now}" } # timing_end — compute duration since timing_start , # emit the JSON line to stderr, and optionally write the textfile # collector entry. If no start was recorded for , emit nothing # (defensive — a stray timing_end with no start is a no-op). timing_end() { _stage="$1" _now=$(date +%s) _start="" # Scan the records for the matching stage. _rest="" while IFS= read -r _line; do [ -n "$_line" ] || continue case "$_line" in "${_stage}="*) _start="${_line#*=}" ;; *) _rest="${_rest:+${_rest} }${_line}" ;; esac done <&2 # Optional node_exporter textfile collector. if [ -n "${NODE_TEXTFILE_COLLECTOR_DIR:-}" ] && \ [ -d "$NODE_TEXTFILE_COLLECTOR_DIR" ] && \ [ -w "$NODE_TEXTFILE_COLLECTOR_DIR" ]; then _tf="${NODE_TEXTFILE_COLLECTOR_DIR}/praxis_deploy_timing_${_stage}.prom" { printf '# HELP praxis_deploy_timing_seconds Duration of the %s deploy stage.\n' "$_stage" printf '# TYPE praxis_deploy_timing_seconds gauge\n' printf 'praxis_deploy_timing_seconds{stage="%s"} %s\n' "$_stage" "$_duration" } > "$_tf" 2>/dev/null || true fi }