diff --git a/package-lock.json b/package-lock.json index 6b96534..f35db5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1649,6 +1649,15 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@slack/types": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.11.0.tgz", + "integrity": "sha512-UlIrDWvuLaDly3QZhCPnwUSI/KYmV1N9LyhuH6EDKCRS1HWZhyTG3Ja46T3D0rYfqdltKYFXbJSSRPwZpwO0cQ==", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, "node_modules/@tsd/typescript": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.4.tgz", @@ -8069,7 +8078,8 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^4.0.1" + "@myunisoft/httpie": "^4.0.1", + "@slack/types": "^2.11.0" }, "engines": { "node": ">=18" diff --git a/src/discord/src/index.ts b/src/discord/src/index.ts index 8241564..80576ed 100644 --- a/src/discord/src/index.ts +++ b/src/discord/src/index.ts @@ -1,9 +1,10 @@ // Import Third-party Dependencies -import { ExecuteWebhookOptions, WebhookNotifier } from "@sigyn/notifiers"; +import { WebhookNotifierOptions, WebhookNotifier } from "@sigyn/notifiers"; // CONSTANTS const kWebhookUsername = "Sigyn Agent"; const kAvatarUrl = "https://user-images.githubusercontent.com/39910767/261796970-1c07ee01-30e4-464c-b9f9-903b93f84ff3.png"; + // https://gist.github.com/thomasbnt/b6f455e2c7d743b796917fa3c205f812 const kEmbedColor = { critical: 15548997, @@ -12,7 +13,7 @@ const kEmbedColor = { info: 16777215 }; -class DiscordNotifier extends WebhookNotifier { +class DiscordNotifier extends WebhookNotifier { contentTemplateOptions() { return { transform: ({ key, value }) => (key === "lokiUrl" ? value : `**${value === undefined ? "unknown" : value}**`), @@ -20,13 +21,15 @@ class DiscordNotifier extends WebhookNotifier { }; } - async formatWebhook(): Promise { + async formatWebhookBody(): Promise { if (this.data.ruleConfig?.logql) { this.data.ruleConfig.logql = this.#formatLogQL(this.data.ruleConfig.logql); } - const title = await this.formatTitle(); - const content = await this.formatContent(); + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { embeds: [{ @@ -44,8 +47,13 @@ class DiscordNotifier extends WebhookNotifier { } } -export function execute(options: ExecuteWebhookOptions) { +export async function execute( + options: WebhookNotifierOptions +) { const notifier = new DiscordNotifier(options); + const body = await notifier.formatWebhookBody(); - return notifier.execute(); + return notifier.execute( + body + ); } diff --git a/src/discord/test/execute.spec.ts b/src/discord/test/execute.spec.ts index 9519773..b6ef138 100644 --- a/src/discord/test/execute.spec.ts +++ b/src/discord/test/execute.spec.ts @@ -37,7 +37,9 @@ describe("executeWebhook()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }); @@ -56,7 +58,9 @@ describe("executeWebhook()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }), { diff --git a/src/notifiers/docs/usage.md b/src/notifiers/docs/usage.md index 3677eb8..b74100d 100644 --- a/src/notifiers/docs/usage.md +++ b/src/notifiers/docs/usage.md @@ -7,46 +7,46 @@ ## Webhook -Create a class that extends from `WebhookNotifier` to build a Webhook notifier. +Create a class that extends from `WebhookNotifier` to build a Webhook notifier. ```ts -import { ExecuteWebhookOptions, WebhookNotifier } from "@sigyn/notifiers"; +import { WebhookNotifierOptions, WebhookNotifier } from "@sigyn/notifiers"; -class MyAwesomeWebhookNotifier extends WebhookNotifier { - async formatWebhook(): Promise { - const title = await this.formatTitle(); - const content = await this.formatContent(); +interface MyAwesomeFormat { + title: string, + content: string +} + +class MyAwesomeWebhookNotifier extends WebhookNotifier { + async formatWebhookBody(): Promise { + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { title, - content.join("\n") + content: content.join("\n") } } } -export function execute(options: ExecuteWebhookOptions) { +export function execute( + options: WebhookNotifierOptions +) { const notifier = new MyAwesomeWebhookNotifier(options); + const body = await notifier.formatWebhookBody(); - return notifier.execute(); + return notifier.execute( + body + ); } ``` -The only required method to implement is `formatWebhook()`. -This method return the Webhook body. -```ts -async execute() { - const body = await this.formatWebhook(); - - return httpie.post(this.webhookUrl, { - body, - headers: this.#headers - }); -} -``` You can use `formatTitle()` & `formatContent()` to get title & content formatted with template data. Theses functions uses `@sigyn/morphix` and you can customise the options of boths: ```ts -class MyAwesomeWebhookNotifier extends WebhookNotifier { +class MyAwesomeWebhookNotifier extends WebhookNotifier { contentTemplateOptions() { return { transform: ({ value }) => (value === undefined ? "unknown" : value), @@ -61,13 +61,15 @@ class MyAwesomeWebhookNotifier extends WebhookNotifier { } } - async formatWebhook(): Promise { - const title = await this.formatTitle(); - const content = await this.formatContent(); + async formatWebhookBody(): Promise { + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { title, - content.join("\n") + content: content.join("\n") } } } @@ -77,6 +79,7 @@ class MyAwesomeWebhookNotifier extends WebhookNotifier { > The `contentTemplateOptions` & `titleTemplateOptions` above are the default values. By default, `showSeverityEmoji` is truthy: this option add an emoji before the title depending the alert **severity**. + ```ts const kSeverityEmoji = { critical: "💥", @@ -85,27 +88,33 @@ const kSeverityEmoji = { info: "📢" }; ``` + You can do `this.showSeverityEmoji = false` to disable this behavior. + ```ts async formatWebhook(): Promise { this.showSeverityEmoji = false; - const title = await this.formatTitle(); - const content = await this.formatContent(); + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { title, - content.join("\n") + content: content.join("\n") } } ``` + You can also disable it in the constructor + ```ts class MyAwesomeWebhookNotifier extends WebhookNotifier { // directly set the property to false showSeverityEmoji = false; - constructor(options: ExecuteWebhookOptions) { + constructor(options: WebhookNotifierOptions) { super(options); // or this.showSeverityEmoji = false; @@ -121,21 +130,25 @@ You can see implementation examples with our notifiers: ## Interfaces ```ts -interface ExecuteWebhookOptions { - webhookUrl: string; - data: ExecuteWebhookData; - template: SigynInitializedTemplate; +export interface WebhookNotifierOptions { + webhookUrl: string; + data: WebhookData; + template: SigynInitializedTemplate; } -interface ExecuteWebhookData { - ruleConfig?: NotifierFormattedSigynRule; - counter?: number; - severity: "critical" | "error" | "warning" | "info"; - label?: Record; - lokiUrl?: string; - agentFailure?: { - errors: string; - rules: string; - }; - rules?: string; + +export interface WebhookData { + ruleConfig?: NotifierFormattedSigynRule; + counter?: number; + severity: "critical" | "error" | "warning" | "info"; + label?: Record; + lokiUrl?: string; + agentFailure?: { + errors: string; + rules: string; + } + rules?: string; + labelCount: number; + labelMatchCount: number; + labelMatchPercent?: number; } ``` diff --git a/src/notifiers/src/index.ts b/src/notifiers/src/index.ts index 47e5ac3..6eb0ac2 100644 --- a/src/notifiers/src/index.ts +++ b/src/notifiers/src/index.ts @@ -1,30 +1,2 @@ -// Import Third-party Dependencies -import { NotifierFormattedSigynRule, SigynInitializedTemplate } from "@sigyn/config"; - // Import Internal Dependencies -import { WebhookNotifier } from "./webhook"; - -export { WebhookNotifier }; - -export interface ExecuteWebhookOptions { - webhookUrl: string; - data: ExecuteWebhookData; - template: SigynInitializedTemplate; -} - -export interface ExecuteWebhookData { - ruleConfig?: NotifierFormattedSigynRule; - counter?: number; - severity: "critical" | "error" | "warning" | "info"; - label?: Record; - lokiUrl?: string; - agentFailure?: { - errors: string; - rules: string; - } - rules?: string; - labelCount: number; - labelMatchCount: number; - labelMatchPercent?: number; -} - +export * from "./webhook.js"; diff --git a/src/notifiers/src/webhook.ts b/src/notifiers/src/webhook.ts index c873822..a2e729c 100644 --- a/src/notifiers/src/webhook.ts +++ b/src/notifiers/src/webhook.ts @@ -1,8 +1,8 @@ -// Import Internal Dependencies -import { ExecuteWebhookData, ExecuteWebhookOptions } from "."; - // Import Third-party Dependencies -import { SigynInitializedTemplate } from "@sigyn/config"; +import { + SigynInitializedTemplate, + NotifierFormattedSigynRule +} from "@sigyn/config"; import { morphix } from "@sigyn/morphix"; import * as httpie from "@myunisoft/httpie"; @@ -14,9 +14,31 @@ const kSeverityEmoji = { info: "📢" }; -export class WebhookNotifier { +export interface WebhookNotifierOptions { + webhookUrl: string; + data: WebhookData; + template: SigynInitializedTemplate; +} + +export interface WebhookData { + ruleConfig?: NotifierFormattedSigynRule; + counter?: number; + severity: "critical" | "error" | "warning" | "info"; + label?: Record; + lokiUrl?: string; + agentFailure?: { + errors: string; + rules: string; + } + rules?: string; + labelCount: number; + labelMatchCount: number; + labelMatchPercent?: number; +} + +export class WebhookNotifier { webhookUrl: string; - data: ExecuteWebhookData; + data: WebhookData; template: SigynInitializedTemplate; showSeverityEmoji = true; @@ -27,7 +49,7 @@ export class WebhookNotifier { ignoreMissing: true }; - constructor(options: ExecuteWebhookOptions) { + constructor(options: WebhookNotifierOptions) { this.webhookUrl = JSON.parse(JSON.stringify((options.webhookUrl))); this.data = JSON.parse(JSON.stringify((options.data))); this.template = JSON.parse(JSON.stringify((options.template))); @@ -45,7 +67,7 @@ export class WebhookNotifier { return this.#defaultTemplateOptions; } - async formatTitle() { + formatTitle(): Promise { if (this.data.ruleConfig) { this.data = { ...this.data, @@ -57,11 +79,14 @@ export class WebhookNotifier { this.template.title = `${kSeverityEmoji[this.data.severity]} ${this.template.title}`; } - return await morphix(this.template.title, this.data, this.titleTemplateOptions()); + return morphix(this.template.title, this.data, this.titleTemplateOptions()); } - async formatContent() { - // We update the data here at the end of lifecycle in case the user update data / ruleConfig previous in the lifecycle + async formatContent(): Promise { + /** + * We update the data here at the end of lifecycle + * in case the user update data / ruleConfig previous in the lifecycle + */ if (this.data.ruleConfig) { this.data = { ...this.data, @@ -69,22 +94,14 @@ export class WebhookNotifier { }; } - const contents: string[] = []; - - for (const content of this.template.content) { - contents.push(await morphix(content, this.data, this.contentTemplateOptions())); - } - - return contents; + return Promise.all( + this.template.content.map((content) => morphix(content, this.data, this.contentTemplateOptions())) + ); } - async formatWebhook() { - throw new Error("formatWebhook method must be implemented"); - } - - async execute() { - const body = await this.formatWebhook(); - + async execute( + body: T + ): Promise> { return httpie.post(this.webhookUrl, { body, headers: this.#headers diff --git a/src/notifiers/test/webhook.spec.ts b/src/notifiers/test/webhook.spec.ts index e667747..d9f075f 100644 --- a/src/notifiers/test/webhook.spec.ts +++ b/src/notifiers/test/webhook.spec.ts @@ -12,8 +12,8 @@ const kMockAgent = new MockAgent(); const kDispatcher = getGlobalDispatcher(); const kDummyWebhoobURL = "https://webhook.test"; -class DummyWebhookNotifier extends WebhookNotifier { - async formatWebhook() { +class DummyWebhookNotifier extends WebhookNotifier<{ foo: string }> { + async formatWebhookBody() { return { foo: "bar" }; } } @@ -41,7 +41,9 @@ describe("Webhook", () => { const notifier = new DummyWebhookNotifier({ webhookUrl: "https://webhook.test/dummy", data: { - severity: "critical" + severity: "critical", + labelCount: 0, + labelMatchCount: 1 }, template: { title: "Test", @@ -49,7 +51,10 @@ describe("Webhook", () => { } }); - const response = await notifier.execute(); + const body = await notifier.formatWebhookBody(); + const response = await notifier.execute( + body + ); assert.strictEqual(response.statusCode, 200); }); diff --git a/src/slack/package.json b/src/slack/package.json index e8b05a9..1ecb830 100644 --- a/src/slack/package.json +++ b/src/slack/package.json @@ -38,6 +38,7 @@ "author": "GENTILHOMME Thomas ", "license": "MIT", "dependencies": { - "@myunisoft/httpie": "^4.0.1" + "@myunisoft/httpie": "^4.0.1", + "@slack/types": "^2.11.0" } } diff --git a/src/slack/src/index.ts b/src/slack/src/index.ts index 1a78b38..362d3ac 100644 --- a/src/slack/src/index.ts +++ b/src/slack/src/index.ts @@ -1,5 +1,6 @@ // Import Third-party Dependencies -import { ExecuteWebhookOptions, WebhookNotifier } from "@sigyn/notifiers"; +import { WebhookNotifierOptions, WebhookNotifier } from "@sigyn/notifiers"; +import { MessageAttachment } from "@slack/types"; // CONSTANTS const kAttachmentColor = { @@ -9,7 +10,11 @@ const kAttachmentColor = { info: "#E7E7E7" }; -class SlackNotifier extends WebhookNotifier { +export interface SlackWebhookBodyFormat { + attachments?: MessageAttachment[]; +} + +class SlackNotifier extends WebhookNotifier { contentTemplateOptions() { return { transform: ({ value, key }) => { @@ -23,7 +28,7 @@ class SlackNotifier extends WebhookNotifier { }; } - async formatWebhook(): Promise { + async formatWebhookBody(): Promise { if (this.data.ruleConfig?.logql) { this.data.ruleConfig.logql = this.#formatLogQL(this.data.ruleConfig.logql); } @@ -40,8 +45,10 @@ class SlackNotifier extends WebhookNotifier { return formattedText; }); - const title = await this.formatTitle(); - const content = await this.formatContent(); + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { attachments: [ @@ -51,6 +58,7 @@ class SlackNotifier extends WebhookNotifier { title, fields: [ { + title, value: content.join("\n").replaceAll(/>(?!\s|$)/g, "›"), short: false } @@ -65,8 +73,13 @@ class SlackNotifier extends WebhookNotifier { } } -export function execute(options: ExecuteWebhookOptions) { +export async function execute( + options: WebhookNotifierOptions +) { const notifier = new SlackNotifier(options); + const body = await notifier.formatWebhookBody(); - return notifier.execute(); + return notifier.execute( + body + ); } diff --git a/src/slack/test/execute.spec.ts b/src/slack/test/execute.spec.ts index 0b89a93..5e442a8 100644 --- a/src/slack/test/execute.spec.ts +++ b/src/slack/test/execute.spec.ts @@ -37,7 +37,9 @@ describe("execute()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }); @@ -56,7 +58,9 @@ describe("execute()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }), { diff --git a/src/teams/src/index.ts b/src/teams/src/index.ts index 17ff239..310147a 100644 --- a/src/teams/src/index.ts +++ b/src/teams/src/index.ts @@ -1,7 +1,12 @@ // Import Third-party Dependencies -import { ExecuteWebhookOptions, WebhookNotifier } from "@sigyn/notifiers"; +import { WebhookNotifierOptions, WebhookNotifier } from "@sigyn/notifiers"; -class TeamsNotifier extends WebhookNotifier { +export interface TeamsWebhookBodyFormat { + title: string, + text: string +} + +class TeamsNotifier extends WebhookNotifier { contentTemplateOptions() { return { transform: ({ value, key }) => (key === "logql" || key === "lokiUrl" ? value : `**${value ?? "unknown"}**`), @@ -9,13 +14,15 @@ class TeamsNotifier extends WebhookNotifier { }; } - async formatWebhook(): Promise { + async formatWebhookBody(): Promise { if (this.data.ruleConfig?.logql) { this.data.ruleConfig.logql = this.#formatLogQL(this.data.ruleConfig.logql); } - const title = await this.formatTitle(); - const content = await this.formatContent(); + const [title, content] = await Promise.all([ + this.formatTitle(), + this.formatContent() + ]); return { title, @@ -28,8 +35,13 @@ class TeamsNotifier extends WebhookNotifier { } } -export function execute(options: ExecuteWebhookOptions) { +export async function execute( + options: WebhookNotifierOptions +) { const notifier = new TeamsNotifier(options); + const body = await notifier.formatWebhookBody(); - return notifier.execute(); + return notifier.execute( + body + ); } diff --git a/src/teams/test/execute.spec.ts b/src/teams/test/execute.spec.ts index 3f20928..01c7d6c 100644 --- a/src/teams/test/execute.spec.ts +++ b/src/teams/test/execute.spec.ts @@ -37,7 +37,9 @@ describe("execute()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }); @@ -56,7 +58,9 @@ describe("execute()", () => { data: { counter: 10, severity: "error", - label: { foo: "bar" } + label: { foo: "bar" }, + labelCount: 0, + labelMatchCount: 0 }, template: { title: "foo", content: [] } }), {