UNPKG

@statusfy/core

Version:
347 lines (297 loc) 10.3 kB
const { readdir, stat, readFile } = require("fs"); const { promisify } = require("util"); const spawn = require("cross-spawn"); const drop = require("lodash.drop"); const sortBy = require("lodash.sortby"); const createMarkdown = require("@statusfy/markdown"); const { logger, hash, path, fse, LRU, Dates } = require("@statusfy/common"); const dates = Dates(); const readdirP = promisify(readdir); const statP = promisify(stat); const readFileP = promisify(readFile); const md = createMarkdown(); const cache = new LRU({ max: 1000, maxAge: process.env.NODE_ENV !== "development" ? 1000 * 60 * 60 : 1 }); const getGitLastUpdatedTimeStamp = filepath => { const { stdout } = spawn.sync("git", ["log", "-1", "--format=%ct", filepath]); return parseInt(stdout.toString("utf-8")) * 1000; }; class Incident { constructor(fileContent, fileName) { // Save content this.content = md.render(fileContent); // Save properties const matter = md.frontmatter; this.title = matter.title; this.description = matter.description; this.date = dates.parse(matter.date).toISOString(); this.modified = dates.parse(matter.modified).toISOString(); this.scheduled = matter.scheduled ? dates.parse(matter.scheduled).toISOString() : null; this.duration = matter.duration ? Math.ceil(matter.duration) : undefined; this.severity = matter.severity; this.affectedsystems = matter.affectedsystems; this.resolved = matter.resolved; this.id = matter.id; if (!this.id) { this.id = hash(fileName); } } getData() { return { id: this.id, title: this.title, description: this.description, date: this.date, modified: this.modified, scheduled: this.scheduled, duration: this.duration, severity: this.severity, affectedsystems: this.affectedsystems, resolved: this.resolved, content: this.content }; } } const readFileIncidents = async dirPath => { const allIncidents = []; const exists = await fse.pathExists(dirPath); if (!exists) { logger.warn(`Content Directory not found: ${dirPath}`); } else { try { const files = (await readdirP(dirPath)).map(f => path.join(dirPath, f)); for (let i = 0; i < files.length; i++) { const filePath = files[i]; const isFile = (await statP(filePath)).isFile(); if ( isFile && path.extname(filePath) === ".md" && path.basename(filePath).toLowerCase() !== "readme.md" ) { const fileName = path.relative(dirPath, filePath); const fileContent = (await readFileP(filePath)).toString("utf8"); const incident = new Incident(fileContent, fileName).getData(); if (!incident.modified) { try { incident.modified = new Date( getGitLastUpdatedTimeStamp(filePath) ).toISOString(); } catch (error) { incident.modified = null; } } allIncidents.push(incident); } } } catch (error) { logger.error(error); } } return allIncidents; }; const getIncidents = async siteConfig => { const incidents = {}; const scheduled = {}; // Retrieve incidents for each language for (let i = 0; i < siteConfig.locales.length; i++) { const locale = siteConfig.locales[i]; const cacheKey = `incidents-${locale.code}`; let localeIncidents = cache.get(cacheKey); if (!localeIncidents) { let contentPath = path.join(siteConfig.sourceDir, siteConfig.content.dir); if (locale.code !== siteConfig.defaultLocale) { contentPath = path.join(contentPath, locale.code); } localeIncidents = await readFileIncidents(contentPath); } incidents[locale.code] = localeIncidents.filter(i => { if (i.severity === "under-maintenance" && i.scheduled && i.duration) { return new Date(i.scheduled) < new Date(); } else { return true; } }); scheduled[locale.code] = localeIncidents.filter(i => { if (i.severity === "under-maintenance" && i.scheduled && i.duration) { return new Date(i.scheduled) >= new Date(); } else { return false; } }); cache.set(cacheKey, localeIncidents); } return { incidents, scheduled }; }; const getPaginatedItems = (items, page = 1, pageSize = 10) => { // Based on https://gist.github.com/jstott/7b50d4f4790c357227bafd13b4ef32a4 pageSize = pageSize === -1 ? items.length : pageSize; const offset = (page - 1) * pageSize; const pagedItems = drop(items, offset).slice(0, pageSize); return { page, pageSize, total: items.length, totalPages: Math.ceil(items.length / pageSize), data: pagedItems }; }; module.exports = async function database(siteConfig, finalDate) { const { incidents, scheduled } = await getIncidents(siteConfig); const sortIncidents = incidents => sortBy(incidents, o => -Number(new Date(o.date))); const sortScheduled = scheduled => sortBy(scheduled, o => -Number(new Date(o.scheduled))); return { incidents(lang, page = 1, pageSize = 10) { const sortedIncidents = sortIncidents(incidents[lang]); const paginatedIncidents = getPaginatedItems( sortedIncidents, page, pageSize ); return { count: sortedIncidents.length, page: paginatedIncidents.page, page_size: paginatedIncidents.pageSize, total_pages: paginatedIncidents.totalPages || 1, incidents: paginatedIncidents.data }; }, incidentsHistory(lang, page = 1, pageSize = 3) { const sortedIncidents = sortIncidents(incidents[lang]); let periods = []; if (sortedIncidents.length > 0) { const start = dates.parse( sortedIncidents[sortedIncidents.length - 1].date ); const end = dates.parse(sortedIncidents[0].date); const range = dates.range(start, end, "month"); periods = range.dates.reverse().map((period, i) => { return { id: `${period.year()}-${period.month() + 1}`, incidents: [], order: i + 1, count: 0 }; }); // eslint-disable-next-line no-unused-vars for (const incident of sortedIncidents) { const incidentDate = dates.parse(incident.date); const month = incidentDate.month() + 1; const year = incidentDate.year(); const monthObject = periods.find(d => d.id === `${year}-${month}`); monthObject.count += 1; monthObject.incidents.push(incident); } // Exclude periods with no incidents periods = periods.filter(e => e.count > 0); } const paginatedPeriods = getPaginatedItems(periods, page, pageSize); return { count: periods.length, page: paginatedPeriods.page, page_size: paginatedPeriods.pageSize, total_pages: paginatedPeriods.totalPages || 1, periods: paginatedPeriods.data }; }, incidentsTimeline(lang, daysNumber = 7) { const sortedIncidents = sortIncidents(incidents[lang]); // Beginning of today const end = dates.parse(finalDate); // {daysNumber} days ago from today const start = end.subtract(daysNumber, "days"); const range = dates.range(start, end); const days = []; // Get incidents for each day in the range let order = 1; // eslint-disable-next-line no-unused-vars for (const day of range.dates.reverse()) { const incidents = []; // eslint-disable-next-line no-unused-vars for (const incident of sortedIncidents) { const incidentDate = dates.parse(incident.date); const difference = incidentDate.diff(day, "hours"); // If difference is less that 24 hours, include incident // in the selected day if (difference > 0 && difference <= 24) { incidents.push(incident); } else if (Math.abs(difference) > (daysNumber + 1) * 24) { // Incidents are already ordered chronologically, // so if difference is more than daysNumber + 1, // it's not worthy to keep looking for other incidents break; } } days.push({ date: day, count: incidents.length, incidents, order }); order++; } /* eslint-disable prettier/prettier */ const daysSinceLatest = sortedIncidents && sortedIncidents.length < 1 ? 0 : sortedIncidents[0] && sortedIncidents[0].date ? Math.abs(end.diff(dates.parse(sortedIncidents[0].date), "days")) : 0; /* eslint-enable */ return { count: days.length, days, daysSinceLatest }; }, incident(id, lang) { return incidents[lang].filter(i => i.id === id)[0]; }, systems(lang) { const systems = []; // Important order of relevance const systemSeverities = [ "under-maintenance", "major-outage", "partial-outage", "degraded-performance" ]; let order = 1; siteConfig.content.systems.forEach(system => { const currentSystem = { name: system, status: "operational", order }; // eslint-disable-next-line no-unused-vars for (const severity of systemSeverities) { const unresolved = incidents[lang].filter( incident => incident.affectedsystems.includes(system) && incident.severity === severity && incident.resolved === false ).length; if (unresolved > 0) { currentSystem.status = severity; break; } } systems.push(currentSystem); order++; }); return systems; }, scheduled(lang) { const sortedScheduled = sortScheduled(scheduled[lang]); const paginatedScheduled = getPaginatedItems(sortedScheduled, 1, -1); return { count: sortedScheduled.length, incidents: paginatedScheduled.data }; } }; };