Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: alert template message pt3 #326

Merged
merged 2 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"express-session": "^1.17.3",
"express-winston": "^4.2.0",
"extract-domain": "^2.4.1",
"handlebars": "^4.7.8",
"http-graceful-shutdown": "^3.1.13",
"isemail": "^3.2.0",
"jsonwebtoken": "^9.0.0",
Expand All @@ -39,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
Loading