From de9712cbfb54046fdc6f81ef3373ad92eabca9bd Mon Sep 17 00:00:00 2001 From: Warren <5959690+wrn14897@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:51:08 -0700 Subject: [PATCH] feat: alert template message pt6 (#343) --- .../src/tasks/__tests__/checkAlerts.test.ts | 58 +++++++++++++++++-- packages/api/src/tasks/checkAlerts.ts | 52 ++++++++++++----- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/packages/api/src/tasks/__tests__/checkAlerts.test.ts b/packages/api/src/tasks/__tests__/checkAlerts.test.ts index f2ed3bd6c..39bf7c346 100644 --- a/packages/api/src/tasks/__tests__/checkAlerts.test.ts +++ b/packages/api/src/tasks/__tests__/checkAlerts.test.ts @@ -24,6 +24,7 @@ import { buildLogSearchLink, doesExceedThreshold, escapeJsonValues, + expandToNestedObject, getDefaultExternalAction, injectIntoPlaceholders, processAlert, @@ -144,6 +145,47 @@ describe('checkAlerts', () => { expect(doesExceedThreshold(false, 10, 10)).toBe(false); }); + it('expandToNestedObject', () => { + expect(expandToNestedObject({}).__proto__).toBeUndefined(); + expect(expandToNestedObject({})).toEqual({}); + expect(expandToNestedObject({ foo: 'bar' })).toEqual({ foo: 'bar' }); + expect(expandToNestedObject({ 'foo.bar': 'baz' })).toEqual({ + foo: { bar: 'baz' }, + }); + expect(expandToNestedObject({ 'foo.bar.baz': 'qux' })).toEqual({ + foo: { bar: { baz: 'qux' } }, + }); + // mix + expect( + expandToNestedObject({ + 'foo.bar.baz': 'qux', + 'foo.bar.quux': 'quuz', + 'foo1.bar1.baz1': 'qux1', + }), + ).toEqual({ + foo: { bar: { baz: 'qux', quux: 'quuz' } }, + foo1: { bar1: { baz1: 'qux1' } }, + }); + // overwriting + expect( + expandToNestedObject({ 'foo.bar.baz': 'qux', 'foo.bar': 'quuz' }), + ).toEqual({ + foo: { bar: 'quuz' }, + }); + // max depth + expect( + expandToNestedObject( + { + 'foo.bar.baz.qux.quuz.quux': 'qux', + }, + '.', + 3, + ), + ).toEqual({ + foo: { bar: { baz: {} } }, + }); + }); + describe('Alert Templates', () => { const defaultSearchView: any = { alert: { @@ -532,8 +574,8 @@ describe('checkAlerts', () => { await renderAlertTemplate({ template: ` -{{#is_match "k8s.pod.name" "otel-collector-123"}} - Runbook URL: {{attributes.runbookUrl}} +{{#is_match "attributes.k8s.pod.name" "otel-collector-123"}} + Runbook URL: {{attributes.runbook.url}} hi i matched @slack_webhook-My_Web {{/is_match}} @@ -549,8 +591,14 @@ describe('checkAlerts', () => { }, }, attributes: { - runbookUrl: 'https://example.com', - 'k8s.pod.name': 'otel-collector-123', + runbook: { + url: 'https://example.com', + }, + k8s: { + pod: { + name: 'otel-collector-123', + }, + }, }, }, title: 'Alert for "My Search" - 10 lines found', @@ -563,7 +611,7 @@ describe('checkAlerts', () => { // @slack_webhook should not be called await renderAlertTemplate({ template: - '{{#is_match "host" "web"}} @slack_webhook-My_Web {{/is_match}}', // partial name should work + '{{#is_match "attributes.host" "web"}} @slack_webhook-My_Web {{/is_match}}', // partial name should work view: { ...defaultSearchView, alert: { diff --git a/packages/api/src/tasks/checkAlerts.ts b/packages/api/src/tasks/checkAlerts.ts index 3eba405be..076b1e35e 100644 --- a/packages/api/src/tasks/checkAlerts.ts +++ b/packages/api/src/tasks/checkAlerts.ts @@ -4,6 +4,7 @@ import * as fns from 'date-fns'; import * as fnsTz from 'date-fns-tz'; import Handlebars, { HelperOptions } from 'handlebars'; +import _ from 'lodash'; import { escapeRegExp, isString } from 'lodash'; import mongoose from 'mongoose'; import ms from 'ms'; @@ -107,6 +108,36 @@ export const doesExceedThreshold = ( return false; }; +// transfer keys of attributes with dot into nested object +// ex: { 'a.b': 'c', 'd.e.f': 'g' } -> { a: { b: 'c' }, d: { e: { f: 'g' } } } +export const expandToNestedObject = ( + obj: Record, + separator = '.', + maxDepth = 10, +) => { + const result: Record = Object.create(null); // An object NOT inheriting from `Object.prototype` + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const keys = key.split(separator); + let nestedObj = result; + + for (let i = 0; i < keys.length; i++) { + if (i >= maxDepth) { + break; + } + const nestedKey = keys[i]; + if (i === keys.length - 1) { + nestedObj[nestedKey] = obj[key]; + } else { + nestedObj[nestedKey] = nestedObj[nestedKey] || {}; + nestedObj = nestedObj[nestedKey]; + } + } + } + } + return result; +}; + // ------------------------------------------------------------ // ----------------- Alert Message Template ------------------- // ------------------------------------------------------------ @@ -114,7 +145,7 @@ export const doesExceedThreshold = ( type AlertMessageTemplateDefaultView = { // FIXME: do we want to include groupBy in the external alert schema? alert: z.infer & { groupBy?: string }; - attributes: Record; + attributes: ReturnType; dashboard: ReturnType< typeof translateDashboardDocumentToExternalDashboard > | null; @@ -411,16 +442,8 @@ export const renderAlertTemplate = async ({ logStreamTableVersion?: ITeam['logStreamTableVersion']; }; }) => { - const { - alert, - attributes, - dashboard, - endTime, - group, - savedSearch, - startTime, - value, - } = view; + const { alert, dashboard, endTime, group, savedSearch, startTime, value } = + view; const defaultExternalAction = getDefaultExternalAction(alert); const targetTemplate = @@ -430,14 +453,13 @@ export const renderAlertTemplate = async ({ ).trim() : translateExternalActionsToInternal(template ?? ''); - const attributesMap = new Map(Object.entries(attributes ?? {})); const isMatchFn = function (shouldRender: boolean) { return function ( targetKey: string, targetValue: string, options: HelperOptions, ) { - if (attributesMap.get(targetKey) === targetValue) { + if (_.has(view, targetKey) && _.get(view, targetKey) === targetValue) { if (shouldRender) { return options.fn(this); } else { @@ -581,12 +603,14 @@ const fireChannelEvent = async ({ if (team == null) { throw new Error('Team not found'); } + + const attributesNested = expandToNestedObject(attributes); const templateView: AlertMessageTemplateDefaultView = { alert: { ...translateAlertDocumentToExternalAlert(alert), groupBy: alert.groupBy, }, - attributes, + attributes: attributesNested, dashboard: dashboard ? translateDashboardDocumentToExternalDashboard({ _id: dashboard._id,