src/platform/monitoring/DowntimeNotification/util/helpers.js
import camelCaseKeysRecursive from 'camelcase-keys-recursive';
import moment from 'moment';
import * as Sentry from '@sentry/browser';
import environment from 'platform/utilities/environment';
import ENVIRONMENTS from 'site/constants/environments';
import externalServiceStatus from '../config/externalServiceStatus';
import defaultExternalServices from '../config/externalServices';
/**
* Derives downtime status based on a time range
* @param {string|Date|Moment} startTime
* @param {string|Date|Moment} endTime
* @returns {string} A service status
*/
export function getStatusForTimeframe(startTime, endTime) {
const now = moment();
const hasStarted = now.isSameOrAfter(startTime);
if (hasStarted) {
// Check for indefinite downtime (null endTime) or that the endTime is in the future
if (!endTime || now.isBefore(endTime)) {
return externalServiceStatus.down;
}
// The downtime must be old and outdated. The API should filter these so this shouldn't happen.
return externalServiceStatus.ok;
}
const startsWithinHour = now.add(1, 'hour').isSameOrAfter(startTime);
if (startsWithinHour) return externalServiceStatus.downtimeApproaching;
return externalServiceStatus.ok;
}
export function createGlobalMaintenanceWindow({
startTime,
endTime,
externalServices = defaultExternalServices,
}) {
return [
{
attributes: {
externalService: 'global',
startTime,
endTime,
},
},
...Object.keys(externalServices).map(externalService => ({
attributes: {
externalService,
startTime,
endTime,
},
})),
];
}
/**
* Creates a Map of downtime information using the "externalService" property as keys
* @param {Array} maintenanceWindows The raw JSON data from the API
* @returns {Map}
*/
export function createServiceMap(maintenanceWindows = []) {
const serviceMap = new Map();
// Maintenance windows should be sorted in ascending order
// so that when a single externalService has multiple upcoming
// maintenance windows, we can easily grab the one with the
// earliest startTime and ignore any others that we encounter
// Expected format for `attributes.startTime`: YYYY-MM-DDTHH:MM:SS.SSSZ
const sortedMaintenanceWindows = maintenanceWindows.sort((a, b) => {
const aStart = a.attributes.startTime;
const bStart = b.attributes.startTime;
return aStart.localeCompare(bStart);
});
for (const maintenanceWindow of sortedMaintenanceWindows) {
const {
attributes: {
externalService,
startTime: startTimeRaw,
endTime: endTimeRaw,
description,
},
} = maintenanceWindow;
const startTime = moment(startTimeRaw);
const endTime = endTimeRaw && moment(endTimeRaw);
const status = getStatusForTimeframe(startTime, endTime);
// For each externalService, we only care about the maintenance
// window with the earliest startTime (the sorting above should
// guarantee that the first one we encounter has the earliest
// startTime)
if (!serviceMap.has(externalService)) {
serviceMap.set(externalService, {
externalService,
status,
startTime,
endTime,
description,
});
}
}
return serviceMap;
}
/**
* Determines the downtime with the soonest startTime by using a service map to look up downtime information for each service in a list of service names
* @param {Map} serviceMap A Map as created by createServiceMap
* @param {Array<string>} serviceNames A list of external services
* @returns {object} A downtime object containing properties "externalService", "status", "startTime", and "endTime"
*/
export function getSoonestDowntime(serviceMap, serviceNames) {
return serviceNames
.map(serviceName => serviceMap.get(serviceName))
.filter(service => !!service)
.filter(service => service.status !== externalServiceStatus.ok)
.reduce((mostUrgentService, service) => {
if (!mostUrgentService) return service;
return mostUrgentService.startTime.isBefore(service.startTime)
? mostUrgentService
: service;
}, null);
}
/**
* Retrieves a list of global downtimes from a cached JSON file and gets the
* downtime that includes the current time.
*
* The file is generated by a vets-api job that pulls the global maintenance
* windows from PagerDuty every hour. The content is in JSON, structured as an
* array of maintenance window objects with the follow attributes:
* pagerduty_id, external_service, start_time, end_time, description
*
* @returns {object} A global downtime window that covers the current time
* if it exists and null if not
*/
export const getCurrentGlobalDowntime = (() => {
const BUCKET_BASE_URL = 's3-us-gov-west-1.amazonaws.com';
const MAINTENANCE_WINDOWS_SUBDOMAINS = Object.freeze({
[ENVIRONMENTS.VAGOVDEV]: 'dev-va-gov-maintenance-windows',
[ENVIRONMENTS.VAGOVSTAGING]: 'staging-va-gov-maintenance-windows',
[ENVIRONMENTS.VAGOVPROD]: 'prod-va-gov-maintenance-windows',
});
const subdomain = MAINTENANCE_WINDOWS_SUBDOMAINS[environment.BUILDTYPE];
const maintenanceWindowsUrl = subdomain
? `https://${subdomain}.${BUCKET_BASE_URL}/maintenance_windows.json`
: null;
const includesCurrentTime = ({ startTime, endTime }) =>
moment().isAfter(startTime) && moment().isBefore(endTime);
return async () => {
try {
const response = await fetch(maintenanceWindowsUrl);
const data = camelCaseKeysRecursive(await response.json());
return data.find(includesCurrentTime) || null;
} catch (error) {
Sentry.withScope(scope => {
scope.setExtra('error', error);
Sentry.captureMessage('Error fetching maintenance windows file');
});
return null;
}
};
})();