diff --git a/README.md b/README.md index 320358a..31da1f8 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,9 @@ npm run test ``` npm run lint ``` + +### HealthCheck + +``` +/.well-known/apollo/server-health +``` \ No newline at end of file diff --git a/src/block-watcher.ts b/src/block-watcher.ts index 3ef62e6..bdd9392 100644 --- a/src/block-watcher.ts +++ b/src/block-watcher.ts @@ -6,9 +6,12 @@ class BlockWatcher { private _lastBlockHeight!: number; private _timer!: NodeJS.Timeout; + private _healthy: boolean; + constructor() { this._callbacks = []; this._lastBlockHeight = 0; + this._healthy = true; } public onNewBlock(callback: (height?: number) => void): BlockWatcher { @@ -45,6 +48,7 @@ class BlockWatcher { repository .getMaxHeight() .then((height) => { + this._healthy = true; if (!height || this._lastBlockHeight >= height) { return; } @@ -57,10 +61,15 @@ class BlockWatcher { this._notify(height); }) .catch((e) => { + this._healthy = true; this._notify(); console.error(e); }); } + + public isHealthy(): boolean { + return this._healthy; + } } export const blockWatcher = new BlockWatcher(); diff --git a/src/context/database-context.ts b/src/context/database-context.ts index 8bda6da..9b8d78a 100644 --- a/src/context/database-context.ts +++ b/src/context/database-context.ts @@ -14,6 +14,7 @@ import { TokenRepository } from "./token-repository"; import { BlocksRepository } from "./blocks-repository"; export class DatabaseContext { + private dataSource: DataSource; public readonly transactions!: TransactionRepository; public readonly blockInfo!: BlocksRepository; public readonly boxes!: BoxRepository; @@ -28,6 +29,7 @@ export class DatabaseContext { public readonly unconfirmedInputs!: UnconfirmedInputRepository; constructor(dataSource: DataSource) { + this.dataSource = dataSource; const context: RepositoryDataContext = { dataSource, graphQLDataLoader: new GraphQLDatabaseLoader(dataSource, { disableCache: true }) @@ -48,4 +50,8 @@ export class DatabaseContext { this.dataInputs = new BaseRepository(DataInputEntity, "dti", { context, defaults }); this.inputs = new BaseRepository(InputEntity, "input", { context, defaults }); } + + checkConnection = () => { + return this.dataSource.isInitialized; + }; } diff --git a/src/health-check.ts b/src/health-check.ts new file mode 100644 index 0000000..8cb5609 --- /dev/null +++ b/src/health-check.ts @@ -0,0 +1,31 @@ +import { blockWatcher } from "./block-watcher"; +import { redisClient } from "./caching"; +import { DatabaseContext } from "./context/database-context"; +import { nodeService } from "./services"; + +export const checkHealth = async (dataContext: DatabaseContext): Promise => { + const checks = { + db: () => () => dataContext.checkConnection, + redis: async () => (await redisClient.mget("test")).length === 1, + node: async () => (await nodeService.getNodeInfo()).status === 200, + blockWatcher: () => blockWatcher.isHealthy() + }; + + const results = await Promise.all( + Object.entries(checks).map(async ([key, func]) => ({ + [key]: await func() + })) + ); + + let isAnyFailed = false; + for (const result of results) { + for (const [key, value] of Object.entries(result)) { + if (!value) { + console.error(`🚫 ${key} is not healthy.`); + isAnyFailed = true; + } + } + } + + if (isAnyFailed) throw new Error("Service is unhealthy."); +}; diff --git a/src/index.ts b/src/index.ts index 0616c2d..b947679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { DatabaseContext } from "./context/database-context"; import { initializeDataSource } from "./data-source"; import { generateSchema } from "./graphql/schema"; import { nodeService } from "./services"; +import { checkHealth } from "./health-check"; const { TS_NODE_DEV, MAX_QUERY_DEPTH } = process.env; @@ -55,7 +56,8 @@ async function startServer(schema: GraphQLSchema, dataContext: DatabaseContext) } } ) - ] + ], + onHealthCheck: checkHealth.bind(null, dataContext) }); const { url } = await server.listen({ port: 3000 }); console.log(`🚀 Server ready at ${url}`);