diff --git a/server/package.json b/server/package.json index d432e3b..2a83158 100644 --- a/server/package.json +++ b/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", diff --git a/server/src/config.ts b/server/src/config.ts index a6337e7..41381f5 100644 --- a/server/src/config.ts +++ b/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 = { diff --git a/server/src/services/alert_service.ts b/server/src/services/alert_service.ts index 096ab4d..b50209b 100644 --- a/server/src/services/alert_service.ts +++ b/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 { + protected async generateNotification(segment: Segment): Promise { 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 { 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}`); }); diff --git a/server/src/services/notification_service.ts b/server/src/services/notification_service.ts index f0c312e..8c9fe93 100644 --- a/server/src/services/notification_service.ts +++ b/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 + }); + } +}