Phase 3 gave us unspoofable cgroup-based agent identity, resource limits, and time-based access grants. But all output went to stderr via env_logger — fine for a terminal, useless for production:
Problem 1: No structured logs. The [ALLOW] / [DENY] log lines are human-readable but machine-unfriendly. SIEMs, log aggregators, and dashboards can't parse them without custom regex. A single missed regex change breaks the pipeline.
Problem 2: No real-time notifications. If an agent is blocked from reading /etc/shadow at 3 AM, nobody knows until someone reads the logs the next day. There's no way to send an alert to Slack, email, or a webhook when a policy violation occurs.
Problem 3: No observability metrics. Questions like "how many file access events per minute?", "how many policy violations today?", or "is the daemon even running?" require manually parsing logs. There's no Prometheus endpoint for dashboards or alerting rules.
Problem 4: No alert hygiene. A single misconfigured agent can generate thousands of identical DENY events per minute (e.g., a polling loop hitting a denied path). Without deduplication or rate limiting, any notification system would be overwhelmed.
Phase 4 solves all four by adding a complete alerting and observability layer: structured JSON logging, webhook/Slack/email notifications, Prometheus metrics, and built-in dedup/throttling — all configured via the same TOML config file.
| File | Lines | Purpose |
|---|---|---|
guardian/src/alerting/mod.rs |
427 | AlertManager, AlertSender, AlertEvent types, dedup/throttle, dispatch |
guardian/src/alerting/json_log.rs |
195 | SIEM-compatible JSONL file logger with size-based rotation |
guardian/src/alerting/webhook.rs |
86 | Generic HTTP POST to any endpoint with auth headers |
guardian/src/alerting/slack.rs |
111 | Slack Block Kit formatted messages with severity colors |
guardian/src/alerting/email.rs |
109 | Async SMTP email via lettre with STARTTLS |
guardian/src/alerting/metrics.rs |
164 | Prometheus counters + lightweight TCP HTTP server |
configs/minimal.toml |
— | Preset: bare minimum, monitor-only |
configs/recommended.toml |
— | Preset: production defaults with JSON + Prometheus |
configs/strict.toml |
— | Preset: maximum security, all outputs |
configs/development.toml |
— | Preset: verbose, permissive, stdout JSON |
| File | What Changed |
|---|---|
guardian/Cargo.toml |
Added reqwest (HTTP), lettre (SMTP), chrono (timestamps), prometheus (metrics) |
guardian/src/config.rs |
Added AlertingConfig, JsonLogConfig, WebhookConfig, SlackConfig, EmailConfig, PrometheusConfig structs. Added validate_alerting_config() with URL/severity/required-field checks. Made alerting an optional field in Config. |
guardian/src/main.rs |
Added mod alerting. Integrated AlertSender into event processing pipeline. Added --validate-config CLI flag. Added SIGHUP handler for config hot-reload. Updated step numbering (6→13 steps). |
config.toml |
Added complete [alerting] section with all output examples (JSON log + Prometheus enabled, webhook/Slack/email commented). |
eBPF (KERNEL)
┌─────────────────────────────────────────────────────────────────────┐
│ sys_enter_openat ──► capture event ──► perf buffer (per-CPU) │
│ sys_enter_execve ──► capture event ──► perf buffer (per-CPU) │
└────────────────────────────────────────┬────────────────────────────┘
│
│ async read
▼
USERSPACE (per-CPU tokio tasks)
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ process_file_event() / process_exec_event() │
│ │ │
│ ├──► env_logger (existing stderr output, unchanged) │
│ │ [ALLOW] / [DENY|MONITOR] / [BLOCKED|ENFORCE] │
│ │ │
│ ├──► AlertSender.send() │
│ │ │ │
│ │ ├──► Prometheus counters (synchronous, atomic) │
│ │ │ guardian_file_events_total{agent, action} │
│ │ │ guardian_exec_events_total{agent, action} │
│ │ │ │
│ │ └──► mpsc::channel (4096 buffer, non-blocking) │
│ │ │ │
│ │ ▼ │
│ │ AlertManager (tokio task) │
│ │ │ │
│ │ ├──► Severity filter (global min_severity) │
│ │ ├──► Dedup filter (hash-based, configurable window) │
│ │ ├──► Rate limiter (per-minute cap) │
│ │ │ │
│ │ ├──► JSON Log (JSONL file, size-based rotation) │
│ │ ├──► Webhook (HTTP POST, JSON payload) │
│ │ ├──► Slack (Block Kit, colored sidebar) │
│ │ └──► Email (SMTP/STARTTLS, plain text) │
│ │ │
│ └──► events_lost counter (on perf buffer overflow) │
│ │
│ Prometheus HTTP Server (separate tokio task) │
│ GET /metrics ──► prometheus::TextEncoder ──► HTTP 200 │
│ │
│ SIGHUP Handler (separate tokio task) │
│ SIGHUP ──► reload config.toml ──► update IpcState.config │
│ │
└─────────────────────────────────────────────────────────────────────┘
Synchronous metrics, async alerts. Prometheus counters are updated directly in the event processors using atomic operations — no channel involved. This means metrics are always accurate, even if the alert channel is full or the AlertManager is busy sending a slow webhook. Alert delivery (JSON log, webhook, Slack, email) happens asynchronously in a separate task so it never blocks event processing.
Non-blocking channel with backpressure. The mpsc::channel(4096) buffer absorbs bursts. try_send() drops the event (and increments alerts_dropped) rather than blocking. This is the correct trade-off: a slow webhook endpoint should never slow down eBPF event processing.
Single AlertManager task. All dedup/throttle state lives in one task. No locking needed. The channel serializes events naturally. Cleanup runs on a 60-second interval via tokio::select!.
When a file access event occurs, here is the complete flow:
In process_file_event() (guardian/src/main.rs), after evaluating the policy:
let (severity, action) = if allowed {
(Severity::Info, Action::Allow)
} else if enforce_mode {
(Severity::Critical, Action::Blocked)
} else {
(Severity::Warning, Action::Deny)
};
alert_tx.send(AlertEvent {
timestamp: chrono::Utc::now(),
severity,
event_type: EventType::FileAccess,
action,
agent_name: agent.name.clone(),
pid: event.tgid,
comm: comm.to_string(),
path: filename.to_string(),
access_mode: access_mode.clone(),
identity_method: agent.effective_identity().to_string(),
policy_mode: mode_tag.to_lowercase(),
});The severity mapping is:
| Condition | Severity | Action |
|---|---|---|
| File access allowed | Info |
Allow |
| File access denied, monitor mode | Warning |
Deny |
| File access blocked, enforce mode | Critical |
Blocked |
| Exec allowed | Info |
Allow |
| Exec denied | Warning |
Deny |
impl AlertSender {
pub fn send(&self, event: AlertEvent) {
// Synchronous: always runs, even if channel is full
self.metrics.record_event(&event);
// Async: drops event if channel buffer (4096) is full
if self.tx.try_send(event).is_err() {
self.metrics.alerts_dropped.inc();
}
}
}The AlertManager task runs a tokio::select! loop:
async fn run(&mut self, mut rx: mpsc::Receiver<AlertEvent>) {
let mut cleanup_interval = tokio::time::interval(Duration::from_secs(60));
loop {
tokio::select! {
Some(event) = rx.recv() => {
self.process_event(event).await;
}
_ = cleanup_interval.tick() => {
self.cleanup_dedup_cache();
}
}
}
}For each event, three filters are applied in order:
Filter 1: Global severity threshold. If the event's severity is below min_severity (default: "warning"), it's dropped. This means Info events (allowed accesses) are not alerted by default — they're only in Prometheus counters.
Filter 2: Deduplication. A hash is computed from (agent_name, event_type, path, action). If the same hash was seen within dedup_window_seconds (default: 300), the event is suppressed. This prevents alert storms when an agent polls a denied path in a loop.
fn dedup_key(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.agent_name.hash(&mut hasher);
self.event_type.hash(&mut hasher);
self.path.hash(&mut hasher);
self.action.hash(&mut hasher);
hasher.finish()
}Filter 3: Rate limiting. A sliding 1-minute window caps total alerts to rate_limit_per_minute (default: 100). If the cap is hit, remaining events in that minute are dropped. The window resets every 60 seconds.
Each output has its own severity filter (independent of the global filter). The dispatch order is:
- JSON Log — writes to file (or stdout), always processes all events that pass the global filter
- Webhook — HTTP POST if
event.severity >= webhook.min_severity - Slack — Slack webhook if
event.severity >= slack.min_severity - Email — SMTP send if
event.severity >= email.min_severity
Each output's success/failure is tracked in the guardian_alerts_sent_total{output, status} Prometheus counter.
Each line is a self-contained JSON object. This format is called JSONL (JSON Lines) and is the standard for SIEM ingestion (Elasticsearch, Splunk, Loki, etc.).
{"timestamp":"2026-03-10T14:30:00.123456Z","severity":"critical","event_type":"file_access","action":"blocked","agent":{"name":"claude-code","identity":"cgroup","pid":12345,"comm":"cat"},"file":{"path":"/etc/shadow","flags":"READ"},"policy":{"mode":"enforce"},"host":{"hostname":"myhost"}}Pretty-printed for readability:
{
"timestamp": "2026-03-10T14:30:00.123456Z",
"severity": "critical",
"event_type": "file_access",
"action": "blocked",
"agent": {
"name": "claude-code",
"identity": "cgroup",
"pid": 12345,
"comm": "cat"
},
"file": {
"path": "/etc/shadow",
"flags": "READ"
},
"policy": {
"mode": "enforce"
},
"host": {
"hostname": "myhost"
}
}Field reference:
| Field | Type | Description |
|---|---|---|
timestamp |
ISO 8601 | UTC timestamp with microsecond precision |
severity |
string | "info", "warning", or "critical" |
event_type |
string | "file_access" or "exec_attempt" |
action |
string | "allow", "deny", or "blocked" |
agent.name |
string | Agent name from config |
agent.identity |
string | "comm" or "cgroup" |
agent.pid |
integer | Process TGID |
agent.comm |
string | Process comm name (from /proc/PID/comm) |
file.path |
string | File path from the openat() syscall |
file.flags |
string | Open flags: "READ", `"WRITE |
policy.mode |
string | "enforce" or "monitor" |
host.hostname |
string | Machine hostname from /etc/hostname |
Log rotation: When the file exceeds max_size_mb (default: 100 MB), the logger rotates:
events.json→events.json.1events.json.1→events.json.2- ... up to
max_files(default: 5) - New
events.jsonis created empty
HTTP POST with Content-Type: application/json:
{
"version": "1.0",
"source": "guardian-shell",
"timestamp": "2026-03-10T14:30:00.123456Z",
"hostname": "myhost",
"severity": "critical",
"event_type": "file_access",
"action": "blocked",
"agent_name": "claude-code",
"pid": 12345,
"comm": "cat",
"path": "/etc/shadow",
"access_mode": "READ",
"identity_method": "cgroup",
"policy_mode": "enforce"
}Optional headers:
Authorizationheader viaauth_headerconfig field- Custom headers via
headersmap (e.g.,X-Source = "guardian-shell")
Uses Slack's Block Kit for rich formatting with a color-coded sidebar:
- Red (
#dc3545) forcriticalseverity - Yellow (
#ffc107) forwarningseverity - Blue (
#17a2b8) forinfoseverity
The message includes a header with severity emoji, a section with agent/event/path/PID fields, and a context bar with hostname, mode, identity method, and timestamp.
Plain-text email with structured fields:
Subject: [Guardian Shell] CRITICAL — BLOCKED on /etc/shadow (agent: claude-code)
Guardian Shell Security Alert
================================
Severity: critical
Event: file_access
Action: blocked
Timestamp: 2026-03-10T14:30:00Z
Agent: claude-code
PID: 12345 (cat)
Identity: cgroup
Path: /etc/shadow
Flags: READ
Mode: enforce
Host: myhost
---
This alert was generated by Guardian Shell.
Uses SMTP with STARTTLS on the configured port (default: 587). Supports username/password authentication.
| Metric | Type | Labels | Description |
|---|---|---|---|
guardian_guardian_file_events_total |
Counter | agent, action |
Total file access events observed |
guardian_guardian_exec_events_total |
Counter | agent, action |
Total exec attempt events observed |
guardian_guardian_ebpf_events_lost_total |
Counter | — | Total eBPF events lost due to full perf buffer |
guardian_guardian_alerts_sent_total |
Counter | output, status |
Total alerts sent to each output destination |
guardian_guardian_alerts_dropped_total |
Counter | — | Total alerts dropped due to full channel |
Metrics are updated at two levels:
-
Event processors (synchronous, per-CPU tasks):
file_events_totalandexec_events_totalare incremented viaAlertSender.send()before the event enters the channel. This means counters are accurate even if the AlertManager drops events. -
AlertManager (async, single task):
alerts_sent_totalis incremented after each output dispatch attempt, withstatus="success"orstatus="error". -
Perf buffer readers:
events_lostis incremented when the kernel reports dropped events from the perf ring buffer.
A lightweight TCP server (no framework dependency) listens on the configured address (default: 127.0.0.1:9090). It handles GET requests to the configured endpoint (default: /metrics) and responds with Prometheus text exposition format.
$ curl http://127.0.0.1:9090/metrics
# HELP guardian_guardian_file_events_total Total file access events observed
# TYPE guardian_guardian_file_events_total counter
guardian_guardian_file_events_total{agent="claude-code",action="allow"} 14523
guardian_guardian_file_events_total{agent="claude-code",action="blocked"} 7
# HELP guardian_guardian_exec_events_total Total exec attempt events observed
# TYPE guardian_guardian_exec_events_total counter
guardian_guardian_exec_events_total{agent="claude-code",action="allow"} 892
# HELP guardian_guardian_ebpf_events_lost_total Total eBPF events lost due to full perf buffer
# TYPE guardian_guardian_ebpf_events_lost_total counter
guardian_guardian_ebpf_events_lost_total 0
# HELP guardian_guardian_alerts_sent_total Total alerts sent to output destinations
# TYPE guardian_guardian_alerts_sent_total counter
guardian_guardian_alerts_sent_total{output="json_log",status="success"} 7
guardian_guardian_alerts_sent_total{output="webhook",status="success"} 5
guardian_guardian_alerts_sent_total{output="webhook",status="error"} 2
# HELP guardian_guardian_alerts_dropped_total Total alerts dropped due to full channel
# TYPE guardian_guardian_alerts_dropped_total counter
guardian_guardian_alerts_dropped_total 0
Add the Prometheus endpoint as a data source in Grafana, then create dashboards:
Useful queries:
# Policy violations per minute
rate(guardian_guardian_file_events_total{action="blocked"}[5m]) * 60
# Alert delivery success rate
sum(rate(guardian_guardian_alerts_sent_total{status="success"}[5m]))
/
sum(rate(guardian_guardian_alerts_sent_total[5m]))
# Events lost ratio
rate(guardian_guardian_ebpf_events_lost_total[5m])
/
(rate(guardian_guardian_file_events_total[5m]) + rate(guardian_guardian_ebpf_events_lost_total[5m]))
Validates the configuration file and exits without starting the daemon:
$ sudo target/release/guardian --config config.toml --validate-config
[INFO guardian] Configuration is valid.
[INFO guardian] Alerting: configured
[INFO guardian] JSON log: enabled
[INFO guardian] Prometheus: enabledValidation checks include:
- TOML syntax and field types
- Mode must be
"monitor"or"enforce" - Agent identity must be
"comm"or"cgroup" - Severity values must be
"info","warning", or"critical" - Webhook URL must start with
http://orhttps://(warning) - Slack webhook URL must look like a Slack URL (warning)
- Enabled outputs must have required fields (URL, SMTP host, from/to addresses)
- Overly permissive allow patterns are flagged
This is useful for CI/CD pipelines, pre-deployment checks, and config file editing.
Reload agent policies without restarting the daemon:
$ sudo kill -HUP $(pidof guardian)On SIGHUP, the daemon:
- Reads and validates the config file
- Updates the
IpcState.config(shared with IPC handlers) - Logs the reload result
[INFO guardian] SIGHUP received — reloading configuration...
[INFO guardian] Configuration reloaded: 2 agent(s), mode=enforce
If the new config is invalid, the previous config is kept:
[ERROR guardian] Config reload failed (keeping previous config): Agent 'test': invalid default action 'invalid'. Must be 'allow' or 'deny'
What reloads: Agent file access policies, exec policies, agent list.
What does NOT reload: Alerting outputs (webhook URLs, Slack tokens, etc.), BPF maps for existing agents, enforcement mode. These require a full daemon restart because they involve persistent connections and kernel state.
[alerting]
# Global minimum severity: events below this are not alerted.
# "info" = all events, "warning" = denials only, "critical" = enforced blocks only
min_severity = "warning"
# Suppress duplicate alerts (same agent + event type + path + action)
# within this time window. Set to 0 to disable dedup.
dedup_window_seconds = 300
# Maximum alerts dispatched per minute across all outputs.
# Prevents alert storms from overwhelming downstream systems.
rate_limit_per_minute = 100
# --- Structured JSON Logging ---
[alerting.json_log]
enabled = true
path = "/var/log/guardian/events.json" # Omit for stdout
max_size_mb = 100 # Rotate when file exceeds this
max_files = 5 # Keep N rotated files
# --- Webhook (generic HTTP POST) ---
[alerting.webhook]
enabled = true
url = "https://siem.example.com/api/v1/events"
auth_header = "Bearer your-api-token"
min_severity = "warning"
# Optional custom headers
[alerting.webhook.headers]
X-Source = "guardian-shell"
X-Environment = "production"
# --- Slack ---
[alerting.slack]
enabled = true
webhook_url = "https://hooks.slack.com/services/T.../B.../xxx"
channel = "#security-alerts" # Optional channel override
min_severity = "critical"
# --- Email (SMTP) ---
[alerting.email]
enabled = true
smtp_host = "smtp.gmail.com"
smtp_port = 587 # STARTTLS
username = "alerts@example.com"
password = "app-password-here"
from = "Guardian Shell <guardian@example.com>"
to = ["security-team@example.com", "oncall@example.com"]
min_severity = "critical"
# --- Prometheus Metrics ---
[alerting.prometheus]
enabled = true
listen_address = "127.0.0.1:9090"
endpoint = "/metrics"| Level | Meaning | Typical Output |
|---|---|---|
info |
Allowed access events | JSON log only (high volume) |
warning |
Denied access in monitor mode | JSON log, webhook |
critical |
Blocked access in enforce mode | All outputs |
Each output can set its own min_severity independently. The global min_severity acts as a pre-filter before any per-output filter.
Four ready-to-use configs are in configs/:
| Preset | mode |
Alerting | Best For |
|---|---|---|---|
minimal.toml |
monitor | None | Quick testing, learning the tool |
recommended.toml |
enforce | JSON log + Prometheus | Production deployments |
strict.toml |
enforce | All outputs (commented) | Maximum security |
development.toml |
monitor | JSON to stdout + Prometheus | Debugging, development |
| Crate | Version | Features | Purpose |
|---|---|---|---|
reqwest |
0.12 | rustls-tls, json |
HTTP client for webhooks and Slack (uses rustls, no openssl) |
lettre |
0.11 | tokio1-rustls-tls, smtp-transport, builder |
Async SMTP email transport |
chrono |
0.4 | serde |
ISO 8601 timestamps with microsecond precision |
prometheus |
0.13 | — | Counter/gauge metrics with text encoding |
All TLS uses rustls — no OpenSSL system dependency required.
| Decision | Rationale |
|---|---|
| Async mpsc channel | Event processors never block on I/O. The 4096-element buffer absorbs bursts. try_send() drops rather than blocks — eBPF event processing is higher priority than alert delivery. |
| Synchronous Prometheus counters | Metrics must be accurate regardless of channel state. Atomic counter increments have negligible overhead. This is how Tetragon handles it too. |
| Per-output severity filters | Webhook might want all warnings, but Slack/email should only fire for critical events. One global filter + per-output overrides provides flexible noise control. |
| Hash-based dedup | Computing hash(agent, event_type, path, action) is O(1) and the HashMap lookup is O(1). No sorting, no windowing, no external state. Dedup cache is cleaned every 60 seconds. |
| JSONL format | One JSON object per line. No parser state between lines. Works with grep, jq, tail -f, Elasticsearch bulk API, Loki, Splunk HEC. Industry standard for structured logs. |
| Slack Block Kit | Slack's recommended formatting API. Color-coded sidebar makes severity instantly visible. Structured fields (agent, path, PID) are scannable without reading prose. |
| Size-based log rotation | Simple, no external dependency (logrotate not required). The daemon handles its own rotation. Configurable max size and max files. |
| Simple TCP metrics server | A full HTTP framework (axum) is unnecessary for serving /metrics. The lightweight TCP handler is ~40 lines. Axum will be added in Phase 5 for the dashboard. |
| SIGHUP config reload | Standard Unix daemon pattern. Used by nginx, haproxy, falco, and most production daemons. Reloads policy without disrupting active monitoring. |
| Optional alerting section | Existing Phase 3 configs work unchanged. The [alerting] table is Option<AlertingConfig> — when absent, a no-op AlertSender is used that only tracks internal metrics. |
| Preset config templates | Falco ships commented default configs. Having four presets (minimal → strict) reduces onboarding friction and shows best practices. |
--validate-config flag |
Catches config errors before deployment. Essential for CI/CD. Returns exit code 0 on success, non-zero on failure. |
| reqwest with rustls | Avoids the openssl-dev system dependency that breaks builds on minimal containers. Rustls is pure-Rust and statically linked. |
This implementation was informed by analysis of five major open-source security monitoring tools:
| Pattern | Source Project | How We Applied It |
|---|---|---|
| Per-output enable + severity filter | Falco (falco.yaml output channels) |
Each output in [alerting.*] has enabled and min_severity |
| Sidecar separation for complex routing | Falcosidekick (60+ output plugins) | Core daemon handles 5 outputs directly; complex routing deferred to Phase 5 |
| JSONL event export format | Tetragon (export-file config) |
One JSON per line with nested agent, file, policy, host objects |
| Drop-in config snippets | Tetragon (/etc/tetragon/tetragon.conf.d/) |
Preset configs in configs/ directory |
| Prometheus metrics with namespace prefix | Tetragon (port 2112, --metrics-label-filter) |
guardian_ prefix, label-based cardinality control |
| Commented default config file | Falco (ships a 200+ line falco.yaml with every option) |
config.toml has all outputs as commented examples |
| SIGHUP config reload | Falco, nginx, HAProxy | tokio::signal::unix::SignalKind::hangup() handler |
| Alert throttling/dedup | Prometheus Alertmanager (group_wait, repeat_interval) |
Hash-based dedup window + per-minute rate limit |
| Config validation subcommand | Tetragon (--generate-docs), nginx (-t flag) |
--validate-config flag |
| Block Kit formatted Slack messages | Falcosidekick Slack output | Color-coded sidebar, structured fields, context bar |
| Feature | Phase 1 | Phase 2 | Phase 3 | Phase 4 |
|---|---|---|---|---|
| Output | stderr only | stderr only | stderr only | JSON + webhook + Slack + email |
| Metrics | None | None | None | Prometheus endpoint |
| Notifications | None | None | None | Real-time (webhook, Slack, email) |
| Alert hygiene | None | None | None | Dedup + rate limiting |
| Config validation | Basic | Basic | Basic | --validate-config + URL checks |
| Hot reload | None | None | None | SIGHUP |
| Preset configs | None | None | None | 4 templates |
| Enforcement | Monitor only | Kernel blocks | Kernel blocks | Kernel blocks |
| Identity | Comm name | Comm + TGID | Cgroup (unspoofable) | Cgroup (unspoofable) |
| Resource limits | None | None | Memory/CPU/PIDs | Memory/CPU/PIDs |
All 14 existing unit tests pass without modification:
running 14 tests
test config::tests::test_effective_identity_comm ... ok
test config::tests::test_effective_identity_cgroup ... ok
test config::tests::test_exact_match ... ok
test config::tests::test_exec_policy ... ok
test config::tests::test_pattern_to_policy_rule ... ok
test config::tests::test_policy_default_allow ... ok
test config::tests::test_policy_default_deny ... ok
test config::tests::test_policy_deny_takes_precedence ... ok
test config::tests::test_recursive_wildcard ... ok
test config::tests::test_single_level_wildcard ... ok
test tests::test_comm_to_key ... ok
test tests::test_decode_open_flags_rdwr_trunc ... ok
test tests::test_decode_open_flags_read_only ... ok
test tests::test_decode_open_flags_write_create ... ok
All 5 config files pass --validate-config:
$ for f in config.toml configs/*.toml; do
echo "=== $f ==="
target/release/guardian --config "$f" --validate-config 2>&1 | grep -E "valid|enabled"
done
=== config.toml ===
Configuration is valid.
JSON log: enabled
Prometheus: enabled
=== configs/development.toml ===
Configuration is valid.
JSON log: enabled
Prometheus: enabled
=== configs/minimal.toml ===
Configuration is valid.
Alerting: not configured
=== configs/recommended.toml ===
Configuration is valid.
JSON log: enabled
Prometheus: enabled
=== configs/strict.toml ===
Configuration is valid.
JSON log: enabled
Prometheus: enabledBoth debug and release builds compile cleanly:
cargo build --package guardian # debug: OK
cargo build --package guardian --release # release: OK (1m 01s)Only pre-existing warnings from Phase 3 code (unused log_level field, unused get_cgroup_id function).