From 838c1f726bdcfa6251e5aba1133c8d3d4d07bb1e Mon Sep 17 00:00:00 2001 From: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> Date: Mon, 23 Sep 2024 23:32:16 +0200 Subject: [PATCH] fix(agent/compositeRules): exclude rules that does not have trigger alert (#308) --- .gitignore | 3 + src/agent/src/compositeRules.ts | 16 +- .../src/notifiers/compositeRules.notifier.ts | 5 +- src/agent/test/FT/compositeRules.spec.ts | 160 +----------------- .../FT/compositeRulesLocalNotifier.spec.ts | 72 ++++++++ .../FT/compositeRulesSeverityFilter.spec.ts | 93 ++++++++++ .../test/FT/compositeRulesUntriggered.spec.ts | 90 ++++++++++ .../composite-rules-local/sigyn.config.json | 83 +++++++++ src/agent/test/FT/helpers.ts | 24 +++ 9 files changed, 378 insertions(+), 168 deletions(-) create mode 100644 src/agent/test/FT/compositeRulesLocalNotifier.spec.ts create mode 100644 src/agent/test/FT/compositeRulesSeverityFilter.spec.ts create mode 100644 src/agent/test/FT/compositeRulesUntriggered.spec.ts create mode 100644 src/agent/test/FT/fixtures/composite-rules-local/sigyn.config.json diff --git a/.gitignore b/.gitignore index cd821cd4..0e455602 100644 --- a/.gitignore +++ b/.gitignore @@ -135,3 +135,6 @@ dist src/**/temp docs/.vitepress/dist docs/.vitepress/cache + +# Ignore all local SQLite files +*.sqlite3* diff --git a/src/agent/src/compositeRules.ts b/src/agent/src/compositeRules.ts index ff58ddcd..f307ba64 100644 --- a/src/agent/src/compositeRules.ts +++ b/src/agent/src/compositeRules.ts @@ -64,10 +64,10 @@ export function handleCompositeRules(logger: Logger) { } for (const compositeRule of compositeRules) { - const ruleIdsObj = getDB().prepare( - `SELECT id FROM rules WHERE name IN (${compositeRule.rules.map(() => "?").join(",")})` - ).all(compositeRule.rules) as { id: number }[]; - const ruleIds = ruleIdsObj.map(({ id }) => id); + const rulesObj = getDB().prepare( + `SELECT id, name FROM rules WHERE name IN (${compositeRule.rules.map(() => "?").join(",")})` + ).all(compositeRule.rules) as { id: number, name: string }[]; + const ruleIds = rulesObj.map(({ id }) => id); const subtractedInterval = utils.cron.durationOrCronToDate(compositeRule.interval, "subtract").valueOf(); const { count } = getDB() // eslint-disable-next-line max-len @@ -76,10 +76,11 @@ export function handleCompositeRules(logger: Logger) { subtractedInterval, ...ruleIds ) as { count: number }; - const processedRulesIdsObj = getDB() + const processedRules = getDB() .prepare(`SELECT ruleId FROM alerts WHERE createdAt >= ? AND ruleId IN (${ruleIds.map(() => "?").join(",")})`) .all(subtractedInterval, ...ruleIds) as { ruleId: number }[]; - const processedRulesIds = [...new Set(processedRulesIdsObj.flatMap(({ ruleId }) => ruleIds.filter((id) => id === ruleId)))]; + const processedRulesIds = processedRules.map(({ ruleId }) => ruleId); + const processedRulesNames = rulesObj.filter(({ id }) => processedRulesIds.includes(id)).map(({ name }) => name); if (count < compositeRule.notifCount) { logger.info(`[${compositeRule.name}](alertsCount:${count}|notifCount:${compositeRule.notifCount})`); @@ -117,7 +118,8 @@ export function handleCompositeRules(logger: Logger) { compositeRule.notifiers.map((notifierName) => { return { notifierConfig: notifiers[notifierName], - compositeRuleName: compositeRule.name + compositeRuleName: compositeRule.name, + ruleNames: processedRulesNames }; }) ); diff --git a/src/agent/src/notifiers/compositeRules.notifier.ts b/src/agent/src/notifiers/compositeRules.notifier.ts index abef495d..9d4edf96 100644 --- a/src/agent/src/notifiers/compositeRules.notifier.ts +++ b/src/agent/src/notifiers/compositeRules.notifier.ts @@ -9,6 +9,7 @@ const kCompositeRuleSeverity = "critical"; export interface CompositeRuleAlert extends Alert { compositeRuleName: string; + ruleNames: string[]; } export class CompositeRuleNotifier extends Notifier { @@ -52,7 +53,7 @@ export class CompositeRuleNotifier extends Notifier { } #compositeRuleAlertData(alert: CompositeRuleAlert) { - const { compositeRuleName } = alert; + const { compositeRuleName, ruleNames } = alert; const compositeRule = this.config.compositeRules!.find((compositeRule) => compositeRule.name === compositeRuleName)!; const rulesLabels = Object.create(null); @@ -73,7 +74,7 @@ export class CompositeRuleNotifier extends Notifier { severity: kCompositeRuleSeverity, compositeRuleName, label: rulesLabels, - rules: compositeRule.rules.join(", ") + rules: ruleNames.join(", ") }; } } diff --git a/src/agent/test/FT/compositeRules.spec.ts b/src/agent/test/FT/compositeRules.spec.ts index 09bdd40e..fb04f4af 100644 --- a/src/agent/test/FT/compositeRules.spec.ts +++ b/src/agent/test/FT/compositeRules.spec.ts @@ -12,20 +12,12 @@ import isCI from "is-ci"; // Import Internal Dependencies import { getDB, initDB } from "../../src/database"; -import { MockLogger } from "./helpers"; +import { createRuleAlert, MockLogger, resetRuteMuteUntil, ruleMuteUntilTimestamp } from "./helpers"; import { handleCompositeRules } from "../../src/compositeRules"; import { Rule } from "../../src/rules"; // CONSTANTS const kCompositeRulesConfigLocation = path.join(__dirname, "/fixtures/composite-rules/sigyn.config.json"); -const kUntriggeredCompositeRulesConfigLocation = path.join( - __dirname, - "/fixtures/composite-rules-no-mute-untriggered/sigyn.config.json" -); -const kSeverityFilterCompositeRulesConfigLocation = path.join( - __dirname, - "/fixtures/composite-rules-sev-filters/sigyn.config.json" -); const kLogger = new MockLogger(); const kMockAgent = new MockAgent(); const kGlobalDispatcher = getGlobalDispatcher(); @@ -178,153 +170,3 @@ describe("Composite Rules", { concurrency: 1 }, () => { assert.ok(ruleMuteUntilTimestamp(rules[2]) > Date.now()); }); }); - -describe("Composite Rules with muteUntriggered falsy", { concurrency: 1 }, () => { - let config: SigynInitializedConfig; - let rules: any; - - before(async() => { - fs.mkdirSync(".temp", { recursive: true }); - - initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" }); - - process.env.GRAFANA_API_TOKEN = "toto"; - setGlobalDispatcher(kMockAgent); - - const pool = kMockAgent.get("https://discord.com"); - pool.intercept({ - method: "POST", - path: () => true - }).reply(200); - - initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" }); - - config = await initConfig(kUntriggeredCompositeRulesConfigLocation); - rules = config.rules.map((ruleConfig) => { - const rule = new Rule(ruleConfig, { logger: kLogger }); - rule.init(); - - return rule; - }); - }); - - beforeEach(() => { - getDB().prepare("DELETE FROM alerts").run(); - }); - - after(() => { - setGlobalDispatcher(kGlobalDispatcher); - }); - - it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => { - resetRuteMuteUntil(rules[0]); - resetRuteMuteUntil(rules[1]); - resetRuteMuteUntil(rules[2]); - - getDB().prepare("DELETE FROM compositeRuleAlerts").run(); - - assert.equal(ruleMuteUntilTimestamp(rules[0]), 0); - assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); - assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); - - createRuleAlert(rules[0], 5); - createRuleAlert(rules[1], 5); - // DO NOT SEND ALERTS FOR RULE 2 - // createRuleAlert(rules[2], 2); - - handleCompositeRules(kLogger); - await setTimeout(kTimeout); - - assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now()); - assert.ok(ruleMuteUntilTimestamp(rules[1]) > Date.now()); - assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); - }); -}); - -describe("Composite Rules with severity filters", { concurrency: 1 }, () => { - let config: SigynInitializedConfig; - let rules: any; - - before(async() => { - fs.mkdirSync(".temp", { recursive: true }); - - initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" }); - - process.env.GRAFANA_API_TOKEN = "toto"; - setGlobalDispatcher(kMockAgent); - - const pool = kMockAgent.get("https://discord.com"); - pool.intercept({ - method: "POST", - path: () => true - }).reply(200); - - initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" }); - - config = await initConfig(kSeverityFilterCompositeRulesConfigLocation); - rules = config.rules.map((ruleConfig) => { - const rule = new Rule(ruleConfig, { logger: kLogger }); - rule.init(); - - return rule; - }); - }); - - beforeEach(() => { - getDB().prepare("DELETE FROM alerts").run(); - }); - - after(() => { - setGlobalDispatcher(kGlobalDispatcher); - }); - - it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => { - resetRuteMuteUntil(rules[0]); - resetRuteMuteUntil(rules[1]); - resetRuteMuteUntil(rules[2]); - resetRuteMuteUntil(rules[3]); - - getDB().prepare("DELETE FROM compositeRuleAlerts").run(); - - assert.equal(ruleMuteUntilTimestamp(rules[0]), 0); - assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); - assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); - assert.equal(ruleMuteUntilTimestamp(rules[3]), 0); - - createRuleAlert(rules[0], 5); - createRuleAlert(rules[1], 5); - createRuleAlert(rules[2], 5); - createRuleAlert(rules[3], 5); - - handleCompositeRules(kLogger); - await setTimeout(kTimeout); - - assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now()); - assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); - assert.ok(ruleMuteUntilTimestamp(rules[2]) > Date.now()); - assert.ok(ruleMuteUntilTimestamp(rules[3]) > Date.now()); - }); -}); - -function createRuleAlert(rule: Rule, times: number) { - let i = 0; - while (i++ < times) { - getDB() - .prepare("INSERT INTO alerts (ruleId, createdAt) VALUES (?, ?)") - .run(rule.getRuleFromDatabase().id, Date.now()); - } -} - -function ruleMuteUntilTimestamp(rule: Rule): number { - const { muteUntil } = getDB() - .prepare("SELECT muteUntil FROM rules WHERE name = ?") - .get(rule.config.name) as { muteUntil: number }; - - return muteUntil; -} - -function resetRuteMuteUntil(rule: Rule): void { - getDB() - .prepare("UPDATE rules SET muteUntil = ? WHERE name = ?") - .run(0, rule.config.name); -} diff --git a/src/agent/test/FT/compositeRulesLocalNotifier.spec.ts b/src/agent/test/FT/compositeRulesLocalNotifier.spec.ts new file mode 100644 index 00000000..dd8eaa41 --- /dev/null +++ b/src/agent/test/FT/compositeRulesLocalNotifier.spec.ts @@ -0,0 +1,72 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import fs from "node:fs"; +import path from "node:path"; +import { before, beforeEach, describe, it } from "node:test"; +import { setTimeout } from "node:timers/promises"; + +// Import Third-party Dependencies +import { SigynInitializedConfig, initConfig } from "@sigyn/config"; +import isCI from "is-ci"; + +// Import Internal Dependencies +import { getDB, initDB } from "../../src/database"; +import { createRuleAlert, MockLogger } from "./helpers"; +import { handleCompositeRules } from "../../src/compositeRules"; +import { Rule } from "../../src/rules"; +import { TestingNotifier } from "./mocks/sigyn-test-notifier.js"; + +// CONSTANTS +const kCompositeRulesConfigLocation = path.join(__dirname, "/fixtures/composite-rules-local/sigyn.config.json"); +const kLogger = new MockLogger(); +// time to wait for the task to be fully executed (alert sent) +const kTimeout = isCI ? 350 : 200; +const kTestingNotifier = TestingNotifier.getInstance(); + +describe("Composite Rules with Local Notifier", { concurrency: 1 }, () => { + let config: SigynInitializedConfig; + let rules: any; + + before(async() => { + fs.mkdirSync(".temp", { recursive: true }); + + initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" }); + + process.env.GRAFANA_API_TOKEN = "toto"; + + initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" }); + + config = await initConfig(kCompositeRulesConfigLocation); + rules = config.rules.map((ruleConfig) => { + const rule = new Rule(ruleConfig, { logger: kLogger }); + rule.init(); + + return rule; + }); + + kTestingNotifier.clear(); + }); + + beforeEach(() => { + getDB().prepare("DELETE FROM alerts").run(); + }); + + it("should includes rules that have triggered alert only", async() => { + getDB().prepare("DELETE FROM compositeRuleAlerts").run(); + createRuleAlert(rules[0], 5); + createRuleAlert(rules[1], 5); + + handleCompositeRules(kLogger); + await setTimeout(kTimeout); + + const [notif] = kTestingNotifier.notifArguments; + const { rules: notifedRules } = notif.data; + + assert.ok(notifedRules.includes(rules[0].getRuleFromDatabase().name), "Rule 0 has trigger alert so it should be included"); + assert.ok(notifedRules.includes(rules[1].getRuleFromDatabase().name), "Rule 1 has trigger alert so it should be included"); + assert.ok( + notifedRules.includes(rules[2].getRuleFromDatabase().name) === false, + "Rule 2 has not trigger alert so it should not be included" + ); + }); +}); diff --git a/src/agent/test/FT/compositeRulesSeverityFilter.spec.ts b/src/agent/test/FT/compositeRulesSeverityFilter.spec.ts new file mode 100644 index 00000000..f8a9e8af --- /dev/null +++ b/src/agent/test/FT/compositeRulesSeverityFilter.spec.ts @@ -0,0 +1,93 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import fs from "node:fs"; +import path from "node:path"; +import { before, beforeEach, after, describe, it } from "node:test"; +import { setTimeout } from "node:timers/promises"; + +// Import Third-party Dependencies +import { MockAgent, getGlobalDispatcher, setGlobalDispatcher } from "@myunisoft/httpie"; +import { SigynInitializedConfig, initConfig } from "@sigyn/config"; +import isCI from "is-ci"; + +// Import Internal Dependencies +import { getDB, initDB } from "../../src/database"; +import { createRuleAlert, MockLogger, resetRuteMuteUntil, ruleMuteUntilTimestamp } from "./helpers"; +import { handleCompositeRules } from "../../src/compositeRules"; +import { Rule } from "../../src/rules"; + +// CONSTANTS +const kSeverityFilterCompositeRulesConfigLocation = path.join( + __dirname, + "/fixtures/composite-rules-sev-filters/sigyn.config.json" +); +const kLogger = new MockLogger(); +const kMockAgent = new MockAgent(); +const kGlobalDispatcher = getGlobalDispatcher(); +// time to wait for the task to be fully executed (alert sent) +const kTimeout = isCI ? 350 : 200; + +describe("Composite Rules with severity filters", { concurrency: 1 }, () => { + let config: SigynInitializedConfig; + let rules: any; + + before(async() => { + fs.mkdirSync(".temp", { recursive: true }); + + initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" }); + + process.env.GRAFANA_API_TOKEN = "toto"; + setGlobalDispatcher(kMockAgent); + + const pool = kMockAgent.get("https://discord.com"); + pool.intercept({ + method: "POST", + path: () => true + }).reply(200); + + initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" }); + + config = await initConfig(kSeverityFilterCompositeRulesConfigLocation); + rules = config.rules.map((ruleConfig) => { + const rule = new Rule(ruleConfig, { logger: kLogger }); + rule.init(); + + return rule; + }); + }); + + beforeEach(() => { + getDB().prepare("DELETE FROM alerts").run(); + }); + + after(() => { + setGlobalDispatcher(kGlobalDispatcher); + }); + + it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => { + resetRuteMuteUntil(rules[0]); + resetRuteMuteUntil(rules[1]); + resetRuteMuteUntil(rules[2]); + resetRuteMuteUntil(rules[3]); + + getDB().prepare("DELETE FROM compositeRuleAlerts").run(); + + assert.equal(ruleMuteUntilTimestamp(rules[0]), 0); + assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); + assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); + assert.equal(ruleMuteUntilTimestamp(rules[3]), 0); + + createRuleAlert(rules[0], 5); + createRuleAlert(rules[1], 5); + createRuleAlert(rules[2], 5); + createRuleAlert(rules[3], 5); + + handleCompositeRules(kLogger); + await setTimeout(kTimeout); + + assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now()); + assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); + assert.ok(ruleMuteUntilTimestamp(rules[2]) > Date.now()); + assert.ok(ruleMuteUntilTimestamp(rules[3]) > Date.now()); + }); +}); diff --git a/src/agent/test/FT/compositeRulesUntriggered.spec.ts b/src/agent/test/FT/compositeRulesUntriggered.spec.ts new file mode 100644 index 00000000..257dc788 --- /dev/null +++ b/src/agent/test/FT/compositeRulesUntriggered.spec.ts @@ -0,0 +1,90 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import fs from "node:fs"; +import path from "node:path"; +import { before, beforeEach, after, describe, it } from "node:test"; +import { setTimeout } from "node:timers/promises"; + +// Import Third-party Dependencies +import { MockAgent, getGlobalDispatcher, setGlobalDispatcher } from "@myunisoft/httpie"; +import { SigynInitializedConfig, initConfig } from "@sigyn/config"; +import isCI from "is-ci"; + +// Import Internal Dependencies +import { getDB, initDB } from "../../src/database"; +import { createRuleAlert, MockLogger, resetRuteMuteUntil, ruleMuteUntilTimestamp } from "./helpers"; +import { handleCompositeRules } from "../../src/compositeRules"; +import { Rule } from "../../src/rules"; + +// CONSTANTS +const kUntriggeredCompositeRulesConfigLocation = path.join( + __dirname, + "/fixtures/composite-rules-no-mute-untriggered/sigyn.config.json" +); +const kLogger = new MockLogger(); +const kMockAgent = new MockAgent(); +const kGlobalDispatcher = getGlobalDispatcher(); +// time to wait for the task to be fully executed (alert sent) +const kTimeout = isCI ? 350 : 200; + +describe("Composite Rules with muteUntriggered falsy", () => { + let config: SigynInitializedConfig; + let rules: any; + + before(async() => { + fs.mkdirSync(".temp", { recursive: true }); + + initDB(kLogger, { databaseFilename: ".temp/test.sqlite3" }); + + process.env.GRAFANA_API_TOKEN = "toto"; + setGlobalDispatcher(kMockAgent); + + const pool = kMockAgent.get("https://discord.com"); + pool.intercept({ + method: "POST", + path: () => true + }).reply(200); + + initDB(kLogger, { databaseFilename: ".temp/test-agent.sqlite3" }); + + config = await initConfig(kUntriggeredCompositeRulesConfigLocation); + rules = config.rules.map((ruleConfig) => { + const rule = new Rule(ruleConfig, { logger: kLogger }); + rule.init(); + + return rule; + }); + }); + + beforeEach(() => { + getDB().prepare("DELETE FROM alerts").run(); + }); + + after(() => { + setGlobalDispatcher(kGlobalDispatcher); + }); + + it("should not mute rules that have not triggered alerts when muteUntrigged is false", async() => { + resetRuteMuteUntil(rules[0]); + resetRuteMuteUntil(rules[1]); + resetRuteMuteUntil(rules[2]); + + getDB().prepare("DELETE FROM compositeRuleAlerts").run(); + + assert.equal(ruleMuteUntilTimestamp(rules[0]), 0); + assert.equal(ruleMuteUntilTimestamp(rules[1]), 0); + assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); + + createRuleAlert(rules[0], 5); + createRuleAlert(rules[1], 5); + // DO NOT SEND ALERTS FOR RULE 2 + // createRuleAlert(rules[2], 2); + + handleCompositeRules(kLogger); + await setTimeout(kTimeout); + + assert.ok(ruleMuteUntilTimestamp(rules[0]) > Date.now()); + assert.ok(ruleMuteUntilTimestamp(rules[1]) > Date.now()); + assert.equal(ruleMuteUntilTimestamp(rules[2]), 0); + }); +}); diff --git a/src/agent/test/FT/fixtures/composite-rules-local/sigyn.config.json b/src/agent/test/FT/fixtures/composite-rules-local/sigyn.config.json new file mode 100644 index 00000000..7f1d737b --- /dev/null +++ b/src/agent/test/FT/fixtures/composite-rules-local/sigyn.config.json @@ -0,0 +1,83 @@ +{ + "loki": { + "apiUrl": "http://localhost:3100" + }, + "grafana": { + "apiUrl": "http://localhost:3000" + }, + "notifiers": { + "../../test/FT/mocks/sigyn-test-notifier.js": { + "notifier": "../../test/FT/mocks/sigyn-test-notifier.js", + "webhookUrl": "foo" + } + }, + "rules": [ + { + "name": "Local AAA", + "logql": "{env=\"preprod\"} |= `my super logql`", + "polling": "200ms", + "alert": { + "on": { + "count": "1", + "interval": "1s" + }, + "template": { + "title": "{ruleName} - Triggered {counter} times!", + "content": [ + "- LogQL: {logql}" + ] + } + } + }, + { + "name": "Local BBB", + "logql": "{env=\"preprod\"} |= `my super logql`", + "polling": "200ms", + "alert": { + "on": { + "count": "1", + "interval": "1s" + }, + "template": { + "title": "{ruleName} - Triggered {counter} times!", + "content": [ + "- LogQL: {logql}" + ] + } + } + }, + { + "name": "Local CCC", + "logql": "{env=\"preprod\"} |= `my super logql`", + "polling": "200ms", + "alert": { + "on": { + "count": "1", + "interval": "1s" + }, + "template": { + "title": "{ruleName} - Triggered {counter} times!", + "content": [ + "- LogQL: {logql}" + ] + } + } + } + ], + "compositeRules": [ + { + "name": "Composite Rule", + "template": { + "title": "title", + "content": ["content"] + }, + "notifCount": 6, + "throttle": { + "interval": "5m", + "count": 3 + }, + "ruleCountThreshold": 2, + "muteRules": true + } + ] +} diff --git a/src/agent/test/FT/helpers.ts b/src/agent/test/FT/helpers.ts index c8b15d1b..27d8c5be 100644 --- a/src/agent/test/FT/helpers.ts +++ b/src/agent/test/FT/helpers.ts @@ -6,6 +6,7 @@ import dayjs from "dayjs"; // Import Internal Dependencies import { DbAlert, DbAlertNotif, DbNotifier, DbRule, getDB } from "../../src/database"; +import { Rule } from "../../src/rules"; // CONSTANTS const kDatabaseFilename = "test/.temp/test-db.sqlite3"; @@ -66,3 +67,26 @@ export class MockLogger { export function resetAgentFailures() { getDB().prepare("DELETE FROM agentFailures").run(); } + +export function createRuleAlert(rule: Rule, times: number) { + let i = 0; + while (i++ < times) { + getDB() + .prepare("INSERT INTO alerts (ruleId, createdAt) VALUES (?, ?)") + .run(rule.getRuleFromDatabase().id, Date.now()); + } +} + +export function ruleMuteUntilTimestamp(rule: Rule): number { + const { muteUntil } = getDB() + .prepare("SELECT muteUntil FROM rules WHERE name = ?") + .get(rule.config.name) as { muteUntil: number }; + + return muteUntil; +} + +export function resetRuteMuteUntil(rule: Rule): void { + getDB() + .prepare("UPDATE rules SET muteUntil = ? WHERE name = ?") + .run(0, rule.config.name); +}