6cf63cb064
Verification layers: Structural: PASS (all scripts executable, syntax clean, Dockerfile valid) Behavioral: PASS (121 bats, 77 pytest, docker build succeeds, compose valid) Security: PASS (no secrets committed, .dockerignore excludes .env*, env_file pattern) Quality: PASS (coreci patterns followed, no coreci refs, G-104/G-105/G-106 verified) P0 issues found and auto-fixed: 1. docker-compose.yml: removed invalid restart_policy key, fixed env_file syntax 2. pyproject.toml: added fastapi + uvicorn deps (v0.1 gap exposed by Dockerfile) 3. timing.sh: renamed coreci_deploy_timing → praxis_deploy_timing (TASK-03-07) 4. firstboot-hook.sh: fixed idempotency check (/opt/praxis/.git not /usr/local/bin/praxis-deploy) P1+ issues: 8 (1 fixed: lxc-config.sh default alignment, 7 noted for post-hoc review) REQ coverage: 18/20 covered, 2 deferred (live first-boot timing + live E2E require cluster) Must-haves: 25/28 pass, 2 partial (comment-only diffs, no Makefile), 1 deferred ---ci--- project: praxis phase: 1 milestone: v0.2 status: verify ---/ci---
101 lines
3.8 KiB
Bash
Executable File
101 lines
3.8 KiB
Bash
Executable File
# 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_<stage>.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 <stage> — record the current epoch for <stage>.
|
|
# 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 <stage> — compute duration since timing_start <stage>,
|
|
# emit the JSON line to stderr, and optionally write the textfile
|
|
# collector entry. If no start was recorded for <stage>, 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 <<EOF
|
|
${_TIMING_STARTS}
|
|
EOF
|
|
[ -n "$_start" ] || return 0
|
|
_duration=$((_now - _start))
|
|
_TIMING_STARTS="$_rest"
|
|
# Structured JSON to stderr (slog-style: single-line JSON).
|
|
printf '{"event":"praxis_deploy_timing","stage":"%s","duration_s":%s}\n' \
|
|
"$_stage" "$_duration" >&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
|
|
} |