Browse Source

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

pull/1/head
rozetko 4 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": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"@corpglory/tsdb-kit": "^1.1.1", "@corpglory/tsdb-kit": "^1.1.1",
"@slack/web-api": "^6.0.0",
"@types/jest": "^23.3.14", "@types/jest": "^23.3.14",
"@types/koa": "^2.0.46", "@types/koa": "^2.0.46",
"@types/koa-bodyparser": "^4.2.0", "@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 { export enum AlertTypes {
WEBHOOK = 'webhook', 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_TYPE = getConfigFieldAndPrintOrExit('HASTIC_ALERT_TYPE', AlertTypes.WEBHOOK, _.values(AlertTypes));
export const HASTIC_ALERT_IMAGE = getConfigFieldAndPrintOrExit('HASTIC_ALERT_IMAGE', false); 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_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 ANALYTICS_PING_INTERVAL = 500; // ms
export const PACKAGE_VERSION = getPackageVersion(); export const PACKAGE_VERSION = getPackageVersion();
export const GIT_INFO = { 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 { Segment } from '../models/segment_model';
import { availableReporter } from '../utils/reporter'; import { availableReporter } from '../utils/reporter';
import { toTimeZone } from '../utils/time'; import { toTimeZone } from '../utils/time';
import { getGrafanaUrl } from '../utils/grafana';
import { ORG_ID, HASTIC_API_KEY, HASTIC_ALERT_IMAGE } from '../config'; import { ORG_ID, HASTIC_API_KEY, HASTIC_ALERT_IMAGE } from '../config';
import axios from 'axios'; import axios from 'axios';
@ -20,20 +21,23 @@ export class Alert {
}; };
protected async send(segment) { protected async send(segment) {
const notification = await this.makeNotification(segment); const notification = await this.generateNotification(segment);
try { try {
console.log('sending a notification...');
await Notifier.sendNotification(notification); await Notifier.sendNotification(notification);
console.log('notification is successfully sent');
} catch(error) { } catch(error) {
console.error(`can't send notification ${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 meta = this.makeMeta(segment);
const text = this.makeMessage(meta); const text = this.makeMessage(meta);
let result: Notification = { meta, text }; let result: Notification = { meta, text };
if(HASTIC_ALERT_IMAGE) { if(HASTIC_ALERT_IMAGE) {
try { try {
console.log('Trying to load image for notification');
const image = await this.loadImage(); const image = await this.loadImage();
result.image = image; result.image = image;
} catch(err) { } catch(err) {
@ -44,14 +48,15 @@ export class Alert {
return result; return result;
} }
protected async loadImage() { protected async loadImage(): Promise<Buffer> {
const headers = { Authorization: `Bearer ${HASTIC_API_KEY}` }; const headers = { Authorization: `Bearer ${HASTIC_API_KEY}` };
const dashdoardId = this.analyticUnit.panelId.split('/')[0]; const dashdoardId = this.analyticUnit.panelId.split('/')[0];
const panelId = this.analyticUnit.panelId.split('/')[1]; 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 dashboardInfo: any = await axios.get(dashboardApiURL, { headers });
const dashboardName = _.last(dashboardInfo.data.meta.url.split('/')); 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 = { const params = {
panelId, panelId,
ordId: ORG_ID, ordId: ORG_ID,
@ -63,20 +68,21 @@ export class Alert {
headers, headers,
responseType: 'arraybuffer' responseType: 'arraybuffer'
}); });
return new Buffer(response.data, 'binary').toString('base64'); return new Buffer(response.data, 'binary');
} }
protected makeMeta(segment: Segment): AnalyticMeta { protected makeMeta(segment: Segment): AnalyticMeta {
const dashdoardId = this.analyticUnit.panelId.split('/')[0]; const dashdoardId = this.analyticUnit.panelId.split('/')[0];
const panelId = this.analyticUnit.panelId.split('/')[1]; 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 = { let alert: AnalyticMeta = {
type: WebhookType.DETECT, type: WebhookType.DETECT,
analyticUnitType: this.analyticUnit.type, analyticUnitType: this.analyticUnit.type,
analyticUnitName: this.analyticUnit.name, analyticUnitName: this.analyticUnit.name,
analyticUnitId: this.analyticUnit.id, analyticUnitId: this.analyticUnit.id,
grafanaUrl, grafanaUrl: notificationUrl,
from: segment.from, from: segment.from,
to: segment.to, to: segment.to,
message: segment.message message: segment.message
@ -214,6 +220,7 @@ export class AlertService {
to: now to: now
} }
console.log('sending a notification...');
Notifier.sendNotification({ text, meta: infoAlert }).catch((err) => { Notifier.sendNotification({ text, meta: infoAlert }).catch((err) => {
console.error(`can't send message ${err.message}`); 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 AnalyticUnit from '../models/analytic_units';
import * as config from '../config'; import * as config from '../config';
import { WebClient } from '@slack/web-api';
import axios from 'axios'; import axios from 'axios';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -33,7 +34,7 @@ export type AnalyticMeta = {
export declare type Notification = { export declare type Notification = {
text: string, text: string,
meta: MetaInfo | AnalyticMeta, meta: MetaInfo | AnalyticMeta,
image?: any image?: Buffer
} }
// TODO: split notifiers into 3 files // TODO: split notifiers into 3 files
@ -51,6 +52,10 @@ export function getNotifier(): Notifier {
return new AlertManagerNotifier(); 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`); 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}`); console.log(`HASTIC_WEBHOOK_URL is not set, skip sending notification: ${notification.text}`);
return; return;
} }
notification.text += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`; 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 = { const options = {
method: 'POST', method: 'POST',
@ -121,7 +129,7 @@ class AlertManagerNotifier implements Notifier {
} }
annotations.message += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`; annotations.message += `\nInstance: ${config.HASTIC_INSTANCE_NAME}`;
let alertData: PostableAlert = { let alertData: PostableAlert = {
labels, labels,
annotations, annotations,
@ -134,7 +142,7 @@ class AlertManagerNotifier implements Notifier {
data: JSON.stringify([alertData]), data: JSON.stringify([alertData]),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}; };
// first part: send "start" request // first part: send "start" request
await axios(options); await axios(options);
// TODO: resolve FAILURE alert only after RECOVERY event // TODO: resolve FAILURE alert only after RECOVERY event
@ -143,3 +151,42 @@ class AlertManagerNotifier implements Notifier {
await axios(options); 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