Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring summary list for each service #8

Merged
merged 31 commits into from
Nov 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eccfcc2
Refactoring summary list for each service
Jozzey Oct 19, 2022
e194946
created shared service section
Jozzey Oct 19, 2022
4fdec5e
created sections for each app
Jozzey Oct 19, 2022
0a26c78
Display `clamdscan` version
StuAA78 Oct 24, 2022
ad77d4a
Add reason for using `promisify`
StuAA78 Oct 24, 2022
c27d253
Display redis version
StuAA78 Oct 24, 2022
2ca95ca
Display address facade status
StuAA78 Oct 24, 2022
faa9aa3
added data from the service repo
Jozzey Oct 24, 2022
c855ef7
renamed health/status endpoint to health/info
Jozzey Oct 25, 2022
db83c28
added applcation info
Jozzey Oct 25, 2022
fdddaf2
refactored _getAddressFacadeData to use got
Jozzey Oct 25, 2022
0f5984c
added charging module info
Jozzey Oct 25, 2022
e2e1827
WIP
Beckyrose200 Oct 26, 2022
4e4f541
refactored app data
Jozzey Oct 26, 2022
667ddfd
temporarily removed failing test
Jozzey Oct 26, 2022
792516a
Add nock and use in new test for service status
Cruikshanks Oct 31, 2022
7b4bc0d
Add proxyrequire and stub exec() calls
Cruikshanks Nov 1, 2022
a357724
Make mocks more obvious
Cruikshanks Nov 1, 2022
716d592
Add services env vars to GitHub CI
Cruikshanks Nov 1, 2022
b588c82
Get rid of console.log() in test
Cruikshanks Nov 1, 2022
a0558c8
Attempt to reduce noise in test setup
Cruikshanks Nov 1, 2022
24b29a2
Resolve one of the sonarcloud issues
Cruikshanks Nov 1, 2022
fb7a7ad
Add tests to cover shell check errors
Cruikshanks Nov 1, 2022
3be02f5
Temporary removal of the TODO
Cruikshanks Nov 1, 2022
2e98679
WIP - Handling errors when checking health/info
Cruikshanks Nov 1, 2022
ddb276b
Add tests for when services are down
Cruikshanks Nov 1, 2022
834eb2c
Fix sonarcloud issues
Cruikshanks Nov 1, 2022
c9acf61
Thoughts on current state
Cruikshanks Nov 1, 2022
83423f9
Fix typo in comment
Cruikshanks Nov 2, 2022
2312a72
Use sinon stub.withArgs() instead of onFirstCall
Cruikshanks Nov 2, 2022
dbe05e2
Rebuild package-lock.json
Cruikshanks Nov 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,17 @@ PORT=3000

# Test config
LOG_IN_TEST=false

# External services
EA_ADDRESS_FACADE_URL=http://localhost:8009
CHARGING_MODULE_URL=http://localhost:8020
SERVICE_FOREGROUND_URL=http://localhost:8001
SERVICE_BACKGROUND_URL=http://localhost:8012
REPORTING_URL=http://localhost:8011
IMPORT_URL=http://localhost:8007
TACTICAL_CRM_URL=http://localhost:8002
EXTERNAL_UI_URL=http://localhost:8000
INTERNAL_UI_URL=http://localhost:8008
TACTICAL_IDM_URL=http://localhost:8003
PERMIT_REPOSITORY_URL=http://localhost:8004
RETURNS_URL=http://localhost:8006
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ jobs:
AIRBRAKE_HOST: https://my-errbit-instance.com
AIRBRAKE_KEY: longvaluefullofnumbersandlettersinlowercase
PORT: 3000
# External services - they don't exist here but we have code that depends on these values being present
EA_ADDRESS_FACADE_URL: http://localhost:8009
CHARGING_MODULE_URL: http://localhost:8020
SERVICE_FOREGROUND_URL: http://localhost:8001
SERVICE_BACKGROUND_URL: http://localhost:8012
REPORTING_URL: http://localhost:8011
IMPORT_URL: http://localhost:8007
TACTICAL_CRM_URL: http://localhost:8002
EXTERNAL_UI_URL: http://localhost:8000
INTERNAL_UI_URL: http://localhost:8008
TACTICAL_IDM_URL: http://localhost:8003
PERMIT_REPOSITORY_URL: http://localhost:8004
RETURNS_URL: http://localhost:8006
# These need to be duplicated in services section for postgres. Unfortunately, there is not a way to reuse them
POSTGRES_USER: water_user
POSTGRES_PASSWORD: password
Expand All @@ -20,6 +33,7 @@ jobs:
POSTGRES_DB_TEST: wabs_test
ENVIRONMENT: dev


# Service containers to run with `runner-job`
services:
# Label used to access the service container
Expand Down
166 changes: 108 additions & 58 deletions app/services/service_status.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
* @module ServiceStatusService
*/

// We use promisify to wrap exec in a promise. This allows us to await it without resorting to using callbacks.
const util = require('util')
const ChildProcess = require('child_process')
const exec = util.promisify(ChildProcess.exec)

const servicesConfig = require('../../config/services.config')

/**
* Returns data required to populate our `/service-status` page, eg. task activity status, virus checker status, service
* version numbers, etc.
Expand All @@ -13,16 +20,19 @@
*/
class ServiceStatusService {
static async go () {
const importData = await this._getImportData()
const virusScannerData = await this._getVirusScannerData()
const cacheConnectivityData = await this._getCacheConnectivityData()
const serviceVersionsData = await this._getServiceVersionsData()
const redisConnectivityData = await this._getRedisConnectivityData()

const addressFacadeData = await this._getAddressFacadeData()
const chargingModuleData = await this._getChargingModuleData()
const appData = await this._getAppData()

return {
importRows: this._mapArrayToTextCells(importData),
virusScannerRows: this._mapArrayToStatusCells(virusScannerData),
cacheConnectivityRows: this._mapArrayToStatusCells(cacheConnectivityData),
serviceVersionsRows: this._mapArrayToStatusCells(serviceVersionsData)
virusScannerData,
redisConnectivityData,
addressFacadeData,
chargingModuleData,
appData
}
}

Expand All @@ -37,70 +47,110 @@ class ServiceStatusService {
})
}

/**
* Receives an array of statuses and returns it in the format required by the nunjucks template in the view.
*/
static _mapArrayToStatusCells (rows) {
// Map each row in the array we've received
return rows.map(row => {
// A status row has only two elements:
// * The thing having its status reported, which is a standard text cell;
// * Its status, which is formatted numeric so that it's right justified on its row.
return [
{ text: row[0] },
{ text: row[1], format: 'numeric' }
]
})
static async _getVirusScannerData () {
try {
const { stdout, stderr } = await exec('clamdscan --version')
return stderr ? `ERROR: ${stderr}` : stdout
} catch (error) {
return `ERROR: ${error.message}`
}
}

static async _getImportData () {
return [
[
'Cell 1.1',
'Cell 1.2',
'Cell 1.3',
'Cell 1.4',
'Cell 1.5'
],
[
'Cell 2.1',
'Cell 2.2',
'Cell 2.3',
'Cell 2.4',
'Cell 2.5'
]
]
static async _getRedisConnectivityData () {
try {
const { stdout, stderr } = await exec('redis-server --version')
return stderr ? `ERROR: ${stderr}` : stdout
} catch (error) {
return `ERROR: ${error.message}`
}
}

static async _getVirusScannerData () {
return [
[
'Status',
'OK'
]
]
static async _getAddressFacadeData () {
const statusUrl = new URL('/address-service/hola', servicesConfig.addressFacade.url)
const result = await this._requestData(statusUrl)

return result.succeeded ? result.response.body : result.response
}

static async _getCacheConnectivityData () {
return [
[
'Status',
'Connected'
]
]
static async _getChargingModuleData () {
const statusUrl = new URL('/status', servicesConfig.chargingModule.url)
const result = await this._requestData(statusUrl)

return result.succeeded ? result.response.headers['x-cma-docker-tag'] : result.response
}

static async _getServiceVersionsData () {
return [
static async _requestData (url) {
// As of v12, the got dependency no longer supports CJS modules. This causes us a problem as we are locked into
// using these for the time being. Some workarounds are provided here: https://github.com/sindresorhus/got/issues/1789
// We have gone the route of using await import('got'). We cannot do this at the top level as Node doesn't support
// top level in CJS so we do it here instead.
const { got } = await import('got')
StuAA78 marked this conversation as resolved.
Show resolved Hide resolved
const result = {
succeeded: true,
response: null
}

try {
result.response = await got.get(url, {
retry: {
// We ensure that the only network errors Got retries are timeout errors
errorCodes: ['ETIMEDOUT'],
// We set statusCodes as an empty array to ensure that 4xx, 5xx etc. errors are not retried
statusCodes: []
}
})
} catch (error) {
const statusCode = error.response ? error.response.statusCode : 'N/A'
result.response = `ERROR: ${statusCode} - ${error.name} - ${error.message}`
StuAA78 marked this conversation as resolved.
Show resolved Hide resolved
result.succeeded = false
}

return result
}

static _getImportJobsData () {
return this._mapArrayToTextCells([
[
'Water service',
'3.0.1'
'Cell 1.1',
'Cell 1.2'
],
[
'IDM',
'2.25.1'
'Cell 2.1',
'Cell 2.2'
]
])
}

static async _getAppData () {
const healthInfoPath = '/health/info'
const services = [
{ name: 'Service - foreground', url: new URL(healthInfoPath, servicesConfig.serviceForeground.url) },
{ name: 'Service - background', url: new URL(healthInfoPath, servicesConfig.serviceBackground.url) },
{ name: 'Reporting', url: new URL(healthInfoPath, servicesConfig.reporting.url) },
{ name: 'Import', url: new URL(healthInfoPath, servicesConfig.import.url) },
{ name: 'Tactical CRM', url: new URL(healthInfoPath, servicesConfig.tacticalCrm.url) },
{ name: 'External UI', url: new URL(healthInfoPath, servicesConfig.externalUi.url) },
{ name: 'Internal UI', url: new URL(healthInfoPath, servicesConfig.internalUi.url) },
{ name: 'Tactical IDM', url: new URL(healthInfoPath, servicesConfig.tacticalIdm.url) },
{ name: 'Permit repository', url: new URL(healthInfoPath, servicesConfig.permitRepository.url) },
{ name: 'Returns', url: new URL(healthInfoPath, servicesConfig.returns.url) }
]

for (const service of services) {
const result = await this._requestData(service.url)

if (result.succeeded) {
const data = JSON.parse(result.response.body)
service.version = data.version
service.commit = data.commit
service.jobs = service.name === 'Import' ? this._getImportJobsData() : []
} else {
service.version = result.response
service.commit = ''
}
}

return services
}
}

Expand Down
95 changes: 56 additions & 39 deletions app/views/service_status.njk
Original file line number Diff line number Diff line change
@@ -1,54 +1,71 @@
{% extends 'layout.njk' %}

{% from "govuk/components/table/macro.njk" import govukTable %}
{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}

{% block content %}
<div class="govuk-body">
<h1 class="govuk-heading-l">Service Status</h1>
<h1 class="govuk-heading-l">Service status</h1>

{{ govukTable({
caption: "Imports",
captionClasses: "govuk-table__caption--m",
firstCellIsHeader: true,
head: [
{
text: "Import task name"
},
{
text: "Success rate (last 3 days)"
},
{
text: "Active"
},
<h2 class="govuk-heading-m">Virus scanner</h2>
<p class="govuk-body">{{ virusScannerData }}</p>

<hr class="govuk-section-break govuk-section-break--m govuk-section-break--visible">

<h2 class="govuk-heading-m">Redis</h2>
<p class="govuk-body">{{ redisConnectivityData }}</p>

<hr class="govuk-section-break govuk-section-break--m govuk-section-break--visible">

<h2 class="govuk-heading-m">Address facade</h2>
<p class="govuk-body">{{ addressFacadeData }}</p>

<hr class="govuk-section-break govuk-section-break--m govuk-section-break--visible">

<h2 class="govuk-heading-m">Charging module</h2>
<p class="govuk-body">{{ chargingModuleData }}</p>

{% for app in appData %}
<hr class="govuk-section-break govuk-section-break--m govuk-section-break--visible">

<h2 class="govuk-heading-m">{{ app.name }}</h2>
{{ govukSummaryList({
classes: 'govuk-summary-list--no-border',
rows: [
{
text: "Last updated"
key: {
text: "Version"
},
value: {
text: app.version
}
},
{
text: "Last checked"
key: {
text: "Commit hash"
},
value: {
text: app.commit
}
}
],
rows: importRows
]
}) }}

{{ govukTable({
caption: "Virus scanner",
captionClasses: "govuk-table__caption--m",
firstCellIsHeader: true,
rows: virusScannerRows
}) }}

{{ govukTable({
caption: "Cache connectivity",
captionClasses: "govuk-table__caption--m",
firstCellIsHeader: true,
rows: cacheConnectivityRows
}) }}
{% if app.jobs.length %}
{{ govukTable({
firstCellIsHeader: false,
head: [
{
text: "Job name"
},
{
text: "Last updated"
}
],
rows: app.jobs
}) }}
{% endif %}

{{ govukTable({
caption: "Service versions",
captionClasses: "govuk-table__caption--m",
firstCellIsHeader: true,
rows: serviceVersionsRows
}) }}
{% endfor %}
</div>
{% endblock %}
{% endblock %}
Loading