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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: alert template message pt4 #338

Merged
merged 11 commits into from
Mar 11, 2024
6 changes: 6 additions & 0 deletions .changeset/green-bulldogs-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperdx/api': patch
'@hyperdx/app': patch
---

feat: introduce conditional alert routing helper #is_match
9 changes: 9 additions & 0 deletions packages/api/src/clickhouse/__tests__/clickhouse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1271,20 +1271,29 @@ Array [
expect(data).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
"testGroup": "group2",
},
"data": 777,
"group": Array [
"group2",
],
"ts_bucket": 1641340800,
},
Object {
"attributes": Object {
"testGroup": "group1",
},
"data": 77,
"group": Array [
"group1",
],
"ts_bucket": 1641340800,
},
Object {
"attributes": Object {
"testGroup": "group1",
},
"data": 7,
"group": Array [
"group1",
Expand Down
40 changes: 32 additions & 8 deletions packages/api/src/clickhouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1818,21 +1818,39 @@ export const getMultiSeriesChartLegacyFormat = async ({

const flatData = result.data.flatMap(row => {
if (seriesReturnType === 'column') {
return series.map((_, i) => {
return series.map((s, i) => {
const groupBy =
s.type === 'number' ? [] : 'groupBy' in s ? s.groupBy : [];
const attributes = groupBy.reduce((acc, curVal, curIndex) => {
acc[curVal] = row.group[curIndex];
return acc;
}, {} as Record<string, string>);
return {
ts_bucket: row.ts_bucket,
group: row.group,
attributes,
data: row[`series_${i}.data`],
group: row.group,
ts_bucket: row.ts_bucket,
};
});
}

// Ratio only has 1 series
const groupBy =
series[0].type === 'number'
? []
: 'groupBy' in series[0]
? series[0].groupBy
: [];
const attributes = groupBy.reduce((acc, curVal, curIndex) => {
acc[curVal] = row.group[curIndex];
return acc;
}, {} as Record<string, string>);
return [
{
ts_bucket: row.ts_bucket,
group: row.group,
attributes,
data: row['series_0.data'],
group: row.group,
ts_bucket: row.ts_bucket,
},
];
});
Expand Down Expand Up @@ -2550,8 +2568,9 @@ export const checkAlert = async ({
`
SELECT
?
count(*) as data,
toUnixTimestamp(toStartOfInterval(timestamp, INTERVAL ?)) as ts_bucket
count(*) AS data,
any(_string_attributes) AS attributes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might also need to extract the default properties like service, level, etc. that are in columns as attributes as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we should. I can add those in the next PR

toUnixTimestamp(toStartOfInterval(timestamp, INTERVAL ?)) AS ts_bucket
FROM ??
WHERE ? AND (?)
GROUP BY ?
Expand Down Expand Up @@ -2596,7 +2615,12 @@ export const checkAlert = async ({
},
});
const result = await rows.json<
ResponseJSON<{ data: string; group?: string; ts_bucket: number }>
ResponseJSON<{
data: string;
group?: string;
ts_bucket: number;
attributes: Record<string, string>;
}>
>();
logger.info({
message: 'checkAlert',
Expand Down
183 changes: 182 additions & 1 deletion packages/api/src/tasks/__tests__/checkAlerts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,16 @@ describe('checkAlerts', () => {
).toMatchInlineSnapshot(
`"{{__hdx_notify_channel__ channel=\\"action\\" id=\\"id-with-multiple-dashes\\"}}"`,
);

// custom template id
expect(
translateExternalActionsToInternal('@action-{{action_id}}'),
).toMatchInlineSnapshot(
`"{{__hdx_notify_channel__ channel=\\"action\\" id=\\"{{action_id}}\\"}}"`,
);
});

it('renderAlertTemplate', async () => {
it('renderAlertTemplate - custom body with single action', async () => {
jest
.spyOn(slack, 'postMessageToWebhook')
.mockResolvedValueOnce(null as any);
Expand Down Expand Up @@ -356,6 +363,180 @@ describe('checkAlerts', () => {
},
);
});

it('renderAlertTemplate - single action with custom action id', 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-{{attributes.webhookName}}', // partial name should work
view: {
...defaultSearchView,
alert: {
...defaultSearchView.alert,
channel: {
type: null, // using template instead
},
},
attributes: {
webhookName: 'My_Webhook',
},
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',
},
],
},
);
});

it('renderAlertTemplate - #is_match with single action', 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:
'{{#is_match "host" "web"}} @slack_webhook-My_Web {{/is_match}}', // partial name should work
view: {
...defaultSearchView,
alert: {
...defaultSearchView.alert,
channel: {
type: null, // using template instead
},
},
team: {
id: team._id.toString(),
logStreamTableVersion: team.logStreamTableVersion,
},
attributes: {
host: 'web',
},
},
title: 'Alert for "My Search" - 10 lines found',
});

// @slack_webhook should not be called
await renderAlertTemplate({
template:
'{{#is_match "host" "web"}} @slack_webhook-My_Web {{/is_match}}', // partial name should work
view: {
...defaultSearchView,
alert: {
...defaultSearchView.alert,
channel: {
type: null, // using template instead
},
},
team: {
id: team._id.toString(),
logStreamTableVersion: team.logStreamTableVersion,
},
attributes: {
host: 'web2',
},
},
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',
'',
'```',
'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', () => {
Expand Down
Loading
Loading