Skip to content

Commit

Permalink
Merge pull request #7 from digitalcredentials/jc-add-healthz
Browse files Browse the repository at this point in the history
add healthz endpoint
  • Loading branch information
jchartrand authored Nov 25, 2024
2 parents 6f5d8a4 + 6228c39 commit 4688bf6
Show file tree
Hide file tree
Showing 19 changed files with 735 additions and 205 deletions.
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**/*.env
.git
.github
.husky
coverage
logs
node_modules
.dockerignore
.editorconfig
.eslintrc.cjs
.gitignore
.lintstagedrc.json
.prettierignore
.prettierrc.js
compose-test.yaml
Dockerfile
README
.env.healthcheck.testing
12 changes: 7 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ jobs:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- name: Run test with Node.js ${{ matrix.node-version }}
run: npm run test
env:
CI: true
run: npm run coveralls
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2
with:
github-token: ${{ github.token }}

1 change: 1 addition & 0 deletions .husky/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_
33 changes: 6 additions & 27 deletions .husky/_/husky.sh
Original file line number Diff line number Diff line change
@@ -1,30 +1,9 @@
#!/bin/sh
if [ -z "$husky_skip_init" ]; then
debug () {
[ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
}
echo "husky - DEPRECATED
readonly hook_name="$(basename "$0")"
debug "starting $hook_name..."
Please remove the following two lines from $0:
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
#!/usr/bin/env sh
. \"\$(dirname -- \"\$0\")/_/husky.sh\"
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi

export readonly husky_skip_init=1
sh -e "$0" "$@"
exitCode="$?"

if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
exit $exitCode
fi

exit 0
fi
They WILL FAIL in v10.0.0
"
5 changes: 5 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
npm test
6 changes: 6 additions & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"*.js": [
"prettier --write",
"eslint"
]
}
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
coverage
node_modules
7 changes: 7 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
trailingComma: 'none',
tabWidth: 2,
semi: false,
singleQuote: true,
bracketSpacing: true
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# transaction-manager-service Changelog

## 0.3.0 - 2024-11-25

### Changed

- added test coverage
- added health check option

## 0.2.0 - 2024-10-11

### Changed
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Transaction Manager Service _(@digitalcredentials/transaction-manager-service)_

[![Build status](https://img.shields.io/github/actions/workflow/status/digitalcredentials/transaction-service/main.yml?branch=main)](https://github.com/digitalcredentials/transaction-service/actions?query=workflow%3A%22Node.js+CI%22)
[![Coverage Status](https://coveralls.io/repos/github/digitalcredentials/transaction-service/badge.svg?branch=main)](https://coveralls.io/github/digitalcredentials/transaction-service?branch=main)

> Express app for managing the transactions used in [VC-API exchanges](https://w3c-ccg.github.io/vc-api/#initiate-exchange).
Expand All @@ -10,6 +11,8 @@

- [Overview](#overview)
- [API](#api)
- [Health Check](#health-check)
- [Environment Variables](#environment-variables)
- [Versioning](#versioning)
- [Contribute](#contribute)
- [License](#license)
Expand All @@ -29,7 +32,7 @@ Especially meant to be used as a service within a Docker compose network, initia

## API

Implements three endpoints:
Implements four endpoints:

* POST /exchange

Expand Down Expand Up @@ -102,6 +105,38 @@ NOTE: the object returned from the initial setup call to the exchanger returns t

At the moment, the [Leaner Credential Wallet](https://lcw.app) only supports the directDeepLink.

* GET /healthz

Which is an endpoint typically meant to be called by the Docker [HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck) option for a specific service. Read more below in the [Health Check](#health-check) section.

## Health Check

Docker has a [HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck) option for monitoring the
state (health) of a container. We've included an endpoint `GET healthz` that checks the health of the signing service (by running a test signature). The endpoint can be directly specified in a CURL or WGET call on the HEALTHCHECK, but we also provide a [healthcheck.js](./healthcheck.js) function that can be similarly invoked by the HEALTHCHECK and which itself hits the `healthz` endpoint, but additionally provides options for both email and Slack notifications when the service is unhealthy.

You can see how we've configured the HEALTHCHECK in our [example compose files](https://github.com/digitalcredentials/docs/blob/main/deployment-guide/DCCDeploymentGuide.md#docker-compose-examples). Our compose files also include an example of how to use [autoheal](https://github.com/willfarrell/docker-autoheal) together with HEALTHCHECK to restart an unhealthy container.

If you want notifications sent to a Slack channel, you'll have to set up a Slack [web hook](https://api.slack.com/messaging/webhooks).

If you want notifications sent to an email address, you'll need an SMTP server to which you can send emails, so something like sendgrid, mailchimp, mailgun, or even your own email account if it allows direct SMTP sends. Gmail can apparently be configured to so so.

## Environment Variables

There is a sample .env file provided called .env.example to help you get started with your own .env file. The supported fields:

| Key | Description | Default | Required |
| --- | --- | --- | --- |
| `PORT` | http port on which to run the express app | 4006 | no |
| `HEALTH_CHECK_SMTP_HOST` | SMTP host for unhealthy notification emails - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_SMTP_USER` | SMTP user for unhealthy notification emails - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_SMTP_PASS` | SMTP password for unhealthy notification emails - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_EMAIL_FROM` | name of email sender for unhealthy notifications emails - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_EMAIL_RECIPIENT` | recipient when unhealthy - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_EMAIL_SUBJECT` | email subject when unhealthy - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_WEB_HOOK` | posted to when unhealthy - see [Health Check](#health-check) | no | no |
| `HEALTH_CHECK_SERVICE_URL` | local url for this service - see [Health Check](#health-check) | http://SIGNER:4004/healthz | no |
| `HEALTH_CHECK_SERVICE_NAME` | service name to use in error messages - see [Health Check](#health-check) | SIGNING-SERVICE | no |

## Versioning

The transaction-service is primarily intended to run as a docker image within a docker compose network, typically as part of a flow that is orchestrated by the [DCC Issuer Coordinator](https://github.com/digitalcredentials/issuer-coordinator) and the [DCC Workflow Coordinator](https://github.com/digitalcredentials/workflow-coordinator).
Expand Down
11 changes: 11 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
import eslintConfigPrettier from 'eslint-config-prettier'
import mochaPlugin from 'eslint-plugin-mocha'

export default [
{ languageOptions: { globals: globals.node } },
mochaPlugin.configs.flat.recommended,
pluginJs.configs.recommended,
eslintConfigPrettier
]
114 changes: 114 additions & 0 deletions healthcheck.js
Original file line number Diff line number Diff line change
@@ -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(); */
31 changes: 26 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
{
"name": "@digitalcredentials/transaction-manager-service",
"name": "@digitalcredentials/transaction-service",
"description": "An express app for managing challenges in a DIDAuth exchange.",
"version": "0.2.0",
"version": "0.3.0",
"type": "module",
"scripts": {
"start": "node -r dotenv/config server.js",
"dev": "nodemon -r dotenv/config server.js",
"test": "NODE_OPTIONS=--experimental-vm-modules npx mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/app.test.js "
"dev-noenv": "nodemon server.js",
"test": "NODE_OPTIONS=--experimental-vm-modules npx c8 mocha --timeout 10000 -r dotenv/config dotenv_config_path=src/test-fixtures/.env.testing src/app.test.js ",
"coveralls": "npm run test; npx c8 report --reporter=text-lcov > ./coverage/lcov.info",
"prepare": "test -d node_modules/husky && husky install || echo \"husky is not installed\"",
"lint": "eslint",
"lint-fix": "eslint --fix"
},
"dependencies": {
"@digitalbazaar/ed25519-signature-2020": "^5.4.0",
"@digitalbazaar/ed25519-verification-key-2020": "^4.1.0",
"@digitalbazaar/vc": "^7.0.0",
"@digitalcredentials/security-document-loader": "^6.0.0",
"axios": "^1.7.7",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "^16.0.3",
"express": "~4.16.1",
"keyv": "^4.5.2",
"keyv-file": "^0.2.0",
"morgan": "~1.9.1"
"morgan": "~1.9.1",
"nodemailer": "^6.9.14"
},
"devDependencies": {
"@eslint/js": "^9.3.0",
"chai": "^4.3.7",
"coveralls": "^3.1.1",
"eslint": "^9.3.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-mocha": "^10.4.3",
"globals": "^15.3.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.5",
"mocha": "^10.2.0",
"nock": "^13.5.4",
"nodemon": "^2.0.21",
"prettier": "3.2.5",
"supertest": "^6.3.3"
},
"keywords": [
Expand All @@ -44,5 +61,9 @@
"url": "https://github.com/digitalcredentials/transaction-manager-service"
},
"homepage": "https://github.com/digitalcredentials/transaction-manager-service",
"bugs": "https://github.com/digitalcredentials/transaction-manager-service/issues"
"bugs": "https://github.com/digitalcredentials/transaction-manager-service/issues",
"lint-staged": {
"*.js": "eslint --cache --fix",
"*.{js,css,md}": "prettier --write"
}
}
6 changes: 6 additions & 0 deletions src/TransactionException.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function TransactionException(code, message, stack) {
this.code = code
this.message = message
this.stack = stack
}

Loading

0 comments on commit 4688bf6

Please sign in to comment.