From 87a5acc7da82a02000d3072132fcaf92916eb17c Mon Sep 17 00:00:00 2001 From: James Chartrand Date: Tue, 7 May 2024 14:26:53 -0400 Subject: [PATCH] add healthcheck --- .dockerignore | 5 +- .gitignore | 5 +- Dockerfile | 4 ++ healthcheck.js | 114 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 9 ++++ package.json | 4 +- src/app.js | 7 +-- 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 healthcheck.js diff --git a/.dockerignore b/.dockerignore index 5c2ac98..39a8dac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,7 +13,10 @@ node_modules .prettierignore .prettierrc.js compose-test.yaml +compose.yaml +compose-health-test.yaml Dockerfile README server-dev-only.cert -server-dev-only.key \ No newline at end of file +server-dev-only.key + .env.healthcheck.testing \ No newline at end of file diff --git a/.gitignore b/.gitignore index dc40ea4..2a071d2 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,7 @@ dist # vscode .vscode -compose-test.yaml \ No newline at end of file +compose-test.yaml +compose.yaml +compose-health-test.yaml +.env.healthcheck.testing \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 53309ef..6bc24e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,10 @@ COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/server.js /app/server.js COPY --from=builder /app/src /app/src COPY --from=builder /app/package.json /app/package.json +COPY --from=builder /app/healthcheck.js /app/healthcheck.js + +# The healthcheck can be run from here, but also from the compose file +# HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD ["/nodejs/bin/node", "app/healthcheck.js"] CMD ["app/server.js"] diff --git a/healthcheck.js b/healthcheck.js new file mode 100644 index 0000000..c1c7d9c --- /dev/null +++ b/healthcheck.js @@ -0,0 +1,114 @@ +import nodemailer from 'nodemailer' +import axios from 'axios' + +const serviceURL = process.env.HEALTH_CHECK_SERVICE_URL +const serviceName = process.env.HEALTH_CHECK_SERVICE_NAME +const shouldPostToWebHook = process.env.HEALTH_CHECK_WEB_HOOK +const shouldSendEmail = + process.env.HEALTH_CHECK_SMTP_HOST && + process.env.HEALTH_CHECK_SMTP_USER && + process.env.HEALTH_CHECK_SMTP_PASS && + process.env.HEALTH_CHECK_EMAIL_FROM && + process.env.HEALTH_CHECK_EMAIL_RECIPIENT + +axios + .get(serviceURL) + .then(async function (response) { + try { + const body = response.data + if (body.healthy === true) { + process.exit(0) + } + await notify(`${serviceName} is unhealthy: ${body.error}`) + process.exit(1) + } catch (error) { + await notify( + `${serviceName} is potentially unhealthy - error: ${JSON.stringify(error)}` + ) + process.exit(1) + } + }) + .catch(async (error) => { + await notify( + `${serviceName} is unhealthy and will restart after 3 tries. Error: ${error.message}` + ) + process.exit(1) + }) + +async function notify(message) { + console.log(message) + if (shouldSendEmail) await sendMail(message) + if (shouldPostToWebHook) await postToWebHook(message) +} + +async function postToWebHook(text) { + await axios + .post(process.env.HEALTH_CHECK_WEB_HOOK, { text }) + .catch((error) => { + console.error(error) + }) +} + +async function sendMail(message) { + const messageParams = { + from: process.env.HEALTH_CHECK_EMAIL_FROM, + to: process.env.HEALTH_CHECK_EMAIL_RECIPIENT, + subject: process.env.HEALTH_CHECK_EMAIL_SUBJECT, + text: message + } + + const mailTransport = { + host: process.env.HEALTH_CHECK_SMTP_HOST, + auth: { + user: process.env.HEALTH_CHECK_SMTP_USER, + pass: process.env.HEALTH_CHECK_SMTP_PASS + }, + ...(process.env.HEALTH_CHECK_SMTP_PORT && { + port: process.env.HEALTH_CHECK_SMTP_PORT + }) + } + + const transporter = nodemailer.createTransport(mailTransport) + + try { + await transporter.sendMail(messageParams) + } catch (error) { + console.log('the email send error: ') + console.log(error) + } +} + +//import * as http from 'node:http'; +/* const options = { hostname: 'SIGNER', port: (process.env.PORT || 4006), path: '/healthz', method: 'GET' }; + +http + .request(options, (res) => { + let body = ''; + + res.on('data', (chunk) => { + body += chunk; + }); + + res.on('end', async () => { + try { + const response = JSON.parse(body); + if (response.healthy === true) { + console.log('healthy response received: ', body); + await sendMail("It worked!") + process.exit(0); + } + + console.log('Unhealthy response received: ', body); + await sendMail(`It worked, but with error: ${JSON.stringify(body)}`) + process.exit(1); + } catch (err) { + console.log('Error parsing JSON response body: ', err); + process.exit(1); + } + }); + }) + .on('error', (err) => { + console.log('Error: ', err); + process.exit(1); + }) + .end(); */ diff --git a/package-lock.json b/package-lock.json index e9a03f0..577f0b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "dotenv": "^16.0.3", "express": "~4.16.1", "morgan": "~1.9.1", + "nodemailer": "^6.9.13", "winston": "^3.10.0" }, "devDependencies": { @@ -13350,6 +13351,14 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", diff --git a/package.json b/package.json index 3e34a5f..4b3c4e3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint-fix": "eslint --fix . --ext .js", "test": "NODE_OPTIONS=--experimental-vm-modules npx c8 mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/*.test.js", "coveralls": "npm run test; c8 report --reporter=text-lcov | coveralls", - "prepare": "test -d node_modules/husky && husky install || echo \"husky is not installed\"" + "prepare": "test -d node_modules/husky && husky install || echo \"husky is not installed\"", + "test:health": "node -r dotenv/config healthcheck.js dotenv_config_path=./.env.healthcheck.testing " }, "dependencies": { "@digitalcredentials/bnid": "^2.1.2", @@ -28,6 +29,7 @@ "dotenv": "^16.0.3", "express": "~4.16.1", "morgan": "~1.9.1", + "nodemailer": "^6.9.13", "winston": "^3.10.0" }, "devDependencies": { diff --git a/src/app.js b/src/app.js index d260214..16c1075 100644 --- a/src/app.js +++ b/src/app.js @@ -29,12 +29,13 @@ export async function build() { if (!data.proof) throw new SigningException(503, 'signing-service healthz failed') } catch (e) { - console.log(`exception in healthz: ${e}`) + console.log(`exception in healthz: ${JSON.stringify(e)}`) return res.status(503).json({ - error: `signing-service healthz check failed with error: ${e}` + error: `signing-service healthz check failed with error: ${e}`, + healthy: false }) } - res.send({ message: 'signing-service server status: ok.' }) + res.send({ message: 'signing-service server status: ok.', healthy: true }) }) app.get('/', function (req, res) {