-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
14,299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); |
25 changes: 25 additions & 0 deletions
25
service-health-backend/migrations/20231006131820_incident_record.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// @ts-check | ||
/** | ||
* @param {import('knex').Knex} knex | ||
*/ | ||
exports.up = async function up(knex) { | ||
await knex.schema.createTable('incident_record', table => { | ||
table.comment( | ||
'Table for tracking the service incidents we have alerted on', | ||
); | ||
|
||
table | ||
.string('incident_id') | ||
.notNullable() | ||
.primary() | ||
.comment('The upstream identifier of the incident'); | ||
table.boolean('sent').notNullable().comment('Have we sent the alert?'); | ||
}); | ||
}; | ||
|
||
/** | ||
* @param {import('knex').Knex} knex | ||
*/ | ||
exports.down = async function down(knex) { | ||
await knex.schema.dropTable('incident_record'); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"name": "@sammbetts/plugin-service-health-backend", | ||
"version": "0.1.0", | ||
"main": "src/index.ts", | ||
"types": "src/index.ts", | ||
"license": "Apache-2.0", | ||
"private": true, | ||
"publishConfig": { | ||
"access": "public", | ||
"main": "dist/index.cjs.js", | ||
"types": "dist/index.d.ts" | ||
}, | ||
"backstage": { | ||
"role": "backend-plugin" | ||
}, | ||
"scripts": { | ||
"start": "backstage-cli package start", | ||
"build": "backstage-cli package build", | ||
"lint": "backstage-cli package lint", | ||
"test": "backstage-cli package test", | ||
"clean": "backstage-cli package clean", | ||
"prepack": "backstage-cli package prepack", | ||
"postpack": "backstage-cli package postpack" | ||
}, | ||
"dependencies": { | ||
"@backstage/backend-common": "^0.19.5", | ||
"@backstage/config": "^1.1.0", | ||
"@types/express": "*", | ||
"axios": "^1.5.1", | ||
"express": "^4.17.1", | ||
"express-promise-router": "^4.1.0", | ||
"knex": "^2.5.1", | ||
"luxon": "^3.4.3", | ||
"node-fetch": "^2.6.7", | ||
"winston": "^3.2.1", | ||
"yn": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"@backstage/cli": "^0.22.13", | ||
"@types/supertest": "^2.0.12", | ||
"msw": "^1.0.0", | ||
"supertest": "^6.2.4" | ||
}, | ||
"files": [ | ||
"dist", | ||
"migrations/**/*.{js,d.ts}" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { DateTime } from 'luxon'; | ||
|
||
export function convertToUKDateTimeFormat(isoDateString: string): string { | ||
const dateTime = DateTime.fromISO(isoDateString, { setZone: true }); | ||
|
||
const day = dateTime.day; | ||
const month = dateTime.month; | ||
const year = dateTime.year; | ||
const hours = dateTime.hour; | ||
const minutes = dateTime.minute; | ||
|
||
return `${day.toString().padStart(2, '0')}/${month | ||
.toString() | ||
.padStart(2, '0')}/${year}, ${hours.toString().padStart(2, '0')}:${minutes | ||
.toString() | ||
.padStart(2, '0')}`; | ||
} |
23 changes: 23 additions & 0 deletions
23
service-health-backend/src/functions/sendSlackNotification.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
export const sendSlackNotification = async ( | ||
serviceName: string, | ||
incidentServiceName: string, | ||
incidentTime: string, | ||
slackWebhookUrl: string, | ||
) => { | ||
const payload = { | ||
text: `:warning: *Service Health - Third party incident alert* :warning: | ||
\nAn active incident has been reported by *${serviceName}* at *${incidentTime}* affecting *${incidentServiceName}*. | ||
\n | ||
\n_Powered by Backstage Service Health Plugin_`, | ||
}; | ||
|
||
fetch(slackWebhookUrl, { | ||
method: 'POST', | ||
headers: {}, | ||
body: JSON.stringify(payload), | ||
}).then(response => { | ||
if (!response.ok) { | ||
throw new Error(`Failed to send Slack notification: ${response.status}`); | ||
} | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './service/router'; | ||
export * from './service/IncidentNotifier'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { getRootLogger } from '@backstage/backend-common'; | ||
import yn from 'yn'; | ||
import { startStandaloneServer } from './service/standaloneServer'; | ||
|
||
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; | ||
const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); | ||
const logger = getRootLogger(); | ||
|
||
startStandaloneServer({ port, enableCors, logger }).catch(err => { | ||
logger.error(err); | ||
process.exit(1); | ||
}); | ||
|
||
process.on('SIGINT', () => { | ||
logger.info('CTRL+C pressed; exiting.'); | ||
process.exit(0); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { | ||
PluginDatabaseManager, | ||
resolvePackagePath, | ||
} from '@backstage/backend-common'; | ||
import { Knex } from 'knex'; | ||
|
||
const migrationsDir = resolvePackagePath( | ||
'@internal/plugin-service-health-backend', | ||
'migrations', | ||
); | ||
|
||
const TABLE_NAME = 'incident_record'; | ||
|
||
export class DatabaseHandler { | ||
static async create( | ||
database: PluginDatabaseManager, | ||
): Promise<DatabaseHandler> { | ||
const client: any = await database.getClient(); | ||
|
||
if (!database.migrations?.skip) { | ||
await client.migrate.latest({ | ||
directory: migrationsDir, | ||
}); | ||
} | ||
|
||
return new DatabaseHandler(client); | ||
} | ||
|
||
private readonly client: Knex; | ||
|
||
private constructor(client: Knex) { | ||
this.client = client; | ||
} | ||
|
||
getIncident = async (incidentId: string) => { | ||
const [incident] = await this.client | ||
.select() | ||
.from(TABLE_NAME) | ||
.where('incident_id', incidentId) | ||
.limit(1); | ||
|
||
return incident; | ||
}; | ||
|
||
createIncidentRecord = async (incidentId: string) => { | ||
await this.client | ||
.insert({ | ||
incident_id: incidentId, | ||
sent: false, | ||
}) | ||
.into(TABLE_NAME); | ||
}; | ||
|
||
updateIncidentRecord = async (id: string, sent: boolean) => { | ||
return await this.client(TABLE_NAME).where({ incident_id: id }).update({ | ||
sent, | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { Logger } from 'winston'; | ||
import { Config } from '@backstage/config'; | ||
import { PluginDatabaseManager } from '@backstage/backend-common'; | ||
import { sendSlackNotification } from '../functions/sendSlackNotification'; | ||
import { refreshAllServices } from './serviceHealthLogic'; | ||
import { DatabaseHandler } from './DatabaseLayer'; | ||
import { convertToUKDateTimeFormat } from '../functions/dateTimeFunction'; | ||
|
||
export class IncidentNotifier { | ||
private readonly logger: Logger; | ||
private db?: DatabaseHandler; | ||
private slackWebhookUrl: string; | ||
|
||
constructor(logger: Logger, config: Config) { | ||
this.logger = logger; | ||
this.slackWebhookUrl = config.getString( | ||
'serviceHealth.slackWebhookUrl', | ||
); | ||
} | ||
|
||
async connect(database: PluginDatabaseManager) { | ||
this.db = await DatabaseHandler.create(database); | ||
} | ||
async run(): Promise<{ services: any[] }> { | ||
if (!this.db) { | ||
this.logger.error( | ||
"Can't handle incidents, not connected to the database", | ||
); | ||
return { services: [] }; | ||
} | ||
try { | ||
const healthData = await refreshAllServices(this.db); | ||
const newIncidents: any[] = []; | ||
|
||
for (const service of healthData) { | ||
for (const incident of service.incidents) { | ||
if (await this.isIncidentNew(incident.id)) { | ||
incident.serviceName = service.serviceName; | ||
incident.componentName = service.incidentComponents; | ||
incident.updated = convertToUKDateTimeFormat( | ||
incident.modified || incident.updated_at || incident.date_updated, | ||
); | ||
newIncidents.push(incident); | ||
} | ||
} | ||
} | ||
if (newIncidents.length > 0) { | ||
await this.logIncidents(newIncidents); | ||
} | ||
return { | ||
services: newIncidents, | ||
}; | ||
} catch (error) { | ||
this.logger.error(`Error while fetching health data: ${(error as Error).message}`); | ||
return { services: [] }; | ||
} | ||
} | ||
|
||
private async isIncidentNew(incidentId: string): Promise<boolean> { | ||
const existingIncident = await this.db?.getIncident(incidentId); | ||
return !existingIncident; | ||
} | ||
|
||
async logIncidents(incidents: any[]): Promise<void> { | ||
for (const incident of incidents) { | ||
try { | ||
const incidentNameOptions = [ | ||
incident.service_name, | ||
incident.componentName, | ||
incident.services, | ||
]; | ||
let incidentName = ''; | ||
for (const option of incidentNameOptions) { | ||
if (option) { | ||
incidentName = option; | ||
break; | ||
} | ||
} | ||
await sendSlackNotification( | ||
incident.serviceName, | ||
incidentName, | ||
incident.updated, | ||
this.slackWebhookUrl, | ||
); | ||
await this.db?.createIncidentRecord(incident.id); | ||
} catch (error) { | ||
this.logger.error( | ||
`Failed to send Slack notification for incident ID: ${incident.id}`, | ||
); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import express from 'express'; | ||
import request from 'supertest'; | ||
import { getVoidLogger } from '@backstage/backend-common'; | ||
|
||
import { createRouter } from './router'; | ||
|
||
describe('createRouter', () => { | ||
let app: express.Express; | ||
|
||
beforeAll(async () => { | ||
const mockDatabaseService = { | ||
getClient: jest.fn(), | ||
migrations: { | ||
skip: true, | ||
}, | ||
}; | ||
const router = await createRouter({ | ||
logger: getVoidLogger(), | ||
database: mockDatabaseService as any, | ||
}); | ||
app = express().use(router); | ||
}); | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
describe('GET /health', () => { | ||
it('returns ok', async () => { | ||
const response = await request(app).get('/health'); | ||
|
||
expect(response.status).toEqual(200); | ||
expect(response.body).toEqual({ status: 'ok' }); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import express, { Request, Response } from 'express'; | ||
import Router from 'express-promise-router'; | ||
import { Logger } from 'winston'; | ||
import { PluginDatabaseManager, errorHandler } from '@backstage/backend-common'; | ||
import { DatabaseHandler } from './DatabaseLayer'; | ||
import { refreshAllServices } from './serviceHealthLogic'; | ||
|
||
export interface RouterOptions { | ||
logger: Logger; | ||
database: PluginDatabaseManager; | ||
} | ||
|
||
export async function createRouter( | ||
options: RouterOptions, | ||
): Promise<express.Router> { | ||
const router = Router(); | ||
const dbHandler = await DatabaseHandler.create(options.database); | ||
router.use(express.json()); | ||
|
||
router.get('/latest', async (_: Request, response: Response) => { | ||
const services = await refreshAllServices(dbHandler); | ||
response.json({ timestamp: Date.now().toString(), services }); | ||
}); | ||
|
||
router.get('/health', async (_: Request, response: Response) => { | ||
response.json({ status: 'ok' }); | ||
}); | ||
|
||
router.post('/health', async (_: Request, response: Response) => { | ||
response.json({ status: 'ok' }); | ||
}); | ||
|
||
router.patch('/health/:id', async (request: Request, response: Response) => { | ||
const { id } = request.params; | ||
const body = request.body; | ||
const count = await dbHandler.updateIncidentRecord(id, body); | ||
|
||
if (count) { | ||
response.json({ status: 'ok' }); | ||
} else { | ||
response.status(404).json({ message: 'Record not found' }); | ||
} | ||
}); | ||
|
||
router.use(errorHandler()); | ||
return router; | ||
} |
Oops, something went wrong.