From 5e9433c7a7beb484f347450114cc7f3a6b31ddb1 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 15:58:01 -0500 Subject: [PATCH 01/10] Add and parse basic incidents configuration --- src/app/PagerBeautyWorker.mjs | 38 +++++++++++++++++++++++++---------- src/pagerbeauty.mjs | 4 ++++ 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/app/PagerBeautyWorker.mjs b/src/app/PagerBeautyWorker.mjs index e95fa91..ee8e486 100644 --- a/src/app/PagerBeautyWorker.mjs +++ b/src/app/PagerBeautyWorker.mjs @@ -42,15 +42,22 @@ export class PagerBeautyWorker { this.onCallsTimer = false; this.schedulesTimer = false; - // Parse refresh interval - const refreshRateMinutes = Number(pagerDutyConfig.schedules.refreshRate); - if (Number.isNaN(refreshRateMinutes)) { - throw new PagerBeautyInitError('Refresh rate is not a number'); - } - this.refreshRateMS = refreshRateMinutes * 60 * 1000; - // Requested schedules list. this.schedulesList = pagerDutyConfig.schedules.list; + + // Schedules and on-calls poll interval. + this.schedulesRefreshMS = PagerBeautyWorker.refreshRateToMs( + pagerDutyConfig.schedules.refreshRate, + ); + + // Incidents are optional + this.incidentsEnabled = pagerDutyConfig.incidents.enabled; + // Incidents poll interval. + if (this.incidentsEnabled) { + this.incidentsRefreshMS = PagerBeautyWorker.refreshRateToMs( + pagerDutyConfig.incidents.refreshRate, + ); + } } // ------- Public API ------------------------------------------------------- @@ -85,26 +92,35 @@ export class PagerBeautyWorker { // ------- Internal machinery ----------------------------------------------- async startSchedulesWorker() { - const { refreshRateMS, schedulesList } = this; + const { schedulesRefreshMS, schedulesList } = this; const schedulesTimerTask = new SchedulesTimerTask({ db: this.db, schedulesService: this.schedulesService, schedulesList, }); - this.schedulesTimer = new Timer(schedulesTimerTask, refreshRateMS); + this.schedulesTimer = new Timer(schedulesTimerTask, schedulesRefreshMS); await this.schedulesTimer.start(); } async startOnCallsWorker() { - const { refreshRateMS } = this; + const { schedulesRefreshMS } = this; const onCallsTimerTask = new OnCallsTimerTask({ db: this.db, onCallsService: this.onCallsService, }); - this.onCallsTimer = new Timer(onCallsTimerTask, refreshRateMS); + this.onCallsTimer = new Timer(onCallsTimerTask, schedulesRefreshMS); await this.onCallsTimer.start(); } + static refreshRateToMs(minutesStr) { + // String minutes to integer milliseconds. + const minutes = Number(minutesStr); + if (Number.isNaN(minutes)) { + throw new PagerBeautyInitError(`Incorrect refresh rate: ${minutesStr}`); + } + return minutes * 60 * 1000; + } + // ------- Class end -------------------------------------------------------- } diff --git a/src/pagerbeauty.mjs b/src/pagerbeauty.mjs index 052cee9..d657c95 100644 --- a/src/pagerbeauty.mjs +++ b/src/pagerbeauty.mjs @@ -47,6 +47,10 @@ const config = { list: process.env.PAGERBEAUTY_PD_SCHEDULES.replace(/\s*/g, '').split(','), refreshRate: process.env.PAGERBEAUTY_REFRESH_RATE_MINUTES || 10, }, + incidents: { + enabled: !process.env.PAGERBEAUTY_INCIDENTS_DISABLE, + refreshRate: process.env.PAGERBEAUTY_INCIDENTS_REFRESH_RATE_MINUTES || 1, + }, }, auth: { name: process.env.PAGERBEAUTY_HTTP_USER, From 8ac7ccab547acda686843ecc3bfdff1841b42f53 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 16:35:08 -0500 Subject: [PATCH 02/10] Create IncidentsTimerTask and stub IncidentsService --- docker-compose.yaml | 1 + src/app/PagerBeautyWorker.mjs | 24 +++++++++++- src/services/IncidentsService.mjs | 62 +++++++++++++++++++++++++++++++ src/services/OnCallsService.mjs | 4 +- src/services/PagerDutyClient.mjs | 6 +++ src/tasks/IncidentsTimerTask.mjs | 33 ++++++++++++++++ 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/services/IncidentsService.mjs create mode 100644 src/tasks/IncidentsTimerTask.mjs diff --git a/docker-compose.yaml b/docker-compose.yaml index 64e59c6..f4a468f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,6 +15,7 @@ services: PAGERBEAUTY_PD_SCHEDULES: ${PAGERBEAUTY_PD_SCHEDULES:-P538IZH,PJ1P5JQ,P2RFGIP} # Faster refresh for dev server PAGERBEAUTY_REFRESH_RATE_MINUTES: ${PAGERBEAUTY_REFRESH_RATE_MINUTES:-0.1} + PAGERBEAUTY_INCIDENTS_REFRESH_RATE_MINUTES: ${PAGERBEAUTY_INCIDENTS_REFRESH_RATE_MINUTES:-0.05} ports: - "8080:8080" env_file: diff --git a/src/app/PagerBeautyWorker.mjs b/src/app/PagerBeautyWorker.mjs index ee8e486..2d32a91 100644 --- a/src/app/PagerBeautyWorker.mjs +++ b/src/app/PagerBeautyWorker.mjs @@ -6,8 +6,10 @@ import logger from 'winston'; import { Timer } from './Timer'; import { PagerBeautyInitError } from '../errors'; +import { IncidentsTimerTask } from '../tasks/IncidentsTimerTask'; import { OnCallsTimerTask } from '../tasks/OnCallsTimerTask'; import { SchedulesTimerTask } from '../tasks/SchedulesTimerTask'; +import { IncidentsService } from '../services/IncidentsService'; import { OnCallsService } from '../services/OnCallsService'; import { SchedulesService } from '../services/SchedulesService'; import { PagerDutyClient } from '../services/PagerDutyClient'; @@ -37,10 +39,13 @@ export class PagerBeautyWorker { ); this.onCallsService = new OnCallsService(this.pagerDutyClient); this.schedulesService = new SchedulesService(this.pagerDutyClient); + // Optional + this.incidentsService = false; // Timers this.onCallsTimer = false; this.schedulesTimer = false; + this.incidentsTimer = false; // Requested schedules list. this.schedulesList = pagerDutyConfig.schedules.list; @@ -57,13 +62,14 @@ export class PagerBeautyWorker { this.incidentsRefreshMS = PagerBeautyWorker.refreshRateToMs( pagerDutyConfig.incidents.refreshRate, ); + this.incidentsService = new IncidentsService(this.pagerDutyClient); } } // ------- Public API ------------------------------------------------------- async start() { - const { db } = this; + const { db, incidentsEnabled } = this; logger.debug('Initializing database.'); db.set('oncalls', new Map()); db.set('schedules', new Map()); @@ -73,6 +79,12 @@ export class PagerBeautyWorker { // Then load on-calls. await this.startOnCallsWorker(); + + // Incidents + if (incidentsEnabled) { + // No need to await on incidents. + this.startIncidentsWorker(); + } return true; } @@ -112,6 +124,16 @@ export class PagerBeautyWorker { await this.onCallsTimer.start(); } + async startIncidentsWorker() { + const { incidentsRefreshMS } = this; + const incidentsTimerTask = new IncidentsTimerTask({ + db: this.db, + incidentsService: this.incidentsService, + }); + this.incidentsTimer = new Timer(incidentsTimerTask, incidentsRefreshMS); + await this.incidentsTimer.start(); + } + static refreshRateToMs(minutesStr) { // String minutes to integer milliseconds. const minutes = Number(minutesStr); diff --git a/src/services/IncidentsService.mjs b/src/services/IncidentsService.mjs new file mode 100644 index 0000000..9554c41 --- /dev/null +++ b/src/services/IncidentsService.mjs @@ -0,0 +1,62 @@ +// ------- Imports ------------------------------------------------------------- + +import logger from 'winston'; + +// ------- Internal imports ---------------------------------------------------- + +// import { OnCall } from '../models/OnCall'; + +// ------- IncidentsService ---------------------------------------------------- + +export class IncidentsService { + constructor(pagerDutyClient) { + this.client = pagerDutyClient; + this.incidentsRepo = new Map(); + } + + async load(onCallsService) { + const onCalls = onCallsService.onCallRepo; + if (!onCalls.size) { + logger.verbose('Skipping incidents load: OnCalls not loaded yet'); + return false; + } + + const missingSchedules = new Set(); + + for (const [scheduleId, onCall] of onCalls.entries()) { + try { + // Limit the number of requests by sending them in sync. + // eslint-disable-next-line no-await-in-loop + const record = await this.client.getActiveIncidentForUserOnSchedule( + onCall.userId, + scheduleId, + ); + // console.dir(record, { colors: true, showHidden: true }); + + // const oncall = OnCall.fromApiRecord(record, schedule); + // logger.verbose(`On-call for schedule ${schedule.id} is loaded`); + // logger.silly(`On-call loaded ${oncall.toString()}`); + this.incidentsRepo.set(scheduleId, record); + } catch (e) { + logger.warn( + `Error loading incident for user ${onCall.userId} ` + + `on schedule ${scheduleId}: ${e}`, + ); + } + } + + if (missingSchedules.size) { + logger.warn( + `Missing incidents data for schedules: ${Array.from(missingSchedules).join()}`, + ); + } + return true; + } + + serialize() { + return Array.from(this.incidentsRepo.values(), r => r.serialize()); + } + // ------- Class end -------------------------------------------------------- +} + +// ------- End ----------------------------------------------------------------- diff --git a/src/services/OnCallsService.mjs b/src/services/OnCallsService.mjs index a5fdc45..a954da3 100644 --- a/src/services/OnCallsService.mjs +++ b/src/services/OnCallsService.mjs @@ -44,7 +44,9 @@ export class OnCallsService { } if (missingSchedules.size) { - logger.warn(`Missing data for schedules: ${Array.from(missingSchedules).join()}`); + logger.warn( + `Missing oncall data for schedules: ${Array.from(missingSchedules).join()}`, + ); } return true; } diff --git a/src/services/PagerDutyClient.mjs b/src/services/PagerDutyClient.mjs index fa5cd28..c15534f 100644 --- a/src/services/PagerDutyClient.mjs +++ b/src/services/PagerDutyClient.mjs @@ -105,6 +105,12 @@ export class PagerDutyClient { return record; } + async getActiveIncidentForUserOnSchedule(userId, scheduleId) { + // WIP. + this.get(`incidents/${userId}/${scheduleId}`); + return false; + } + async getSchedule(scheduleId) { const response = await this.get(`schedules/${scheduleId}`); if (response.schedule === undefined || !response.schedule.id) { diff --git a/src/tasks/IncidentsTimerTask.mjs b/src/tasks/IncidentsTimerTask.mjs new file mode 100644 index 0000000..b546e97 --- /dev/null +++ b/src/tasks/IncidentsTimerTask.mjs @@ -0,0 +1,33 @@ +// ------- Imports ------------------------------------------------------------- + +import logger from 'winston'; + +// ------- IncidentsTimerTask -------------------------------------------------- + +export class IncidentsTimerTask { + constructor({ db, incidentsService }) { + this.db = db; + this.incidentsService = incidentsService; + } + + async run(runNumber, intervalMs) { + logger.verbose(`Incidents refresh run #${runNumber}, every ${intervalMs}ms`); + const oncalls = this.db.get('oncalls'); + const result = await this.incidentsService.load(oncalls); + if (result) { + // @todo: refresh without full override. + this.db.set('incidents', this.incidentsService); + } + return result; + } + + onRunSkip() { + logger.warn( + 'Attempting incidents refresh while the previous request is ' + + 'still running. This should not normally happen. Try decreasing ' + + 'refresh rate', + ); + } +} + +// ------- End ----------------------------------------------------------------- From 611d62e1b9871b017a8a8d510f4f7742f1dcf9aa Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 16:48:22 -0500 Subject: [PATCH 03/10] Add PagerDuty incidents empty catch-all response --- src/services/PagerDutyClient.mjs | 28 +++++++++++++++++++++++++--- test/mocks/incidents/GET.mock | 11 +++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 test/mocks/incidents/GET.mock diff --git a/src/services/PagerDutyClient.mjs b/src/services/PagerDutyClient.mjs index c15534f..8180944 100644 --- a/src/services/PagerDutyClient.mjs +++ b/src/services/PagerDutyClient.mjs @@ -106,9 +106,31 @@ export class PagerDutyClient { } async getActiveIncidentForUserOnSchedule(userId, scheduleId) { - // WIP. - this.get(`incidents/${userId}/${scheduleId}`); - return false; + const searchParams = new URLSearchParams([ + ['user_ids[]', userId], + ]); + + // @todo: limit and make sure it's a current one. + const response = await this.get('incidents', searchParams); + if (response.incidents === undefined) { + throw new PagerDutyClientResponseError('Unexpected parsing errors'); + } + + // No incidents. + if (!response.incidents.length) { + return null; + } + + // Active incident for this schedule. + for (const incident of response.incidents) { + // Find the one with the right schedule + if (incident.scheduleId === scheduleId) { + return incident; + } + } + // Not found. + // @todo: Log? + return null; } async getSchedule(scheduleId) { diff --git a/test/mocks/incidents/GET.mock b/test/mocks/incidents/GET.mock new file mode 100644 index 0000000..0728eef --- /dev/null +++ b/test/mocks/incidents/GET.mock @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +Cache-Control: max-age=0, private, must-revalidate + +{ + "incidents": [], + "limit": 25, + "offset": 0, + "total": null, + "more": false +} From cfae4cd35f1823c08a64070e6ae8aa39db02635d Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 17:13:21 -0500 Subject: [PATCH 04/10] Add active incident mock --- .../GET--user_ids%5B%5D=P6MDP9N.mock | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock diff --git a/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock b/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock new file mode 100644 index 0000000..2af66d3 --- /dev/null +++ b/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock @@ -0,0 +1,98 @@ +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +Cache-Control: max-age=0, private, must-revalidate + +{ + "incidents": [ + { + "incident_number": 10279, + "title": "Test incident", + "description": "Test incident", + "created_at": "2019-01-06T21:54:40Z", + "status": "triggered", + "pending_actions": [], + "incident_key": "c8c0d19200534e44bdf6abd0be0059ff", + "service": { + "id": "P1HPKZR", + "type": "service_reference", + "summary": "PagerBeauty", + "self": "https://api.pagerduty.com/services/P1HPKZR", + "html_url": "https://apidocs.pagerduty.com/services/P1HPKZR" + }, + "assignments": [ + { + "at": "2019-01-06T21:54:40Z", + "assignee": { + "id": "P6MDP9N", + "type": "user_reference", + "summary": "Sergii Tkachenko", + "self": "https://api.pagerduty.com/users/P6MDP9N", + "html_url": "https://apidocs.pagerduty.com/users/P6MDP9N" + } + } + ], + "acknowledgements": [], + "last_status_change_at": "2019-01-06T21:54:40Z", + "last_status_change_by": { + "id": "P1HPKZR", + "type": "service_reference", + "summary": "PagerBeauty", + "self": "https://api.pagerduty.com/services/P1HPKZR", + "html_url": "https://apidocs.pagerduty.com/services/P1HPKZR" + }, + "first_trigger_log_entry": { + "id": "R5XBJY83EFH7U7C45HHRDW1DGJ", + "type": "trigger_log_entry_reference", + "summary": "Triggered through the website", + "self": "https://api.pagerduty.com/log_entries/R5XBJY83EFH7U7C45HHRDW1DGJ", + "html_url": "https://apidocs.pagerduty.com/incidents/PTM70NY/log_entries/R5XBJY83EFH7U7C45HHRDW1DGJ" + }, + "escalation_policy": { + "id": "POQTAHN", + "type": "escalation_policy_reference", + "summary": "PagerBeauty Default Escalation", + "self": "https://api.pagerduty.com/escalation_policies/POQTAHN", + "html_url": "https://apidocs.pagerduty.com/escalation_policies/POQTAHN" + }, + "teams": [], + "alert_counts": { + "all": 0, + "triggered": 0, + "resolved": 0 + }, + "impacted_services": [ + { + "id": "P1HPKZR", + "type": "service_reference", + "summary": "PagerBeauty", + "self": "https://api.pagerduty.com/services/P1HPKZR", + "html_url": "https://apidocs.pagerduty.com/services/P1HPKZR" + } + ], + "is_mergeable": true, + "basic_alert_grouping": null, + "alert_grouping": null, + "priority": { + "id": "PR0RYVX", + "type": "priority", + "summary": "P5", + "self": "https://api.pagerduty.com/priorities/PR0RYVX", + "html_url": null, + "name": "P5", + "description": "", + "order": 64, + "color": "666666" + }, + "urgency": "high", + "id": "PTM70NY", + "type": "incident", + "summary": "[#10279] Test incident", + "self": "https://api.pagerduty.com/incidents/PTM70NY", + "html_url": "https://apidocs.pagerduty.com/incidents/PTM70NY" + } + ], + "limit": 25, + "offset": 0, + "total": null, + "more": false +} From 27f4a782b7a67728618c8b019a9d128a2ae6c36f Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 17:20:24 -0500 Subject: [PATCH 05/10] Incidents: load only active --- src/services/PagerDutyClient.mjs | 9 ++++++++- ...ses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock} | 0 2 files changed, 8 insertions(+), 1 deletion(-) rename test/mocks/incidents/{GET--user_ids%5B%5D=P6MDP9N.mock => GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock} (100%) diff --git a/src/services/PagerDutyClient.mjs b/src/services/PagerDutyClient.mjs index 8180944..d015368 100644 --- a/src/services/PagerDutyClient.mjs +++ b/src/services/PagerDutyClient.mjs @@ -26,6 +26,10 @@ export const INCLUDE_USERS = 'users'; export const INCLUDE_SCHEDULES = 'schedules'; export const INCLUDE_ESCALATION_POLICIES = 'escalation_policies'; +export const INCIDENT_STATUS_TRIGGERED = 'triggered'; +export const INCIDENT_STATUS_ACKNOWLEDGED = 'acknowledged'; +export const INCIDENT_STATUS_RESOLVED = 'resolved'; + // ------- PagerDutyClient ----------------------------------------------------- export class PagerDutyClient { @@ -108,9 +112,12 @@ export class PagerDutyClient { async getActiveIncidentForUserOnSchedule(userId, scheduleId) { const searchParams = new URLSearchParams([ ['user_ids[]', userId], + // Active = triggered + acknowledged + ['statuses[]', INCIDENT_STATUS_TRIGGERED], + ['statuses[]', INCIDENT_STATUS_ACKNOWLEDGED], ]); - // @todo: limit and make sure it's a current one. + // @todo: filter by escalation policies const response = await this.get('incidents', searchParams); if (response.incidents === undefined) { throw new PagerDutyClientResponseError('Unexpected parsing errors'); diff --git a/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock b/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock similarity index 100% rename from test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N.mock rename to test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock From 6d233ac1bb4c0a2788be9b7d660afca34afb4154 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 17:35:03 -0500 Subject: [PATCH 06/10] Parse schedule escalationPolicies --- src/models/Schedule.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/models/Schedule.mjs b/src/models/Schedule.mjs index 9d6ceff..e6f4ff4 100644 --- a/src/models/Schedule.mjs +++ b/src/models/Schedule.mjs @@ -12,6 +12,7 @@ export class Schedule { url, summary, description, + escalationPolicies = [], }) { this.id = id; this.name = name; @@ -19,6 +20,7 @@ export class Schedule { this.timezone = timezone; this.summary = summary; this.description = description; + this.escalationPolicies = new Set(escalationPolicies); } serialize() { @@ -29,6 +31,7 @@ export class Schedule { timezone: this.timezone, summary: this.summary, description: this.description, + escalationPolicies: Array.from(this.escalationPolicies), }; } @@ -45,6 +48,10 @@ export class Schedule { summary: record.summary, description: record.description, }; + const escalationPolicies = record.escalation_policies; + if (escalationPolicies) { + attributes.escalationPolicies = escalationPolicies.map(policy => policy.id); + } return new Schedule(attributes); } // ------- Class end -------------------------------------------------------- From cfa358d31ad9bae00463e7de43ad3baf23e83f0b Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 17:52:54 -0500 Subject: [PATCH 07/10] Match on-call with incidents through schedule escalation policy --- src/services/IncidentsService.mjs | 2 +- src/services/PagerDutyClient.mjs | 16 +++++++++++++--- ...ged&limit=100&sort_by=created_at%3Adesc.mock} | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) rename test/mocks/incidents/{GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock => GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged&limit=100&sort_by=created_at%3Adesc.mock} (99%) diff --git a/src/services/IncidentsService.mjs b/src/services/IncidentsService.mjs index 9554c41..ccd76de 100644 --- a/src/services/IncidentsService.mjs +++ b/src/services/IncidentsService.mjs @@ -29,7 +29,7 @@ export class IncidentsService { // eslint-disable-next-line no-await-in-loop const record = await this.client.getActiveIncidentForUserOnSchedule( onCall.userId, - scheduleId, + onCall.schedule.escalationPolicies, ); // console.dir(record, { colors: true, showHidden: true }); diff --git a/src/services/PagerDutyClient.mjs b/src/services/PagerDutyClient.mjs index d015368..91c5ed0 100644 --- a/src/services/PagerDutyClient.mjs +++ b/src/services/PagerDutyClient.mjs @@ -109,7 +109,7 @@ export class PagerDutyClient { return record; } - async getActiveIncidentForUserOnSchedule(userId, scheduleId) { + async getActiveIncidentForUserOnSchedule(userId, scheduleEscalationPolicies) { const searchParams = new URLSearchParams([ ['user_ids[]', userId], // Active = triggered + acknowledged @@ -117,6 +117,13 @@ export class PagerDutyClient { ['statuses[]', INCIDENT_STATUS_ACKNOWLEDGED], ]); + // Set limit to maximum possible value. + // https://v2.developer.pagerduty.com/v2/docs/pagination + searchParams.append('limit', 100); + + // Order: most recent on top. + searchParams.append('sort_by', 'created_at:desc'); + // @todo: filter by escalation policies const response = await this.get('incidents', searchParams); if (response.incidents === undefined) { @@ -131,12 +138,15 @@ export class PagerDutyClient { // Active incident for this schedule. for (const incident of response.incidents) { // Find the one with the right schedule - if (incident.scheduleId === scheduleId) { + const escalationPolicy = incident.escalation_policy.id; + if (!escalationPolicy) { + continue; + } + if (scheduleEscalationPolicies.has(escalationPolicy)) { return incident; } } // Not found. - // @todo: Log? return null; } diff --git a/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock b/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged&limit=100&sort_by=created_at%3Adesc.mock similarity index 99% rename from test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock rename to test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged&limit=100&sort_by=created_at%3Adesc.mock index 2af66d3..ef824bf 100644 --- a/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged.mock +++ b/test/mocks/incidents/GET--user_ids%5B%5D=P6MDP9N&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged&limit=100&sort_by=created_at%3Adesc.mock @@ -91,7 +91,7 @@ Cache-Control: max-age=0, private, must-revalidate "html_url": "https://apidocs.pagerduty.com/incidents/PTM70NY" } ], - "limit": 25, + "limit": 100, "offset": 0, "total": null, "more": false From 0e2cf23726ccd4c02c4c100a107adc6e105635cc Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 18:16:59 -0500 Subject: [PATCH 08/10] Load incidents and expose through on call --- src/models/Incident.mjs | 64 +++++++++++++++++++++++++++++++ src/models/OnCall.mjs | 10 +++++ src/services/IncidentsService.mjs | 24 +++++++++--- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/models/Incident.mjs diff --git a/src/models/Incident.mjs b/src/models/Incident.mjs new file mode 100644 index 0000000..fbc28fe --- /dev/null +++ b/src/models/Incident.mjs @@ -0,0 +1,64 @@ +// ------- Imports ------------------------------------------------------------- + +// This model to be compatible both with backend and frontend. + +// import moment from 'moment-timezone'; + +// ------- Internal imports ---------------------------------------------------- + +// import { OnCall } from './OnCall'; + +// ------- OnCall -------------------------------------------------------------- + +export class Incident { + constructor({ + id, + status, + scheduleId, + title, + summary, + url, + serviceName, + }) { + this.id = id; + this.scheduleId = scheduleId; + this.status = status; + this.title = title; + this.summary = summary; + this.url = url; + this.serviceName = serviceName; + } + + serialize() { + return { + id: this.id, + scheduleId: this.scheduleId, + status: this.status, + title: this.title, + summary: this.summary, + serviceName: this.serviceName, + url: this.url, + }; + } + + toString() { + return JSON.stringify(this.serialize()); + } + + static fromApiRecord(record, scheduleId) { + const attributes = { + id: record.id, + status: record.status, + description: record.description, + title: record.title, + summary: record.summary, + url: record.html_url, + serviceName: record.service ? record.service.summary : 'Unknown', + scheduleId, + }; + return new Incident(attributes); + } + // ------- Class end -------------------------------------------------------- +} + +// ------- End ----------------------------------------------------------------- diff --git a/src/models/OnCall.mjs b/src/models/OnCall.mjs index 606359e..6e21213 100644 --- a/src/models/OnCall.mjs +++ b/src/models/OnCall.mjs @@ -34,6 +34,7 @@ export class OnCall { } else { this.schedule = new Schedule(schedule); } + this.incident = false; } serialize() { @@ -45,6 +46,7 @@ export class OnCall { dateStart: this.dateStart.utc(), dateEnd: this.dateEnd.utc(), schedule: this.schedule.serialize(), + incident: this.incident ? this.incident.serialize() : null, }; } @@ -52,6 +54,14 @@ export class OnCall { return JSON.stringify(this.serialize()); } + setIncident(incident) { + this.incident = incident; + } + + clearIncident() { + this.incident = false; + } + userAvatarSized(size = 2048) { const url = new URL(this.userAvatarURL); const searchParams = new URLSearchParams(url.searchParams); diff --git a/src/services/IncidentsService.mjs b/src/services/IncidentsService.mjs index ccd76de..ddc06a5 100644 --- a/src/services/IncidentsService.mjs +++ b/src/services/IncidentsService.mjs @@ -4,7 +4,7 @@ import logger from 'winston'; // ------- Internal imports ---------------------------------------------------- -// import { OnCall } from '../models/OnCall'; +import { Incident } from '../models/Incident'; // ------- IncidentsService ---------------------------------------------------- @@ -31,12 +31,24 @@ export class IncidentsService { onCall.userId, onCall.schedule.escalationPolicies, ); - // console.dir(record, { colors: true, showHidden: true }); - // const oncall = OnCall.fromApiRecord(record, schedule); - // logger.verbose(`On-call for schedule ${schedule.id} is loaded`); - // logger.silly(`On-call loaded ${oncall.toString()}`); - this.incidentsRepo.set(scheduleId, record); + if (record) { + const incident = Incident.fromApiRecord(record, scheduleId); + this.incidentsRepo.set(scheduleId, incident); + // @todo: log when oncall already has an incident and it's different + onCall.setIncident(incident); + logger.verbose( + `Incident ${incident.id} is active for schedule ${scheduleId}`, + ); + logger.silly(`Incident ${incident.toString()}`); + } else { + // No incidenta, clear. + const existed = this.incidentsRepo.delete(scheduleId); + onCall.clearIncident(); + if (existed) { + logger.verbose(`Incident resolved for schedule ${scheduleId}`); + } + } } catch (e) { logger.warn( `Error loading incident for user ${onCall.userId} ` From 6bd4c4b29c775555134815e5c4587a8833495abb Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 18:26:29 -0500 Subject: [PATCH 09/10] Dust lint --- src/models/Incident.mjs | 6 ------ src/services/IncidentsService.mjs | 2 +- src/services/PagerDutyClient.mjs | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/models/Incident.mjs b/src/models/Incident.mjs index fbc28fe..8643366 100644 --- a/src/models/Incident.mjs +++ b/src/models/Incident.mjs @@ -2,12 +2,6 @@ // This model to be compatible both with backend and frontend. -// import moment from 'moment-timezone'; - -// ------- Internal imports ---------------------------------------------------- - -// import { OnCall } from './OnCall'; - // ------- OnCall -------------------------------------------------------------- export class Incident { diff --git a/src/services/IncidentsService.mjs b/src/services/IncidentsService.mjs index ddc06a5..4f2b060 100644 --- a/src/services/IncidentsService.mjs +++ b/src/services/IncidentsService.mjs @@ -42,7 +42,7 @@ export class IncidentsService { ); logger.silly(`Incident ${incident.toString()}`); } else { - // No incidenta, clear. + // No incidents, clear. const existed = this.incidentsRepo.delete(scheduleId); onCall.clearIncident(); if (existed) { diff --git a/src/services/PagerDutyClient.mjs b/src/services/PagerDutyClient.mjs index 91c5ed0..e7d1cf8 100644 --- a/src/services/PagerDutyClient.mjs +++ b/src/services/PagerDutyClient.mjs @@ -124,7 +124,6 @@ export class PagerDutyClient { // Order: most recent on top. searchParams.append('sort_by', 'created_at:desc'); - // @todo: filter by escalation policies const response = await this.get('incidents', searchParams); if (response.incidents === undefined) { throw new PagerDutyClientResponseError('Unexpected parsing errors'); @@ -136,6 +135,7 @@ export class PagerDutyClient { } // Active incident for this schedule. + // Match incidents with the schedule through escalation policies. for (const incident of response.incidents) { // Find the one with the right schedule const escalationPolicy = incident.escalation_policy.id; From ad3d25b45d8fc201b0dfc15225aa682197e17822 Mon Sep 17 00:00:00 2001 From: Sergii Tkachenko Date: Sun, 6 Jan 2019 18:31:23 -0500 Subject: [PATCH 10/10] Incident configuration docs and default settings --- README.md | 8 ++++++++ docker-compose.circleci.yaml | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8add684..5399d88 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ PAGERBEAUTY_PD_SCHEDULES=SCHEDL1,SCHEDL2 # Default: 10. # PAGERBEAUTY_REFRESH_RATE_MINUTES=10 +# (Optional) Disable polling for active incidents. +# Default: false +# PAGERBEAUTY_INCIDENTS_DISABLE=true + +# (Optional) How often to refresh active incidents, in minutes. +# Default: 1 +# PAGERBEAUTY_INCIDENTS_REFRESH_RATE_MINUTES=5 + # (Optional) Highest logging level to include into application logs. # One of: error, warn, info, verbose, debug, silly # Default: info diff --git a/docker-compose.circleci.yaml b/docker-compose.circleci.yaml index a7c6409..603b1c7 100644 --- a/docker-compose.circleci.yaml +++ b/docker-compose.circleci.yaml @@ -10,8 +10,9 @@ services: PAGERBEAUTY_PD_API_KEY: v2_api_key # Mock Mock PD APInow only responds with these two schedules PAGERBEAUTY_PD_SCHEDULES: P538IZH,PJ1P5JQ,P2RFGIP - # Faster refresh for dev server + # Faster refreshes for dev server PAGERBEAUTY_REFRESH_RATE_MINUTES: 0.1 + PAGERBEAUTY_INCIDENTS_REFRESH_RATE_MINUTES: 0.05 # Verbose PAGERBEAUTY_LOG_LEVEL: silly ports: