Skip to content

Commit 18b7243

Browse files
hyperpolymathclaude
andcommitted
feat: add self-health VeriSimDB Zig module to close observability loop
Add ffi/zig/src/self_health.zig with reportPipelineHealth() so verisimdb-data can report its own ingest statistics to the VeriSimDB REST API (collection: verisimdb-data:pipeline-health). This closes the pipeline observability loop for Hypatia staleness detection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8c583b3 commit 18b7243

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

ffi/zig/src/self_health.zig

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// verisimdb-data :: ffi/zig/src/self_health.zig
5+
//
6+
// Self-health persistence: verisimdb-data reporting its own ingestion
7+
// health back to the VeriSimDB REST API instance it feeds.
8+
//
9+
// This closes the observability loop:
10+
//
11+
// panic-attack → verisimdb-data (git) → VeriSimDB REST → Hypatia
12+
// ↑ |
13+
// └───── self_health report ──────┘
14+
//
15+
// When VeriSimDB REST is running (http://localhost:8080), verisimdb-data
16+
// can report its own ingest statistics (scan count, last ingest timestamp,
17+
// repo coverage) so Hypatia rules can detect pipeline staleness.
18+
//
19+
// ## Collection: verisimdb-data:pipeline-health
20+
//
21+
// ```json
22+
// {
23+
// "check_id": "vdd:1740000000000",
24+
// "timestamp": "2026-01-30T12:00:00Z",
25+
// "total_scans": 301,
26+
// "total_repos": 5,
27+
// "last_ingest": "2026-03-07T22:19:01Z",
28+
// "status": "healthy"
29+
// }
30+
// ```
31+
//
32+
// ## Usage
33+
//
34+
// Call `reportPipelineHealth` after every ingest to keep VeriSimDB REST
35+
// current. Returns an error on connectivity failure — caller should log
36+
// and continue (fail-open semantics).
37+
38+
const std = @import("std");
39+
40+
const DEFAULT_URL = "http://localhost:8080";
41+
const COLLECTION = "verisimdb-data:pipeline-health";
42+
43+
/// Pipeline health snapshot for self-reporting.
44+
pub const PipelineHealth = struct {
45+
check_id: []const u8,
46+
timestamp: []const u8,
47+
total_scans: u32,
48+
total_repos: u32,
49+
last_ingest: []const u8,
50+
status: []const u8,
51+
};
52+
53+
/// Report pipeline health to VeriSimDB REST.
54+
///
55+
/// This is called after every scan ingestion to keep the VeriSimDB
56+
/// pipeline-health collection current. Fail-open: connectivity errors
57+
/// are returned but do not abort the ingest.
58+
pub fn reportPipelineHealth(allocator: std.mem.Allocator, health: PipelineHealth) !void {
59+
const base_url = std.posix.getenv("VERISIMDB_URL") orelse DEFAULT_URL;
60+
61+
const url = try std.fmt.allocPrint(allocator, "{s}/v1/{s}/{s}", .{
62+
base_url, COLLECTION, health.check_id,
63+
});
64+
defer allocator.free(url);
65+
66+
const body = try std.fmt.allocPrint(allocator,
67+
\\{{
68+
\\ "check_id": "{s}",
69+
\\ "timestamp": "{s}",
70+
\\ "total_scans": {d},
71+
\\ "total_repos": {d},
72+
\\ "last_ingest": "{s}",
73+
\\ "status": "{s}"
74+
\\}}
75+
, .{
76+
health.check_id,
77+
health.timestamp,
78+
health.total_scans,
79+
health.total_repos,
80+
health.last_ingest,
81+
health.status,
82+
});
83+
defer allocator.free(body);
84+
85+
try httpPut(allocator, url, body);
86+
}
87+
88+
/// Generate a health check ID from the current timestamp.
89+
///
90+
/// Format: `vdd:<ts_ms>`
91+
pub fn makeCheckId(allocator: std.mem.Allocator, ts_ms: u64) ![]u8 {
92+
return std.fmt.allocPrint(allocator, "vdd:{d}", .{ts_ms});
93+
}
94+
95+
// ---------------------------------------------------------------------------
96+
// Internal helpers
97+
// ---------------------------------------------------------------------------
98+
99+
fn httpPut(allocator: std.mem.Allocator, url: []const u8, body: []const u8) !void {
100+
var client = std.http.Client{ .allocator = allocator };
101+
defer client.deinit();
102+
103+
const uri = try std.Uri.parse(url);
104+
105+
var header_buf: [4096]u8 = undefined;
106+
var req = try client.open(.PUT, uri, .{
107+
.server_header_buffer = &header_buf,
108+
.extra_headers = &.{
109+
.{ .name = "Content-Type", .value = "application/json" },
110+
},
111+
});
112+
defer req.deinit();
113+
114+
req.transfer_encoding = .{ .content_length = body.len };
115+
try req.send();
116+
try req.writeAll(body);
117+
try req.finish();
118+
try req.wait();
119+
120+
const status = req.response.status;
121+
if (@intFromEnum(status) < 200 or @intFromEnum(status) >= 300) {
122+
return error.VeriSimDbHttpError;
123+
}
124+
}

0 commit comments

Comments
 (0)