diff --git a/src/components/probe/prober/http/index.ts b/src/components/probe/prober/http/index.ts index 0ffa8551b..faf6f9bed 100644 --- a/src/components/probe/prober/http/index.ts +++ b/src/components/probe/prober/http/index.ts @@ -23,23 +23,24 @@ * SOFTWARE. * **********************************************************************************/ +import { BaseProber, NotificationType } from '..' import { getContext } from '../../../../context' import events from '../../../../events' -import { getEventEmitter } from '../../../../utils/events' -import { httpRequest } from './request' -import { BaseProber, NotificationType } from '..' +import type { ProbeAlert } from '../../../../interfaces/probe' import { - type ProbeRequestResponse, probeRequestResult, + type ProbeRequestResponse, type RequestConfig, } from '../../../../interfaces/request' -import type { ProbeAlert } from '../../../../interfaces/probe' -import responseChecker from '../../../../plugins/validate-response/checkers' -import { logResponseTime } from '../../../logger/response-time-log' -import { saveProbeRequestLog } from '../../../logger/history' import type { ValidatedResponse } from '../../../../plugins/validate-response' +import responseChecker from '../../../../plugins/validate-response/checkers' +import { getAlertID } from '../../../../utils/alert-id' +import { getEventEmitter } from '../../../../utils/events' import { isSymonModeFrom } from '../../../config' import { startDowntimeCounter } from '../../../downtime-counter' +import { saveProbeRequestLog } from '../../../logger/history' +import { logResponseTime } from '../../../logger/response-time-log' +import { httpRequest } from './request' type ProbeResultMessageParams = { request: RequestConfig @@ -185,6 +186,15 @@ export class HTTPProber extends BaseProber { requestIndex: number, triggeredAlert: ProbeAlert ) { + const probeID = this.probeConfig.id + const url = this.probeConfig?.requests?.[requestIndex].url || '' + const validation = { + alert: triggeredAlert, + isAlertTriggered: true, + response, + } + const alertId = getAlertID(url, validation, probeID) + getEventEmitter().emit(events.probe.alert.triggered, { probe: this.probeConfig, requestIndex, @@ -193,18 +203,15 @@ export class HTTPProber extends BaseProber { startDowntimeCounter({ alert: triggeredAlert, - probeID: this.probeConfig.id, - url: this.probeConfig?.requests?.[requestIndex].url || '', + probeID, + url, }) this.sendNotification({ - requestURL: this.probeConfig?.requests?.[requestIndex].url || '', + requestURL: url, notificationType: NotificationType.Incident, - validation: { - alert: triggeredAlert, - isAlertTriggered: true, - response, - }, + validation, + alertId, }) this.logMessage( diff --git a/src/components/probe/prober/index.ts b/src/components/probe/prober/index.ts index 18fad03e3..565780a11 100644 --- a/src/components/probe/prober/index.ts +++ b/src/components/probe/prober/index.ts @@ -23,29 +23,30 @@ **********************************************************************************/ import type { Notification } from '@hyperjumptech/monika-notification' -import { type Incident, getContext } from '../../../context' +import { getContext, type Incident } from '../../../context' import events from '../../../events' import type { Probe, ProbeAlert } from '../../../interfaces/probe' import { probeRequestResult, type ProbeRequestResponse, } from '../../../interfaces/request' +import { FAILED_REQUEST_ASSERTION } from '../../../looper' +import type { ValidatedResponse } from '../../../plugins/validate-response' +import { getAlertID } from '../../../utils/alert-id' import { getEventEmitter } from '../../../utils/events' import { log } from '../../../utils/pino' import { isSymonModeFrom } from '../../config' -import { sendAlerts } from '../../notification' -import { saveNotificationLog, saveProbeRequestLog } from '../../logger/history' -import { logResponseTime } from '../../logger/response-time-log' -import type { ValidatedResponse } from '../../../plugins/validate-response' -import { - startDowntimeCounter, - stopDowntimeCounter, -} from '../../downtime-counter' -import { FAILED_REQUEST_ASSERTION } from '../../../looper' import { DEFAULT_INCIDENT_THRESHOLD, DEFAULT_RECOVERY_THRESHOLD, } from '../../config/validation/validator/default-values' +import { + startDowntimeCounter, + stopDowntimeCounter, +} from '../../downtime-counter' +import { saveNotificationLog, saveProbeRequestLog } from '../../logger/history' +import { logResponseTime } from '../../logger/response-time-log' +import { sendAlerts } from '../../notification' export type ProbeResult = { isAlertTriggered: boolean @@ -62,6 +63,7 @@ type SendNotificationParams = { requestURL: string notificationType: NotificationType validation: ValidatedResponse + alertId: string } export interface Prober { @@ -240,8 +242,8 @@ export class BaseProber implements Prober { protected throwIncidentIfNeeded( incidentRetryAttempt: number, incidentThreshold: number = DEFAULT_INCIDENT_THRESHOLD, - message: string = 'Probing failed' - ) { + message = 'Probing failed' + ): void { const isIncidentThresholdMet = incidentRetryAttempt === incidentThreshold - 1 @@ -262,6 +264,7 @@ export class BaseProber implements Prober { requestURL, notificationType, validation, + alertId, }: SendNotificationParams): Promise { const isRecoveryNotification = notificationType === NotificationType.Recover getEventEmitter().emit(events.probe.notification.willSend, { @@ -270,6 +273,7 @@ export class BaseProber implements Prober { url: requestURL, probeState: isRecoveryNotification ? ProbeState.Up : ProbeState.Down, validation, + alertId, }) if (!this.hasNotification()) { @@ -337,14 +341,21 @@ export class BaseProber implements Prober { error: requestResponse.errMessage, }) + const url = this.probeConfig?.requests?.[requestIndex].url || '' + const validation = { + alert: failedRequestAssertion, + isAlertTriggered: true, + response: requestResponse, + } + const probeID = this.probeConfig.id + + const alertId = getAlertID(url, validation, probeID) + this.sendNotification({ - requestURL: this.probeConfig?.requests?.[requestIndex].url || '', + requestURL: url, notificationType: NotificationType.Incident, - validation: { - alert: failedRequestAssertion, - isAlertTriggered: true, - response: requestResponse, - }, + validation: validation, + alertId, }).catch((error) => log.error(error.mesage)) } @@ -366,14 +377,21 @@ export class BaseProber implements Prober { url: this.probeConfig?.requests?.[requestIndex].url || '', }) + const url = this.probeConfig?.requests?.[requestIndex].url || '' + const validation = { + alert: recoveredIncident.alert, + isAlertTriggered: false, + response: probeResults[requestIndex].requestResponse, + } + const probeID = this.probeConfig.id + + const alertId = getAlertID(url, validation, probeID) + this.sendNotification({ - requestURL: this.probeConfig?.requests?.[requestIndex].url || '', + requestURL: url, notificationType: NotificationType.Recover, - validation: { - alert: recoveredIncident.alert, - isAlertTriggered: false, - response: probeResults[requestIndex].requestResponse, - }, + validation: validation, + alertId, }).catch((error) => log.error(error.mesage)) } diff --git a/src/events/subscribers/probe.ts b/src/events/subscribers/probe.ts index b346e8a05..a6e9a466a 100644 --- a/src/events/subscribers/probe.ts +++ b/src/events/subscribers/probe.ts @@ -22,8 +22,8 @@ * SOFTWARE. * **********************************************************************************/ -import events from '../../events' import type { Notification } from '@hyperjumptech/monika-notification' +import events from '../../events' import type { StatuspageNotification } from '../../plugins/visualization/atlassian-status-page' import { AtlassianStatusPageAPI } from '../../plugins/visualization/atlassian-status-page' import { getEventEmitter } from '../../utils/events' diff --git a/src/symon/index.test.ts b/src/symon/index.test.ts index c89d35e67..4f885436a 100644 --- a/src/symon/index.test.ts +++ b/src/symon/index.test.ts @@ -27,13 +27,13 @@ import { rest } from 'msw' import { setupServer } from 'msw/node' import sinon from 'sinon' -import type { Config } from '../interfaces/config' import type { MonikaFlags } from '../flag' +import type { Config } from '../interfaces/config' -import * as loggerHistory from '../components/logger/history' -import { setContext } from '../context' import SymonClient from '.' import { validateProbes } from '../components/config/validation' +import * as loggerHistory from '../components/logger/history' +import { setContext } from '../context' let getUnreportedLogsStub: sinon.SinonStub diff --git a/src/symon/index.ts b/src/symon/index.ts index 8f976c67e..f1cb13d46 100644 --- a/src/symon/index.ts +++ b/src/symon/index.ts @@ -31,10 +31,11 @@ import path from 'path' import Piscina from 'piscina' import { updateConfig } from '../components/config' +import { validateProbes } from '../components/config/validation' import { getOSName } from '../components/notification/alert-message' import { getContext } from '../context' -import { SYMON_API_VERSION, type MonikaFlags } from '../flag' import events from '../events' +import { SYMON_API_VERSION, type MonikaFlags } from '../flag' import { Config } from '../interfaces/config' import { Probe } from '../interfaces/probe' import { ValidatedResponse } from '../plugins/validate-response' @@ -48,7 +49,6 @@ import { publicIpAddress, publicNetworkInfo, } from '../utils/public-ip' -import { validateProbes } from '../components/config/validation' type SymonHandshakeData = { city: string @@ -82,6 +82,7 @@ type NotificationEvent = { probeState: string url: string validation: ValidatedResponse + alertId: string } type ConfigListener = (config: Config) => void @@ -218,43 +219,8 @@ class SymonClient { this.eventEmitter = getEventEmitter() this.eventEmitter.on( events.probe.notification.willSend, - ({ probeID, probeState, url, validation }: NotificationEvent) => { - const getAlertID = ({ - url, - validation, - }: Pick): string => { - if (validation.alert.id) { - return validation.alert.id - } - - const probe = getContext().config?.probes.find( - ({ id }) => id === probeID - ) - if (!probe) { - return '' - } - - const request = probe.requests?.find((request) => request.url === url) - if (!request) { - return '' - } - - return request.alerts?.find((alert) => alert.query === '')?.id || '' - } - - this.notifyEvent({ - alertId: getAlertID({ url, validation }), - event: probeState === 'DOWN' ? 'incident' : 'recovery', - response: { - body: validation.response.data, - headers: validation.response.headers || {}, - size: validation.response.headers['content-length'], - status: validation.response.status, // status is http status code - time: validation.response.responseTime, - }, - }).catch((error: unknown) => { - log.error(error) - }) + ({ probeState, validation, alertId }: NotificationEvent) => { + this.willSendNotification(probeState, validation, alertId) } ) @@ -270,6 +236,26 @@ class SymonClient { this.onConfig((config) => updateConfig(config, false)) } + willSendNotification( + probeState: string, + validation: ValidatedResponse, + alertId: string + ): void { + this.notifyEvent({ + alertId, + event: probeState === 'DOWN' ? 'incident' : 'recovery', + response: { + body: validation.response.data, + headers: validation.response.headers || {}, + size: validation.response.headers['content-length'], + status: validation.response.status, // status is http status code + time: validation.response.responseTime, + }, + }).catch((error: unknown) => { + log.error(error) + }) + } + async notifyEvent(event: SymonClientEvent): Promise { log.debug('Sending incident/recovery event to Symon') await this.httpClient.post('/events', { monikaId: this.monikaId, ...event }) diff --git a/src/utils/alert-id.ts b/src/utils/alert-id.ts new file mode 100644 index 000000000..e87263aed --- /dev/null +++ b/src/utils/alert-id.ts @@ -0,0 +1,48 @@ +/********************************************************************************** + * MIT License * + * * + * Copyright (c) 2021 Hyperjump Technology * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy * + * of this software and associated documentation files (the "Software"), to deal * + * in the Software without restriction, including without limitation the rights * + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * + * copies of the Software, and to permit persons to whom the Software is * + * furnished to do so, subject to the following conditions: * + * * + * The above copyright notice and this permission notice shall be included in all * + * copies or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * + * SOFTWARE. * + **********************************************************************************/ + +import { getContext } from '../context' +import type { ValidatedResponse } from '../plugins/validate-response' + +export function getAlertID( + url: string, + validation: ValidatedResponse, + probeID: string +): string { + if (validation.alert.id) { + return validation.alert.id + } + + const probe = getContext().config?.probes.find(({ id }) => id === probeID) + if (!probe) { + return '' + } + + const request = probe.requests?.find((request) => request.url === url) + if (!request) { + return '' + } + + return request.alerts?.find((alert) => alert.query === '')?.id || '' +} diff --git a/src/utils/expression-parser.ts b/src/utils/expression-parser.ts index 29ba3dcbe..83724688d 100644 --- a/src/utils/expression-parser.ts +++ b/src/utils/expression-parser.ts @@ -24,15 +24,15 @@ import { compileExpression as _compileExpression } from 'filtrex' import { + endsWith, get, has, - endsWith, - startsWith, - lowerCase, - upperCase, - size, includes, isEmpty, + lowerCase, + size, + startsWith, + upperCase, } from 'lodash' // wrap substrings that are object accessor with double quote