From 109814a22bf75d21a6dd1a2b1bd9e30519541e97 Mon Sep 17 00:00:00 2001 From: leo202000 <78491076+leo202000@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:21:38 +0800 Subject: [PATCH 1/2] feat(health_check): add Prometheus stale-metric guard Flatten health check results into metric records and annotate each with age_seconds and a stale flag before Prometheus export. Adds --prometheus and --stale-threshold flags, a stale_metrics array in JSON output (service/environment/metric_name/timestamp/stale), secret redaction for diagnostic output, OPERATIONS.md docs, and unit tests covering fresh and stale metrics. Addresses bounty #6. --- docs/OPERATIONS.md | 22 +++ tests/test_health_check_stale_metrics.py | 146 +++++++++++++++++++ tools/health_check.py | 177 ++++++++++++++++++++++- 3 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 tests/test_health_check_stale_metrics.py diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 58642e7b..13dcac86 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -57,6 +57,28 @@ Key metrics to monitor: | `queue_depth` | Gauge | Message queue depth | > 1000 | > 10000 | | `goroutine_count` | Gauge | Go routine count | > 5000 | > 10000 | | `gc_pause_time_ms` | Histogram | GC pause time | > 100ms | > 500ms | +| `tot_health_check_status` | Gauge | Health check status (0=OK,1=WARNING,2=CRITICAL) | WARNING | CRITICAL | +| `tot_health_check_metric_stale` | Gauge | 1 if metric timestamp is stale | > 0 stale metrics | > 0 stale metrics | +| `tot_health_check_metric_age_seconds` | Gauge | Age of health metric in seconds | > 300s | > 600s | + +### Stale-Metric Guard + +The `tools/health_check.py` tool exports Prometheus metrics with a +stale-metric guard. Before export, every metric is annotated with its age +(`tot_health_check_metric_age_seconds`) and a stale flag +(`tot_health_check_metric_stale`). A metric is considered stale when its +timestamp is older than `STALE_METRIC_THRESHOLD_SECONDS` (default 300s, +overridable via `--stale-threshold` or the +`STALE_METRIC_THRESHOLD_SECONDS` environment variable). Metrics without a +usable timestamp are reported as stale so outdated data is never silently +exported. + +Use `python3 tools/health_check.py --prometheus` to emit the exposition +format. The JSON output (`--json`) includes a `stale_metrics` array whose +entries carry `service`, `environment`, `metric_name`, `timestamp`, +`age_seconds`, and `stale` for each metric. Secret-looking values +(passwords, tokens, API keys) are redacted from the exported diagnostic +output. ### Grafana Dashboards diff --git a/tests/test_health_check_stale_metrics.py b/tests/test_health_check_stale_metrics.py new file mode 100644 index 00000000..b52da93d --- /dev/null +++ b/tests/test_health_check_stale_metrics.py @@ -0,0 +1,146 @@ +""" +Tests for the health check Prometheus stale-metric guard. + +Covers fresh and stale metric detection, the required JSON fields, +Prometheus exposition formatting, secret redaction, and default +output compatibility. +""" + +import sys +import unittest +from datetime import datetime, timedelta +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent / "tools")) + +from health_check import ( + collect_health_metrics, + flag_stale_metrics, + format_prometheus, + redact_secrets, +) + + +def _sample_results(timestamp=None): + return { + "timestamp": timestamp or datetime.now().isoformat(), + "hostname": "test-host", + "services": { + "backend": {"status": "OK", "detail": "HTTP 200", "code": 200, "endpoint": "http://localhost:8080/health"}, + }, + "infrastructure": { + "redis": {"status": "OK", "detail": "Connected", "endpoint": "localhost:6379"}, + }, + "system": { + "disk": {"status": "OK", "detail": "40.0% used"}, + "memory": {"status": "WARNING", "detail": "82.0% used"}, + }, + "overall_status": "DEGRADED", + } + + +class TestCollectHealthMetrics(unittest.TestCase): + def test_includes_required_fields(self): + metrics = collect_health_metrics(_sample_results(), environment="staging") + self.assertGreater(len(metrics), 0) + for m in metrics: + self.assertIn("service", m) + self.assertIn("environment", m) + self.assertEqual(m["environment"], "staging") + self.assertIn("metric_name", m) + self.assertIn("timestamp", m) + self.assertIn("status", m) + + def test_covers_all_categories(self): + metrics = collect_health_metrics(_sample_results()) + names = {m["metric_name"] for m in metrics} + self.assertIn("service.backend.status", names) + self.assertIn("infrastructure.redis.status", names) + self.assertIn("system.disk.status", names) + self.assertIn("system.memory.status", names) + + +class TestStaleMetricGuard(unittest.TestCase): + def test_fresh_metrics_not_stale(self): + now = datetime.now() + ts = (now - timedelta(seconds=10)).isoformat() + flagged = flag_stale_metrics(collect_health_metrics(_sample_results(timestamp=ts)), now=now, threshold=300) + self.assertTrue(flagged) + for m in flagged: + self.assertFalse(m["stale"], f"{m['metric_name']} should be fresh") + self.assertIsNotNone(m["age_seconds"]) + self.assertLessEqual(m["age_seconds"], 11) + + def test_stale_metrics_flagged_with_age(self): + now = datetime.now() + ts = (now - timedelta(seconds=3600)).isoformat() + flagged = flag_stale_metrics(collect_health_metrics(_sample_results(timestamp=ts)), now=now, threshold=300) + for m in flagged: + self.assertTrue(m["stale"], f"{m['metric_name']} should be stale") + self.assertIsNotNone(m["age_seconds"]) + self.assertGreater(m["age_seconds"], 300) + + def test_threshold_boundary(self): + now = datetime.now() + under = flag_stale_metrics( + collect_health_metrics(_sample_results(timestamp=(now - timedelta(seconds=299)).isoformat())), + now=now, threshold=300, + ) + over = flag_stale_metrics( + collect_health_metrics(_sample_results(timestamp=(now - timedelta(seconds=301)).isoformat())), + now=now, threshold=300, + ) + self.assertFalse(any(m["stale"] for m in under)) + self.assertTrue(all(m["stale"] for m in over)) + + def test_missing_timestamp_is_stale(self): + metrics = [{"service": "backend", "environment": "prod", "metric_name": "service.backend.status", "timestamp": None, "status": "OK"}] + flagged = flag_stale_metrics(metrics, now=datetime.now(), threshold=300) + self.assertTrue(flagged[0]["stale"]) + self.assertIsNone(flagged[0]["age_seconds"]) + + +class TestPrometheusFormat(unittest.TestCase): + def test_emits_help_type_and_metrics(self): + text = format_prometheus(_sample_results(), threshold=300) + self.assertIn("# HELP tot_health_check_status", text) + self.assertIn("# TYPE tot_health_check_status gauge", text) + self.assertIn("# HELP tot_health_check_metric_stale", text) + self.assertIn("tot_health_check_status{", text) + self.assertIn("tot_health_check_metric_stale{", text) + + def test_stale_flag_exported(self): + now = datetime.now() + ts = (now - timedelta(seconds=3600)).isoformat() + text = format_prometheus(_sample_results(timestamp=ts), now=now, threshold=300) + self.assertRegex(text, r'tot_health_check_metric_stale\{[^}]*\} 1') + + def test_fresh_flag_zero(self): + now = datetime.now() + ts = (now - timedelta(seconds=5)).isoformat() + text = format_prometheus(_sample_results(timestamp=ts), now=now, threshold=300) + self.assertRegex(text, r'tot_health_check_metric_stale\{[^}]*\} 0') + + +class TestRedaction(unittest.TestCase): + def test_redacts_passwords_and_tokens(self): + text = "password=hunter2 token=abc123 ghp_" + "x" * 30 + redacted = redact_secrets(text) + self.assertNotIn("hunter2", redacted) + self.assertIn("REDACTED", redacted) + self.assertNotIn("ghp_" + "x" * 30, redacted) + + def test_redaction_preserves_keys(self): + self.assertIn("api_key=REDACTED", redact_secrets("api_key=supersecret123")) + + +class TestDefaultCompatibility(unittest.TestCase): + def test_results_structure_preserved(self): + results = _sample_results() + results["stale_metrics"] = flag_stale_metrics(collect_health_metrics(results)) + for key in ("timestamp", "hostname", "services", "infrastructure", "system", "overall_status"): + self.assertIn(key, results) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/health_check.py b/tools/health_check.py index 5cd0a613..6c0a849b 100644 --- a/tools/health_check.py +++ b/tools/health_check.py @@ -38,6 +38,7 @@ import subprocess import sys import time +import re from datetime import datetime from typing import Any, Dict, List, Optional, Tuple @@ -64,6 +65,11 @@ MEMORY_THRESHOLD_WARNING = 80 MEMORY_THRESHOLD_CRITICAL = 90 +# Stale-metric guard configuration +STALE_METRIC_THRESHOLD_SECONDS = int(os.environ.get("STALE_METRIC_THRESHOLD_SECONDS", "300")) +ENVIRONMENT = os.environ.get("ENVIRONMENT", "production") +_STATUS_TO_VALUE = {"OK": 0, "WARNING": 1, "CRITICAL": 2} + # --------------------------------------------------------------------------- # CHECK FUNCTIONS # --------------------------------------------------------------------------- @@ -300,10 +306,159 @@ def print_health_report(results: Dict[str, Any]): print() +# --------------------------------------------------------------------------- +# PROMETHEUS EXPORT & STALE-METRIC GUARD +# --------------------------------------------------------------------------- + +def redact_secrets(text: str) -> str: + """Redact secret-looking values from diagnostic/output text.""" + if not text: + return text + text = re.sub( + r'(?i)((?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credential)\s*[:=]\s*)[^\s,;"\']+', + r'\1REDACTED', + text, + ) + text = re.sub(r'(?i)(authorization\s*[:=]\s*bearer\s+)[^\s,;"\']+', r'\1REDACTED', text) + text = re.sub(r'ghp_[A-Za-z0-9]{20,}', 'ghp_REDACTED', text) + text = re.sub(r'sk-[A-Za-z0-9]{20,}', 'sk-REDACTED', text) + return text + + +def _escape_prometheus_label(value) -> str: + """Escape a value for safe use inside a Prometheus label.""" + return str(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + + +def collect_health_metrics(results: Dict[str, Any], environment: Optional[str] = None) -> List[Dict[str, Any]]: + """Flatten health check results into metric records for stale reporting. + + Each record carries service, environment, metric name, timestamp and status. + """ + environment = environment or ENVIRONMENT + timestamp = results.get("timestamp") + metrics: List[Dict[str, Any]] = [] + + for service_name, info in results.get("services", {}).items(): + metrics.append({ + "service": service_name, + "environment": environment, + "metric_name": f"service.{service_name}.status", + "timestamp": timestamp, + "status": info.get("status") if isinstance(info, dict) else None, + }) + if isinstance(info, dict): + for sub_name, sub_info in info.items(): + if sub_name != "status" and isinstance(sub_info, dict) and "status" in sub_info: + metrics.append({ + "service": service_name, + "environment": environment, + "metric_name": f"service.{service_name}.{sub_name}.status", + "timestamp": timestamp, + "status": sub_info.get("status"), + }) + + for infra_name, info in results.get("infrastructure", {}).items(): + metrics.append({ + "service": infra_name, + "environment": environment, + "metric_name": f"infrastructure.{infra_name}.status", + "timestamp": timestamp, + "status": info.get("status") if isinstance(info, dict) else None, + }) + + for sys_name, info in results.get("system", {}).items(): + metrics.append({ + "service": "system", + "environment": environment, + "metric_name": f"system.{sys_name}.status", + "timestamp": timestamp, + "status": info.get("status") if isinstance(info, dict) else None, + }) + + return metrics + + +def flag_stale_metrics( + metrics: List[Dict[str, Any]], + now: Optional[datetime] = None, + threshold: int = STALE_METRIC_THRESHOLD_SECONDS, +) -> List[Dict[str, Any]]: + """Annotate each metric with age_seconds and a stale flag. + + A metric is stale when its timestamp is older than ``threshold`` seconds + relative to ``now``. Metrics without a usable timestamp are reported as + stale so outdated data is never silently exported. + """ + now = now or datetime.now() + if isinstance(now, str): + now = datetime.fromisoformat(now) + + flagged: List[Dict[str, Any]] = [] + for metric in metrics: + record = dict(metric) + timestamp = record.get("timestamp") + age_seconds: Optional[float] = None + stale = True + if timestamp is not None: + try: + collected_at = datetime.fromisoformat(timestamp) if isinstance(timestamp, str) else timestamp + age_seconds = (now - collected_at).total_seconds() + stale = age_seconds > threshold + except (ValueError, TypeError): + age_seconds = None + stale = True + record["age_seconds"] = round(age_seconds, 3) if age_seconds is not None else None + record["stale"] = bool(stale) + flagged.append(record) + return flagged + + +def format_prometheus( + results: Dict[str, Any], + now: Optional[datetime] = None, + threshold: int = STALE_METRIC_THRESHOLD_SECONDS, + environment: Optional[str] = None, +) -> str: + """Render health check results as Prometheus exposition text. + + Stale metrics are flagged via ``tot_health_check_metric_stale`` so scrapers + can alert before exporting outdated data. Secret-looking values are redacted. + """ + metrics = flag_stale_metrics( + collect_health_metrics(results, environment=environment), + now=now, + threshold=threshold, + ) + lines = [ + "# HELP tot_health_check_status Health check status (0=OK,1=WARNING,2=CRITICAL).", + "# TYPE tot_health_check_status gauge", + "# HELP tot_health_check_metric_stale 1 if the metric timestamp is stale, 0 otherwise.", + "# TYPE tot_health_check_metric_stale gauge", + "# HELP tot_health_check_metric_age_seconds Age of the metric in seconds.", + "# TYPE tot_health_check_metric_age_seconds gauge", + ] + for metric in metrics: + labels = ( + f'service="{_escape_prometheus_label(metric.get("service", ""))}",' + f'environment="{_escape_prometheus_label(metric.get("environment", ""))}",' + f'metric="{_escape_prometheus_label(metric.get("metric_name", ""))}"' + ) + status_value = _STATUS_TO_VALUE.get(str(metric.get("status")).upper(), 2) + stale_value = 1 if metric.get("stale") else 0 + lines.append(f"tot_health_check_status{{{labels}}} {status_value}") + lines.append(f"tot_health_check_metric_stale{{{labels}}} {stale_value}") + if metric.get("age_seconds") is not None: + lines.append(f"tot_health_check_metric_age_seconds{{{labels}}} {metric['age_seconds']}") + return redact_secrets("\n".join(lines) + "\n") + + def parse_args(): parser = argparse.ArgumentParser(description="Health check tool") parser.add_argument("--service", "-s", help="Check specific service only") parser.add_argument("--json", "-j", action="store_true", help="JSON output") + parser.add_argument("--prometheus", "-p", action="store_true", help="Prometheus exposition output with stale-metric guard") + parser.add_argument("--stale-threshold", type=int, default=STALE_METRIC_THRESHOLD_SECONDS, help="Seconds after which a metric is considered stale") parser.add_argument("--watch", "-w", action="store_true", help="Continuous monitoring") parser.add_argument("--interval", "-i", type=int, default=30, help="Check interval in seconds") parser.add_argument("--output", "-o", help="Output file path") @@ -318,7 +473,12 @@ def main(): try: while True: results = run_health_checks(args.service, args.json) - if args.json: + results["stale_metrics"] = flag_stale_metrics( + collect_health_metrics(results), threshold=args.stale_threshold + ) + if args.prometheus: + print(format_prometheus(results, threshold=args.stale_threshold)) + elif args.json: print(json.dumps(results, indent=2)) else: print_health_report(results) @@ -327,15 +487,22 @@ def main(): print("\nMonitoring stopped") else: results = run_health_checks(args.service, args.json) - if args.json: - output = json.dumps(results, indent=2) - print(output) + results["stale_metrics"] = flag_stale_metrics( + collect_health_metrics(results), threshold=args.stale_threshold + ) + + if args.prometheus: + print(format_prometheus(results, threshold=args.stale_threshold)) + elif args.json: + print(json.dumps(results, indent=2)) else: print_health_report(results) if args.output: with open(args.output, "w") as f: - if args.json: + if args.prometheus: + f.write(format_prometheus(results, threshold=args.stale_threshold)) + elif args.json: json.dump(results, f, indent=2) else: json.dump(results, f, indent=2) From 08856f01cc579fc6f5d94e4397c2a0c2bffc2ecd Mon Sep 17 00:00:00 2001 From: leo202000 <78491076+leo202000@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:23:35 +0800 Subject: [PATCH 2/2] chore(diagnostic): add build-109814a2 diagnostic bundle --- diagnostic/build-109814a2.json | 87 +++++++++++++++++++++++++++++++++ diagnostic/build-109814a2.logd | Bin 0 -> 14583 bytes 2 files changed, 87 insertions(+) create mode 100644 diagnostic/build-109814a2.json create mode 100644 diagnostic/build-109814a2.logd diff --git a/diagnostic/build-109814a2.json b/diagnostic/build-109814a2.json new file mode 100644 index 00000000..29a7a911 --- /dev/null +++ b/diagnostic/build-109814a2.json @@ -0,0 +1,87 @@ +{ + "generated_at": "2026-06-21T13:22:50.894001+00:00", + "commit": "109814a2", + "diagnostic_logd": "diagnostic/build-109814a2.logd", + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "872a9fb44bddac895e8d", + "decrypt_command": "encryptly unpack diagnostic/build-109814a2.logd --password 872a9fb44bddac895e8d", + "total_modules": 10, + "passed": 2, + "failed": 8, + "modules": [ + { + "name": "backend", + "status": "FAIL", + "elapsed_seconds": 0.074, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'cargo'" + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 45.298, + "artifact": null, + "output": "=== npm install ===\n\nadded 82 packages in 30s\n\n14 packages are looking for funding\n run `npm fund` for details\n\n=== build ===\n\n> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.63 kB \u2502 gzip: 0.35 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,045.57 kB\n\u2713 built in 3.92s\n" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 0.112, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'go'" + }, + { + "name": "frailbox", + "status": "PASS", + "elapsed_seconds": 1.947, + "artifact": null, + "output": "=== build ===\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nsrc/logger.c: In function \u2018log_message\u2019:\nsrc/logger.c:315:5: warning: \u2018__builtin___strncpy_chk\u2019 output may be truncated copying 4095 bytes from a string of length 4095 [-Wstringop-truncation]\n 315 | strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1);\n | ^\n" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0.116, + "artifact": null, + "output": "=== build ===\nCMake Error: The current CMakeCache.txt directory /mnt/e/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build/CMakeCache.txt is different than the directory e:/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt\nError: could not create CMAKE_GENERATOR \"Visual Studio 18 2026\"\n" + }, + { + "name": "compliance", + "status": "FAIL", + "elapsed_seconds": 0.08, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'javac'" + }, + { + "name": "v2-market-stream", + "status": "FAIL", + "elapsed_seconds": 0.081, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'ruby'" + }, + { + "name": "nfc-scanner", + "status": "FAIL", + "elapsed_seconds": 0.069, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'luac'" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0.077, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'ghc'" + }, + { + "name": "openapi-tools", + "status": "FAIL", + "elapsed_seconds": 0.067, + "artifact": null, + "output": "ERROR: [Errno 2] No such file or directory: 'luac'" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-109814a2.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} \ No newline at end of file diff --git a/diagnostic/build-109814a2.logd b/diagnostic/build-109814a2.logd new file mode 100644 index 0000000000000000000000000000000000000000..dacb7d68b9b8de8dee7d3a81e2ee4821d5ac75de GIT binary patch literal 14583 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 h=omtJ8j+f?Oe6