Browse Source

Slack: support images in alerts #801 (#941)

pull/1/head
rozetko 3 years ago committed by GitHub
parent
commit
027ecc762b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      server/package.json
  2. 6
      server/src/config.ts
  3. 23
      server/src/services/alert_service.ts
  4. 57
      server/src/services/notification_service.ts

1
server/package.json

@ -21,6 +21,7 @@
"dependencies": {},
"devDependencies": {
"@corpglory/tsdb-kit": "^1.1.1",
"@slack/web-api": "^6.0.0",
"@types/jest": "^23.3.14",
"@types/koa": "^2.0.46",
"@types/koa-bodyparser": "^4.2.0",

6
server/src/config.ts

@ -64,7 +64,8 @@ export const ORG_ID = getConfigFieldAndPrintOrExit('ORG_ID', 1);
export enum AlertTypes {
WEBHOOK = 'webhook',
ALERTMANAGER = 'alertmanager'
ALERTMANAGER = 'alertmanager',
SLACK = 'slack'
};
export const HASTIC_ALERT_TYPE = getConfigFieldAndPrintOrExit('HASTIC_ALERT_TYPE', AlertTypes.WEBHOOK, _.values(AlertTypes));
export const HASTIC_ALERT_IMAGE = getConfigFieldAndPrintOrExit('HASTIC_ALERT_IMAGE', false);
@ -74,6 +75,9 @@ export const HASTIC_TIMEZONE_OFFSET = getTimeZoneOffset();
export const HASTIC_ALERTMANAGER_URL = getConfigFieldAndPrintOrExit('HASTIC_ALERTMANAGER_URL', null);
export const HASTIC_SLACK_API_TOKEN = getConfigFieldAndPrintOrExit('HASTIC_SLACK_API_TOKEN', null);
export const HASTIC_SLACK_NOTIFICATION_CHANNEL = getConfigFieldAndPrintOrExit('HASTIC_SLACK_NOTIFICATION_CHANNEL', null);
export const ANALYTICS_PING_INTERVAL = 500; // ms
export const PACKAGE_VERSION = getPackageVersion();
export const GIT_INFO = {

23
server/src/services/alert_service.ts

@ -3,6 +3,7 @@ import * as AnalyticUnit from '../models/analytic_units';
import { Segment } from '../models/segment_model';
import { availableReporter } from '../utils/reporter';
import { toTimeZone } from '../utils/time';
import { getGrafanaUrl } from '../utils/grafana';
import { ORG_ID, HASTIC_API_KEY, HASTIC_ALERT_IMAGE } from '../config';
import axios from 'axios';
@ -20,20 +21,23 @@ export class Alert {
};
protected async send(segment) {
const notification = await this.makeNotification(segment);
const notification = await this.generateNotification(segment);
try {
console.log('sending a notification...');
await Notifier.sendNotification(notification);
console.log('notification is successfully sent');
} catch(error) {
console.error(`can't send notification ${error}`);
};
}
protected async makeNotification(segment: Segment): Promise<Notification> {
protected async generateNotification(segment: Segment): Promise<Notification> {
const meta = this.makeMeta(segment);
const text = this.makeMessage(meta);
let result: Notification = { meta, text };
if(HASTIC_ALERT_IMAGE) {
try {
console.log('Trying to load image for notification');
const image = await this.loadImage();
result.image = image;
} catch(err) {
@ -44,14 +48,15 @@ export class Alert {
return result;
}
protected async loadImage() {
protected async loadImage(): Promise<Buffer> {
const headers = { Authorization: `Bearer ${HASTIC_API_KEY}` };
const dashdoardId = this.analyticUnit.panelId.split('/')[0];
const panelId = this.analyticUnit.panelId.split('/')[1];
const dashboardApiURL = `${this.analyticUnit.grafanaUrl}/api/dashboards/uid/${dashdoardId}`;
const grafanaUrl = getGrafanaUrl(this.analyticUnit.grafanaUrl);
const dashboardApiURL = `${grafanaUrl}/api/dashboards/uid/${dashdoardId}`;
const dashboardInfo: any = await axios.get(dashboardApiURL, { headers });
const dashboardName = _.last(dashboardInfo.data.meta.url.split('/'));
const renderUrl = `${this.analyticUnit.grafanaUrl}/render/d-solo/${dashdoardId}/${dashboardName}`;
const renderUrl = `${grafanaUrl}/render/d-solo/${dashdoardId}/${dashboardName}`;
const params = {
panelId,
ordId: ORG_ID,
@ -63,20 +68,21 @@ export class Alert {
headers,
responseType: 'arraybuffer'
});
return new Buffer(response.data, 'binary').toString('base64');
return new Buffer(response.data, 'binary');
}
protected makeMeta(segment: Segment): AnalyticMeta {
const dashdoardId = this.analyticUnit.panelId.split('/')[0];
const panelId = this.analyticUnit.panelId.split('/')[1];
const grafanaUrl = `${this.analyticUnit.grafanaUrl}/d/${dashdoardId}?panelId=${panelId}&edit=true&fullscreen=true?orgId=${ORG_ID}`;
const grafanaUrl = getGrafanaUrl(this.analyticUnit.grafanaUrl);
const notificationUrl = `${grafanaUrl}/d/${dashdoardId}?panelId=${panelId}&edit=true&fullscreen=true?orgId=${ORG_ID}`;
let alert: AnalyticMeta = {
type: WebhookType.DETECT,
analyticUnitType: this.analyticUnit.type,
analyticUnitName: this.analyticUnit.name,
analyticUnitId: this.analyticUnit.id,
grafanaUrl,
grafanaUrl: notificationUrl,
from: segment.from,
to: segment.to,
message: segment.message
@ -214,6 +220,7 @@ export class AlertService {
to: now
}
console.log('sending a notification...');
Notifier.sendNotification({ text, meta: infoAlert }).catch((err) => {
console.error(`can't send message ${err.message}`);
});

57
server/src/services/notification_service.ts

@ -1,6 +1,7 @@
import * as AnalyticUnit from '../models/analytic_units';
import * as config from '../config';
import { WebClient } from '@slack/web-api';
import axios from 'axios';
import * as _ from 'lodash';
@ -33,7 +34,7 @@ export type AnalyticMeta = {
export declare type Notification = {
text: string,
meta: MetaInfo | AnalyticMeta,
image?: any
image?: Buffer
}
// TODO: split notifiers into 3 files
@ -51,6 +52,10 @@ export function getNotifier(): Notifier {
return new AlertManagerNotifier();
}
if(config.HASTIC_ALERT_TYPE === config.AlertTypes.SLACK) {
return new SlackNotifier();
}
throw new Error(`${config.HASTIC_ALERT_TYPE} alert type not supported`);
}
@ -60,9 +65,12 @@ class WebhookNotifier implements Notifier {
console.log(`HASTIC_WEBHOOK_URL is not set, skip sending notification: ${notification.text}`);
return;
}
notification.text += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`;
const data = JSON.stringify(notification);
const data = JSON.stringify({
...notification,
image: notification.image === undefined ? notification.image : notification.image.toString('base64')
});
const options = {
method: 'POST',
@ -121,7 +129,7 @@ class AlertManagerNotifier implements Notifier {
}
annotations.message += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`;
let alertData: PostableAlert = {
labels,
annotations,
@ -134,7 +142,7 @@ class AlertManagerNotifier implements Notifier {
data: JSON.stringify([alertData]),
headers: { 'Content-Type': 'application/json' }
};
// first part: send "start" request
await axios(options);
// TODO: resolve FAILURE alert only after RECOVERY event
@ -143,3 +151,42 @@ class AlertManagerNotifier implements Notifier {
await axios(options);
}
}
class SlackNotifier implements Notifier {
async sendNotification(notification: Notification) {
if(config.HASTIC_SLACK_API_TOKEN === null) {
console.log(`HASTIC_SLACK_API_TOKEN is not set, skip sending notification: ${notification.text}`);
return;
}
if(config.HASTIC_SLACK_NOTIFICATION_CHANNEL === null) {
console.log(`HASTIC_SLACK_NOTIFICATION_CHANNEL is not set, skip sending notification: ${notification.text}`);
return;
}
notification.text += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`;
const client = new WebClient();
let imageUrl = '';
if(notification.image !== undefined) {
const uploadedFile = await client.files.upload({
channels: config.HASTIC_SLACK_NOTIFICATION_CHANNEL,
file: notification.image,
token: config.HASTIC_SLACK_API_TOKEN
});
if(uploadedFile.file) {
// @ts-ignore
imageUrl = uploadedFile.file.url_private;
}
}
await client.chat.postMessage({
text: notification.text,
attachments: [{ image_url: imageUrl }],
channel: config.HASTIC_SLACK_NOTIFICATION_CHANNEL,
token: config.HASTIC_SLACK_API_TOKEN
});
}
}

Loading…
Cancel
Save