diff --git a/.gitignore b/.gitignore index 3644358..d3296ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ sample* .rewrite/ .venv/ config.test.json + +# local secrets-bearing test config (do not commit) +anotherconfig.test.json diff --git a/src/frontend/exec.zig b/src/frontend/exec.zig index dc61ab9..f985185 100644 --- a/src/frontend/exec.zig +++ b/src/frontend/exec.zig @@ -197,6 +197,52 @@ pub fn policiesActiveFor(registry: *policy.Registry, signal: service_mod.Signal) }; } +/// Mirror the active snapshot's per-signal policy counts into the gauge. Called +/// at scrape time so the gauge always reflects the live snapshot without hooking +/// the loader's reload path. Counts match `policiesActiveFor` (the fast-path +/// gate), so a 0 here is exactly when that signal is raw-forwarded untouched. +pub fn refreshPolicyGauge(ctx: *SharedCtx) void { + const metrics = ctx.metrics orelse return; + const snapshot = ctx.registry.getSnapshot(); + metrics.setPoliciesLoaded(.log, if (snapshot) |s| @intCast(s.getLogTargetIndices().len) else 0); + metrics.setPoliciesLoaded(.metric, if (snapshot) |s| @intCast(s.getMetricTargetIndices().len) else 0); + metrics.setPoliciesLoaded(.trace, if (snapshot) |s| @intCast(s.trace_target_indices.len) else 0); +} + +/// Dump the loaded policies in the active snapshot, for the `/_edge/policies` +/// debug endpoint. `json=true` emits the full policy tree (match rules, keep, +/// transform — via the proto type's own jsonStringify); otherwise a one-line +/// text summary per policy (id, signal, enabled, name). +pub fn writePolicies(registry: *policy.Registry, w: *std.Io.Writer, json: bool) !void { + const snapshot = registry.getSnapshot(); + if (json) { + // std.json's default struct serializer over the proto types. Repeated + // fields surface as {"items":[...],"capacity":N} (raw ArrayList) — fine + // for a debug dump; the policy data is all there. + try w.print("{{\"version\":{d},\"policies\":", .{if (snapshot) |s| s.version else 0}); + try std.json.Stringify.value(if (snapshot) |s| s.policies else &.{}, .{}, w); + try w.writeAll("}\n"); + return; + } + const s = snapshot orelse { + try w.writeAll("# no policy snapshot loaded (0 policies)\n"); + return; + }; + try w.print("# snapshot version={d} policies={d} (log={d} metric={d} trace={d})\n", .{ + s.version, + s.policies.len, + s.getLogTargetIndices().len, + s.getMetricTargetIndices().len, + s.trace_target_indices.len, + }); + for (s.policies) |*p| { + const signal = if (p.target) |t| @tagName(std.meta.activeTag(t)) else "none"; + try w.print("id={s} signal={s} enabled={} name={s}\n", .{ + p.id, signal, p.enabled, p.name, + }); + } +} + pub const BufferedResult = struct { body: []const u8, all_dropped: bool, diff --git a/src/frontend/httpz/server.zig b/src/frontend/httpz/server.zig index 6efb0da..ed7c1b1 100644 --- a/src/frontend/httpz/server.zig +++ b/src/frontend/httpz/server.zig @@ -246,10 +246,20 @@ pub const Handler = struct { // with the stdio driver's /_edge/metrics short-circuit. if (req.method == .GET and std.mem.eql(u8, path, "/_edge/metrics")) { res.header("content-type", "text/plain; version=0.0.4"); + exec.refreshPolicyGauge(ctx); if (ctx.metrics) |metrics| try metrics.writePrometheus(res.writer()); return; } + // Dump the loaded policy snapshot (id/signal/enabled/name). Pairs with + // the gauge: shows *which* policies are active, not just how many. + if (req.method == .GET and std.mem.eql(u8, path, "/_edge/policies")) { + const json = std.mem.eql(u8, (try req.query()).get("format") orelse "", "json"); + res.header("content-type", if (json) "application/json" else "text/plain; charset=utf-8"); + try exec.writePolicies(ctx.registry, res.writer(), json); + return; + } + // Debug tap (config-gated): block this request up to 1s while data-plane // threads stream the next N records into our buffer, before or after // policy evaluation. ctx.tap is null unless enabled in config. diff --git a/src/runtime/runtime_metrics.zig b/src/runtime/runtime_metrics.zig index 6a10a01..da8d155 100644 --- a/src/runtime/runtime_metrics.zig +++ b/src/runtime/runtime_metrics.zig @@ -97,6 +97,12 @@ const PolicyLabels = struct { telemetry: PolicyTelemetryLabel, }; +pub const SignalLabel = enum { log, metric, trace }; + +const PolicySignalLabels = struct { + signal: SignalLabel, +}; + const BuildInfoLabels = struct { version: []const u8, commit: []const u8, @@ -112,6 +118,7 @@ const InternalMetrics = struct { edge_policy_records_evaluated_total: PolicyRecordsEvaluatedTotal, edge_policy_records_kept_total: PolicyRecordsKeptTotal, edge_policy_records_dropped_total: PolicyRecordsDroppedTotal, + edge_policies_loaded: PoliciesLoaded, edge_build_info: BuildInfo, const RequestsTotal = m.CounterVec(u64, RequestLabels); @@ -126,6 +133,7 @@ const InternalMetrics = struct { const PolicyRecordsEvaluatedTotal = m.CounterVec(u64, PolicyLabels); const PolicyRecordsKeptTotal = m.CounterVec(u64, PolicyLabels); const PolicyRecordsDroppedTotal = m.CounterVec(u64, PolicyLabels); + const PoliciesLoaded = m.GaugeVec(u64, PolicySignalLabels); const BuildInfo = m.GaugeVec(u64, BuildInfoLabels); }; @@ -195,6 +203,13 @@ pub const RuntimeMetrics = struct { .{ .help = "Total number of telemetry records dropped after policy evaluation." }, .{}, ), + .edge_policies_loaded = try InternalMetrics.PoliciesLoaded.init( + allocator, + io, + "edge_policies_loaded", + .{ .help = "Number of loaded policies targeting each signal in the active snapshot." }, + .{}, + ), .edge_build_info = try InternalMetrics.BuildInfo.init( allocator, io, @@ -247,6 +262,10 @@ pub const RuntimeMetrics = struct { try self.internal.edge_policy_records_kept_total.incrBy(.{ .telemetry = telemetry }, 0); try self.internal.edge_policy_records_dropped_total.incrBy(.{ .telemetry = telemetry }, 0); } + + inline for (std.meta.tags(SignalLabel)) |signal| { + try self.internal.edge_policies_loaded.set(.{ .signal = signal }, 0); + } } pub fn deinit(self: *RuntimeMetrics) void { @@ -335,6 +354,11 @@ pub const RuntimeMetrics = struct { }, dropped_count) catch |err| log.debug("failed to record policy dropped metric: {}", .{err}); } + pub fn setPoliciesLoaded(self: *RuntimeMetrics, signal: SignalLabel, count: u64) void { + self.internal.edge_policies_loaded.set(.{ .signal = signal }, count) catch |err| + log.warn("failed to set policies loaded metric: {}", .{err}); + } + pub fn setBuildInfo(self: *RuntimeMetrics, version: []const u8, commit: []const u8) void { self.internal.edge_build_info.set(.{ .version = version,