Skip to content

Commit

Permalink
feat: instastatus integration (#971)
Browse files Browse the repository at this point in the history
* feat: instastatus integration

* fix: delete pageID in instatus

* fix: create api pageID and components

* fixed error input incidents in instatus

* fix: eslint

* fix: get page id from Monika configuration

* update clean eslint

* feat: change incident status type

* feat: instastatus integration

* fix: delete pageID in instatus

* fix: create api pageID and components

* fixed error input incidents in instatus

* fix: eslint

* fix: get page id from Monika configuration

* create readme notification instatus

* clean eslint

* fix readme notification

* fix: conflict validate

* change description instatus in monika-config-schema

---------

Co-authored-by: Hari Cahya Nugraha <[email protected]>
Co-authored-by: Nico Prananta <[email protected]>
  • Loading branch information
3 people authored Feb 27, 2023
1 parent 089325d commit c2c12d1
Show file tree
Hide file tree
Showing 12 changed files with 417 additions and 0 deletions.
15 changes: 15 additions & 0 deletions db/migrations/20230112152900.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- instatus_page_incidents definition

-- Drop Table
DROP TABLE IF EXISTS instatus_page_incidents;
-- Create Table
CREATE TABLE instatus_page_incidents (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
status TEXT NOT NULL,
url TEXT NOT NULL,
probe_id INTEGER NOT NULL,
incident_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL);

CREATE INDEX instatus_page_incidents_incident_id_IDX ON instatus_page_incidents (incident_id);
21 changes: 21 additions & 0 deletions docs/src/pages/guides/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ At this moment, Monika support these channel of notifications (You can use just
17. [Pushover](https://hyperjumptech.github.io/monika/guides/notifications#pushover)
18. [Opsgenie](https://hyperjumptech.github.io/monika/guides/notifications#opsgenie)
19. [Pushbullet](https://hyperjumptech.github.io/monika/guides/notifications#pushbullet)
20. [Instatus](https://hyperjumptech.github.io/monika/guides/notifications#instatus)

## Configurations

Expand Down Expand Up @@ -510,3 +511,23 @@ notifications:
| -------- | ---------------------------- | ---------------------- |
| token | Pushbullet Access Token | `a6FJVAA0LVJKrT8k` |
| deviceID | Pushbullet Device Identifier | `ujpah72o0sjAoRtnM0jc` |

## Instatus

[Instatus](https://instatus.com/) is a status and incident communication tool. You need a page ID and an API key to use Instatus. You can obtain it by following the steps in [the documentation](https://dashboard.instatus.com/developer).

```yaml
notifications:
- id: unique-id-instatus
type: instatus
data:
apiKey: YOUR_INSTATUS_API_KEY
pageID: YOUR_INSTATUS_PAGE_ID //You can get it with client.pages.get()
```

| Key | Description | Example |
| ---------------- | ---------------------------- | ---------------------------------- |
| id | Notification identity number | `instatus-id` |
| type | Notification types | `instatus` |
| data.apiKey | Instatus API key | `43d43d2c06ae223a88a9c35523acd00a` |
| data.data.pageID | Instatus page ID | `2hu1aj8r6td7mog6uz1sh` |
5 changes: 5 additions & 0 deletions monika.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ probes:
# type: opsgenie
# data:
# geniekey: "genie-key"
# - id: random-string-instatus
# type: instatus
# data:
# apiKey: YOUR_INSTATUS_API_KEY
# pageID: YOUR_INSTATUS_PAGE_ID

# limit log database size in bytes
db_limit:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,8 @@ export const requiredFieldMessages: Record<string, any> = {
token:
'Pushbullet Access Token not found! You can create your Access Token at https://www.pushbullet.com/#settings',
},
instatus: {
apiKey: 'apiKey not found',
pageID: 'pageID not found',
},
}
11 changes: 11 additions & 0 deletions src/components/config/validation/validator/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import {
slug as atlassianStatuspageSlug,
validateConfig as atlassianStatuspageValidateConfig,
} from '../../../../plugins/visualization/atlassian-status-page'
import {
slug as instatusPageSlug,
validateConfig as instatusPageValidateConfig,
} from '../../../../plugins/visualization/instatus'
import { newPagerDuty } from '../../../notification/channel/pagerduty'
import { requiredFieldMessages } from '../notification-required-fields'

Expand Down Expand Up @@ -78,6 +82,13 @@ const checkByNotificationType = (
return pagerduty.validateConfig(notification.data)
}

if (
notification.type === instatusPageSlug &&
instatusPageValidateConfig(notification.data)
) {
return instatusPageValidateConfig(notification.data)
}

const missingField = validateRequiredFields(notification)
if (missingField) {
return missingField
Expand Down
3 changes: 3 additions & 0 deletions src/components/logger/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ export async function deleteNotificationLogs(
export async function flushAllLogs(): Promise<void> {
const dropAtlassianStatusPageTableSQL =
'DROP TABLE IF EXISTS atlassian_status_page_incidents;'
const dropInstatusPageTableSQL =
'DROP TABLE IF EXISTS instatus_page_incidents;'
const dropProbeRequestsTableSQL = 'DROP TABLE IF EXISTS probe_requests;'
const dropAlertsTableSQL = 'DROP TABLE IF EXISTS alerts;'
const dropNotificationsTableSQL = 'DROP TABLE IF EXISTS notifications;'
Expand All @@ -419,6 +421,7 @@ export async function flushAllLogs(): Promise<void> {
db.run(dropAlertsTableSQL),
db.run(dropNotificationsTableSQL),
db.run(dropMigrationsTableSQL),
db.run(dropInstatusPageTableSQL),

// The VACUUM command cleans the main database by copying its contents to a temporary database file and reloading the original database file from the copy.
// This eliminates free pages, aligns table data to be contiguous, and otherwise cleans up the database file structure.
Expand Down
2 changes: 2 additions & 0 deletions src/components/notification/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { hostname } from 'os'
import { NotificationSendingError, sendNotifications } from '.'
import { Notification } from '../../interfaces/notification'
import { validator as dataStatuspageSchemaValidator } from '../../plugins/visualization/atlassian-status-page'
import { validator as dataInstatusSchemaValidator } from '../../plugins/visualization/instatus'
import getIp from '../../utils/ip'
import { getMessageForStart } from './alert-message'
import { newPagerDuty } from './channel/pagerduty'
Expand Down Expand Up @@ -78,6 +79,7 @@ export const notificationChecker = async (
pushover: dataPushoverSchemaValidator,
gotify: dataGotifySchemaValidator,
pushbullet: dataPushbulletSchemaValidator,
instatus: dataInstatusSchemaValidator,
}

await Promise.all(
Expand Down
32 changes: 32 additions & 0 deletions src/events/subscribers/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import events from '../../events'
import type { Notification } from '../../interfaces/notification'
import type { StatuspageNotification } from '../../plugins/visualization/atlassian-status-page'
import { AtlassianStatusPageAPI } from '../../plugins/visualization/atlassian-status-page'
import type { InstatusPageNotification } from '../../plugins/visualization/instatus'
import { InstatusPageAPI } from '../../plugins/visualization/instatus'
import { getEventEmitter } from '../../utils/events'
import { log } from '../../utils/pino'

Expand Down Expand Up @@ -64,6 +66,36 @@ eventEmitter.on(
)
}
}

const isInstatuspageEnable: InstatusPageNotification | undefined =
notifications.find(
(notification: Notification) => notification.type === 'instatus'
)

if (!isNotificationEmpty && isInstatuspageEnable) {
const { apiKey, pageID } = isInstatuspageEnable.data
const instatusPageAPI = new InstatusPageAPI(apiKey, pageID)
const type = getNotificationType(probeState)

try {
if (!type) {
throw new Error(`probeState ${probeState} is unknown`)
}

const incidentID = await instatusPageAPI.notify({
probeID,
type,
url,
})
log.info(
`Instatus page (${type}). id: ${incidentID}, probeID: ${probeID}, url: ${url}`
)
} catch (error: any) {
log.error(
`Instatus page (Error). probeID: ${probeID}, url: ${url}, probeState: ${probeState} error: ${error}`
)
}
}
}
)

Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import { PagerDutyNotification } from '../components/notification/channel/pagerduty'
import type { StatuspageNotification } from '../plugins/visualization/atlassian-status-page'
import type { InstatusPageNotification } from '../plugins/visualization/instatus'
import {
MailgunData,
MonikaNotifData,
Expand Down Expand Up @@ -167,6 +168,7 @@ export type Notification =
| OpsgenieNotification
| StatuspageNotification
| PushbulletNotification
| InstatusPageNotification

interface BaseNotificationMessageMeta {
type: string
Expand Down
32 changes: 32 additions & 0 deletions src/monika-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,38 @@
}
}
}
},
{
"title": "Instatus",
"type": "object",
"required": ["id", "type", "data"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"description": "The type of notification",
"default": "instatus"
},
"type": {
"const": "instatus"
},
"data": {
"type": "object",
"description": "Data for your payload",
"additionalProperties": false,
"required": ["apiKey", "pageID"],
"properties": {
"apiKey": {
"type": "string",
"description": "Instatus API key"
},
"pageID": {
"type": "string",
"description": "Instatus Page ID"
}
}
}
}
}
]
}
Expand Down
62 changes: 62 additions & 0 deletions src/plugins/visualization/instatus/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { db } from '../../../components/logger/history'

type Incident = {
id: string
status: string
url: string
probeID: string
incidentID: string
}
type InsertIncident = Omit<Incident, 'id'>
type UpdateIncident = Pick<Incident, 'incidentID' | 'status'>
type FindIncident = Pick<Incident, 'probeID' | 'status' | 'url'>

type FindIncidentResponse = {
// eslint-disable-next-line camelcase
incident_id: string
}

export async function insertIncident({
status,
url,
probeID,
incidentID,
}: InsertIncident): Promise<void> {
const dateNow = Math.round(Date.now() / 1000)
const sqlStatement = `INSERT INTO instatus_page_incidents (
status,
url,
probe_id,
incident_id,
created_at,
updated_at
) VALUES (?, ?, ?, ?, ?, ?);`
const sqlParams = [status, url, probeID, incidentID, dateNow, dateNow]

await db.run(sqlStatement, sqlParams)
}

export async function updateIncident({
incidentID,
status,
}: UpdateIncident): Promise<void> {
const dateNow = Math.round(Date.now() / 1000)
const sqlStatement = `UPDATE instatus_page_incidents SET status = ?, updated_at = ?
WHERE incident_id = ?`
const sqlParams = [status, dateNow, incidentID]

await db.run(sqlStatement, sqlParams)
}

export async function findIncident({
probeID,
status,
url,
}: FindIncident): Promise<FindIncidentResponse | undefined> {
const sqlStatement = `SELECT incident_id FROM instatus_page_incidents
WHERE status = ? AND url = ? AND probe_id = ?`
const sqlParams = [status, url, probeID]
const incident = await db.get<FindIncidentResponse>(sqlStatement, sqlParams)

return incident
}
Loading

0 comments on commit c2c12d1

Please sign in to comment.