Skip to content

Commit

Permalink
feat: use handlebars custom helper to trigger notifyChannel
Browse files Browse the repository at this point in the history
  • Loading branch information
wrn14897 committed Feb 28, 2024
1 parent cfa6a3b commit d3e4c00
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 109 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"passport-local": "^1.0.0",
"passport-local-mongoose": "^6.1.0",
"pluralize": "^8.0.0",
"promised-handlebars": "^2.0.1",
"rate-limit-redis": "^3.0.2",
"redis": "^4.6.8",
"semver": "^7.5.2",
Expand Down
37 changes: 26 additions & 11 deletions packages/api/src/models/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import { ObjectId } from 'mongodb';
import mongoose, { Schema } from 'mongoose';

export enum WebhookService {
Slack = 'slack',
}

export interface IWebhook {
_id: ObjectId;
createdAt: Date;
name: string;
service: string;
service: WebhookService;
team: ObjectId;
updatedAt: Date;
url: string;
}

export default mongoose.model<IWebhook>(
'Webhook',
new Schema<IWebhook>(
{
team: { type: Schema.Types.ObjectId, ref: 'Team' },
service: String,
name: String,
url: String,
const WebhookSchema = new Schema<IWebhook>(
{
team: { type: Schema.Types.ObjectId, ref: 'Team' },
service: {
type: String,
enum: Object.values(WebhookService),
required: true,
},
name: {
type: String,
required: true,
},
{ timestamps: true },
),
url: {
type: String,
required: false,
},
},
{ timestamps: true },
);

WebhookSchema.index({ team: 1, service: 1, name: 1 }, { unique: true });

export default mongoose.model<IWebhook>('Webhook', WebhookSchema);
220 changes: 219 additions & 1 deletion packages/api/src/tasks/__tests__/checkAlerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ import LogView from '../../models/logView';
import Webhook from '../../models/webhook';
import * as slack from '../../utils/slack';
import {
buildAlertMessageTemplateHdxLink,
buildAlertMessageTemplateTitle,
buildLogSearchLink,
doesExceedThreshold,
getDefaultExternalAction,
processAlert,
renderAlertTemplate,
roundDownToXMinutes,
translateExternalActionsToInternal,
} from '../checkAlerts';

describe('checkAlerts', () => {
Expand Down Expand Up @@ -81,6 +86,220 @@ describe('checkAlerts', () => {
expect(doesExceedThreshold(false, 10, 10)).toBe(false);
});

describe('Alert Templates', () => {
const defaultSearchView: any = {
alert: {
threshold_type: 'above',
threshold: 1,
source: 'search',
groupBy: 'span_name',
},
savedSearch: {
id: 'id-123',
query: 'level:error',
name: 'My Search',
},
team: {
id: 'team-123',
logStreamTableVersion: 1,
},
group: 'http',
startTime: new Date('2023-03-17T22:13:03.103Z'),
endTime: new Date('2023-03-17T22:13:59.103Z'),
value: 10,
};

const defaultChartView: any = {
alert: {
threshold_type: 'below',
threshold: 10,
source: 'chart',
groupBy: 'span_name',
},
dashboard: {
id: 'id-123',
name: 'My Dashboard',
charts: [
{
name: 'My Chart',
},
],
},
team: {
id: 'team-123',
logStreamTableVersion: 1,
},
startTime: new Date('2023-03-17T22:13:03.103Z'),
endTime: new Date('2023-03-17T22:13:59.103Z'),
granularity: '5 minute',
value: 5,
};

const server = getServer();

beforeAll(async () => {
await server.start();
});

afterEach(async () => {
await server.clearDBs();
jest.clearAllMocks();
});

afterAll(async () => {
await server.stop();
});

it('buildAlertMessageTemplateHdxLink', () => {
expect(buildAlertMessageTemplateHdxLink(defaultSearchView)).toBe(
'http://localhost:9090/search/id-123?from=1679091183103&to=1679091239103&q=level%3Aerror+span_name%3A%22http%22',
);
expect(buildAlertMessageTemplateHdxLink(defaultChartView)).toBe(
'http://localhost:9090/dashboards/id-123?from=1679089083103&granularity=5+minute&to=1679093339103',
);
});

it('buildAlertMessageTemplateTitle', () => {
expect(
buildAlertMessageTemplateTitle({
view: defaultSearchView,
}),
).toBe('Alert for "My Search" - 10 lines found');
expect(
buildAlertMessageTemplateTitle({
view: defaultChartView,
}),
).toBe('Alert for "My Chart" in "My Dashboard" - 5 falls below 10');
});

it('getDefaultExternalAction', () => {
expect(
getDefaultExternalAction({
channel: {
type: 'slack_webhook',
webhookId: '123',
},
} as any),
).toBe('@slack_webhook-123');
expect(
getDefaultExternalAction({
channel: {
type: 'foo',
},
} as any),
).toBeNull();
});

it('translateExternalActionsToInternal', () => {
// normal
expect(
translateExternalActionsToInternal('@slack_webhook-123'),
).toMatchInlineSnapshot(
`"{{__hdx_notify_channel__ channel=\\"slack_webhook\\" id=\\"123\\"}}"`,
);

// with body string
expect(
translateExternalActionsToInternal('blabla @action-id'),
).toMatchInlineSnapshot(
`"blabla {{__hdx_notify_channel__ channel=\\"action\\" id=\\"id\\"}}"`,
);

// multiple actions
expect(
translateExternalActionsToInternal('blabla @action-id @action2-id2'),
).toMatchInlineSnapshot(
`"blabla {{__hdx_notify_channel__ channel=\\"action\\" id=\\"id\\"}} {{__hdx_notify_channel__ channel=\\"action2\\" id=\\"id2\\"}}"`,
);

// id with special characters
expect(
translateExternalActionsToInternal('send @email-mike@hyperdx.io'),
).toMatchInlineSnapshot(
`"send {{__hdx_notify_channel__ channel=\\"email\\" id=\\"mike@hyperdx.io\\"}}"`,
);

// id with multiple dashes
expect(
translateExternalActionsToInternal('@action-id-with-multiple-dashes'),
).toMatchInlineSnapshot(
`"{{__hdx_notify_channel__ channel=\\"action\\" id=\\"id-with-multiple-dashes\\"}}"`,
);
});

it('renderAlertTemplate', async () => {
jest
.spyOn(slack, 'postMessageToWebhook')
.mockResolvedValueOnce(null as any);
jest.spyOn(clickhouse, 'getLogBatch').mockResolvedValueOnce({
data: [
{
timestamp: '2023-11-16T22:10:00.000Z',
severity_text: 'error',
body: 'Oh no! Something went wrong!',
},
{
timestamp: '2023-11-16T22:15:00.000Z',
severity_text: 'info',
body: 'All good!',
},
],
} as any);

const team = await createTeam({ name: 'My Team' });
await new Webhook({
team: team._id,
service: 'slack',
url: 'https://hooks.slack.com/services/123',
name: 'My_Webhook',
}).save();

await renderAlertTemplate({
template: 'Custom body @slack_webhook-My_Web', // partial name should work
view: {
...defaultSearchView,
alert: {
...defaultSearchView.alert,
channel: {
type: null, // using template instead
},
},
team: {
id: team._id.toString(),
logStreamTableVersion: team.logStreamTableVersion,
},
},
title: 'Alert for "My Search" - 10 lines found',
});

expect(slack.postMessageToWebhook).toHaveBeenNthCalledWith(
1,
'https://hooks.slack.com/services/123',
{
text: 'Alert for "My Search" - 10 lines found',
blocks: [
{
text: {
text: [
'*<http://localhost:9090/search/id-123?from=1679091183103&to=1679091239103&q=level%3Aerror+span_name%3A%22http%22 | Alert for "My Search" - 10 lines found>*',
'Group: "http"',
'10 lines found, expected less than 1 lines',
'Custom body ',
'```',
'Nov 16 22:10:00Z [error] Oh no! Something went wrong!',
'Nov 16 22:15:00Z [info] All good!',
'```',
].join('\n'),
type: 'mrkdwn',
},
type: 'section',
},
],
},
);
});
});

describe('processAlert', () => {
const server = getServer();

Expand Down Expand Up @@ -215,7 +434,6 @@ describe('checkAlerts', () => {
'```',
'Nov 16 22:10:00Z [error] Oh no! Something went wrong!',
'```',
'',
].join('\n'),
type: 'mrkdwn',
},
Expand Down
Loading

0 comments on commit d3e4c00

Please sign in to comment.