diff --git a/docs/site/content/en/docs/how-to/config/index.md b/docs/site/content/en/docs/how-to/config/index.md index b5ed46ac..ec30dcc7 100644 --- a/docs/site/content/en/docs/how-to/config/index.md +++ b/docs/site/content/en/docs/how-to/config/index.md @@ -114,6 +114,14 @@ you'll get a webhook event with `hasMedia: True` field, but with no `media.url`. } ``` +### Health Check +Health check is available in [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) only. + +The following environment variables can be used to configure the [Health Check ->]({{< relref "/docs/how-to/other" >}}): +- `WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB` - the threshold in MB for the media files storage. The default value is `100`. +- `WHATSAPP_HEALTH_SESSIONS_FILES_THRESHOLD_MB` - the threshold in MB for the sessions files storage. The default value is `100`. +- `WHATSAPP_HEALTH_MONGODB_TIMEOUT` - the timeout in milliseconds for the MongoDB health check. The default value is `5000`. + ## Examples #### Debug Mode diff --git a/docs/site/content/en/docs/how-to/engines/index.md b/docs/site/content/en/docs/how-to/engines/index.md index e7d3f1a3..2ef955e0 100644 --- a/docs/site/content/en/docs/how-to/engines/index.md +++ b/docs/site/content/en/docs/how-to/engines/index.md @@ -193,7 +193,8 @@ please [create an issue](https://github.com/devlikeapro/whatsapp-http-api/issues | `GET /api/{session}/presence/{chatId}` | ➖ | ✔️ | ➖ | | `POST /api/{session}/presence/{chatId}/subscribe` | ➖ | ✔️ | ➖ | | **Other** | | | | -| `POST /api/version` | ➖ | ✔️ | ➖ | +| `GET /api/version` | ➖ | ✔️ | ➖ | +| `GET /health` ![](/images/versions/plus.png) | ✔️ | ✔️ | ✔️ | | **Webhooks** | WEBJS | NOWEB | VENOM | |-----------------------------------------------------|:-----:|:-----:|:-----:| diff --git a/docs/site/content/en/docs/how-to/other/index.md b/docs/site/content/en/docs/how-to/other/index.md new file mode 100644 index 00000000..5e7a64ec --- /dev/null +++ b/docs/site/content/en/docs/how-to/other/index.md @@ -0,0 +1,192 @@ +--- +title: "Other" +description: "Other features and API" +lead: "" +date: 2020-10-06T08:48:45+00:00 +lastmod: 2020-10-06T08:48:45+00:00 +draft: false +images: [ ] +weight: 802 +--- + +This page provides useful information about other features and API that are not covered in the other sections. + +## Health Check +Health check is available in [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) only. + +The health check endpoint is used to determine the health of the service. + +``` +GET /health +``` + +It returns a **200 OK** status code if the service is healthy. + +The response format: +```json +{ + "status": "ok", + "info": { + "metric1": { + "field": "value" + }, + "metric2": { + "field": "value" + } + }, + "error": {}, + "details": {} +} +``` + +Where: +- `status`: `'error' | 'ok' | 'shutting_down'` - If any health indicator failed the status will be `'error'`. If the app is shutting down but still accepting HTTP requests, the health check will have the `'shutting_down'` status. +- `info`: Object containing information of each health indicator which is of status `'up'`, or in other words "healthy". +- `error`: Object containing information of each health indicator which is of status `'down'`, or in other words "unhealthy". +- `details`: Object containing detailed information of each health indicator. + +### Health Check Indicators +Few things we check in the health check: +- Media files storage space - `mediaFiles.space` +- Sessions files storage space - `sessionsFiles.space` +- MongoDB connection - `mongodb` + +### Configuration +The following environment variables can be used to configure the health check: +- `WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB` - the threshold in MB for the media files storage. The default value is `100`. +- `WHATSAPP_HEALTH_SESSIONS_FILES_THRESHOLD_MB` - the threshold in MB for the sessions files storage. The default value is `100`. +- `WHATSAPP_HEALTH_MONGODB_TIMEOUT` - the timeout in milliseconds for the MongoDB health check. The default value is `5000`. + +### Examples +**Healthy response** when you use [Local Storage]({{< relref "/docs/how-to/storages#sessions" >}}) for session authentication: + +**200 OK** +```json +{ + "status": "ok", + "info": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132979355648, + "threshold": 104857600 + }, + "sessionsFiles.space": { + "status": "up", + "path": "/app/.sessions", + "diskPath": "/", + "free": 132979355648, + "threshold": 104857600 + } + }, + "error": {}, + "details": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132979355648, + "threshold": 104857600 + }, + "sessionsFiles.space": { + "status": "up", + "path": "/app/.sessions", + "diskPath": "/", + "free": 132979355648, + "threshold": 104857600 + } + } +} +``` + +**Healthy response** when you use [MongoDB Storage]({{< relref "/docs/how-to/storages#sessions" >}}) for session authentication: + +**200 OK** +```json +{ + "status": "ok", + "info": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132977496064, + "threshold": 104857600 + }, + "mongodb": { + "status": "up", + "message": "Up and running" + } + }, + "error": {}, + "details": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132977496064, + "threshold": 104857600 + }, + "mongodb": { + "status": "up", + "message": "Up and running" + } + } +} +``` + +**Unhealthy response example** + +**503 Service Unavailable** +```json +{ + "status": "error", + "info": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132976623616, + "threshold": 104857600 + } + }, + "error": { + "mongodb": { + "status": "down", + "error": "Timeout" + } + }, + "details": { + "mediaFiles.space": { + "status": "up", + "path": "/tmp/whatsapp-files", + "diskPath": "/", + "free": 132976623616, + "threshold": 104857600 + }, + "mongodb": { + "status": "down", + "error": "Timeout" + } + } +} +``` + + + +## Get version +Returns the version of the installed docker image. +``` +GET /api/version +``` + +```json +{ + "version": "2024.2.3", + "engine": "NOWEB", + "tier": "PLUS", + "browser": "/usr/bin/google-chrome-stable" +} +``` + diff --git a/docs/site/content/en/docs/how-to/storages/index.md b/docs/site/content/en/docs/how-to/storages/index.md index cae2eac5..d2f2d4a3 100644 --- a/docs/site/content/en/docs/how-to/storages/index.md +++ b/docs/site/content/en/docs/how-to/storages/index.md @@ -61,6 +61,8 @@ Under the hood, the WAHA stores the session data in the following directory stru when you "logout" the session using [POST /api/sessions/logout]({{< relref "/docs/how-to/sessions#logout">}}) or providing `logout: True` in [POST /api/sessions/stop]({{< relref "/docs/how-to/sessions#stop">}}) it removes the directory with the session data. +### Health Check +The [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) provides [the health check endpoint]({{< relref "/docs/how-to/other" >}}) that checks the local storage. ## Sessions - MongoDB If you want to use the MongoDB to store the session data, you need to: @@ -103,6 +105,9 @@ If you use the [MongoDB Atlas](https://www.mongodb.com/atlas/database) you must For production please consider running the MongoDB server close to the WAHA server for the best performance and security reasons. +### Health Check +The [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) provides [the health check endpoint]({{< relref "/docs/how-to/other" >}}) that checks the MongoDB connection. + ## Media When your WhatsApp instance receives media files, it stores them in the media storage. @@ -136,6 +141,9 @@ Here's all the steps in one command: docker run -v /path/to/on/host/.media:/app/.media -e WHATSAPP_FILES_FOLDER=/app/.media -e WHATSAPP_FILES_LIFETIME=0 -p 3000:3000/tcp devlikeapro/whatsapp-http-api-plus ``` +### Health Check +The [WAHA Plus ![](/images/versions/plus.png)]({{< relref "/docs/how-to/plus-version" >}}) provides [the health check endpoint]({{< relref "/docs/how-to/other" >}}) that checks the local storage. + ## Media - S3 If you're interested in using the S3 storage or any other cloud storage (like [self-hosted S3 - Minio](https://min.io/)), please create an issue or vote for the S3 issue in [the GitHub repository](https://github.com/devlikeapro/whatsapp-http-api/issues?q=is%3Aissue+is%3Aopen+S3). diff --git a/docs/site/content/en/docs/overview/changelog.md b/docs/site/content/en/docs/overview/changelog.md index f5467efc..c0d34d2a 100644 --- a/docs/site/content/en/docs/overview/changelog.md +++ b/docs/site/content/en/docs/overview/changelog.md @@ -23,6 +23,7 @@ You even can **subscribe to get new updates** there! - Add support for [MongoDB as storage for Session data]({{< relref "/docs/how-to/storages" >}}) - Support persistent file storage for media files - [now you can save media files between container restarts]({{< relref "/docs/how-to/storages#media" >}}) - If you set `WHATSAPP_FILES_LIFETIME=0` environment variable - media files will be never deleted. +- Add `GET /api/health` endpoint to [check the health of the service](https://waha.devlike.pro/docs/how-to/other/) ## 2024.1 - Implement [Patron Portal](https://portal.devlike.pro/) where you can get your personal API key and manage your perks. diff --git a/package.json b/package.json index 28f2cb78..48fda473 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "@adiwajshing/baileys": "github:WhiskeySockets/Baileys", "@adiwajshing/keyed-db": "^0.2.4", + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^9.0.9", "@nestjs/config": "^0.5.0", "@nestjs/core": "^9.0.9", @@ -31,8 +32,10 @@ "@nestjs/platform-express": "^9.0.9", "@nestjs/serve-static": "^2.1.3", "@nestjs/swagger": "^7.1.11", + "@nestjs/terminus": "^10.2.3", "@types/lodash": "^4.14.194", "@types/ws": "^8.5.4", + "check-disk-space": "^3.4.0", "class-validator": "^0.12.2", "del": "^6.0.0", "express-basic-auth": "^1.2.1", diff --git a/src/api/health.controller.ts b/src/api/health.controller.ts new file mode 100644 index 00000000..6dd2dea2 --- /dev/null +++ b/src/api/health.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { HealthCheck } from '@nestjs/terminus'; + +import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService'; + +@Controller('health') +@ApiTags('other') +export class HealthController { + constructor(private wahaHealth: WAHAHealthCheckService) {} + + @Get() + @HealthCheck() + async check() { + return this.wahaHealth.check(); + } +} diff --git a/src/config.service.ts b/src/config.service.ts index 4e414910..802947db 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -155,4 +155,25 @@ export class WhatsappConfigService { getSwaggerAdvancedConfigEnabled(): boolean { return this.configService.get('WHATSAPP_SWAGGER_CONFIG_ADVANCED', false); } + + getHealthMediaFilesThreshold(): number { + return this.configService.get( + 'WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB', + 100, + ); + } + + getHealthSessionFilesThreshold(): number { + return this.configService.get( + 'WHATSAPP_HEALTH_SESSION_FILES_THRESHOLD_MB', + 100, + ); + } + + getHealthMongoTimeout(): number { + return this.configService.get( + 'WHATSAPP_HEALTH_MONGO_TIMEOUT_MS', + 3000, + ); + } } diff --git a/src/core/abc/WAHAHealthCheckService.ts b/src/core/abc/WAHAHealthCheckService.ts new file mode 100644 index 00000000..fed59124 --- /dev/null +++ b/src/core/abc/WAHAHealthCheckService.ts @@ -0,0 +1,18 @@ +import { ConsoleLogger, Injectable } from '@nestjs/common'; +import { DiskHealthIndicator, HealthCheckService } from '@nestjs/terminus'; +import type { HealthCheckResult } from '@nestjs/terminus/dist/health-check/health-check-result.interface'; + +import { WhatsappConfigService } from '../../config.service'; +import { SessionManager } from './manager.abc'; + +@Injectable() +export abstract class WAHAHealthCheckService { + constructor( + protected sessionManager: SessionManager, + protected health: HealthCheckService, + protected log: ConsoleLogger, + protected config: WhatsappConfigService, + ) {} + + abstract check(): Promise; +} diff --git a/src/core/app.module.core.ts b/src/core/app.module.core.ts index e4b6eb50..fabe41df 100644 --- a/src/core/app.module.core.ts +++ b/src/core/app.module.core.ts @@ -2,12 +2,14 @@ import { ConsoleLogger, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { PassportModule } from '@nestjs/passport'; import { ServeStaticModule } from '@nestjs/serve-static'; +import { TerminusModule } from '@nestjs/terminus'; import { AuthController } from '../api/auth.controller'; import { ChatsController } from '../api/chats.controller'; import { ChattingController } from '../api/chatting.controller'; import { ContactsController } from '../api/contacts.controller'; import { GroupsController } from '../api/groups.controller'; +import { HealthController } from '../api/health.controller'; import { PresenceController } from '../api/presence.controller'; import { ScreenshotController } from '../api/screenshot.controller'; import { @@ -18,6 +20,8 @@ import { StatusController } from '../api/status.controller'; import { VersionController } from '../api/version.controller'; import { WhatsappConfigService } from '../config.service'; import { SessionManager } from './abc/manager.abc'; +import { WAHAHealthCheckService } from './abc/WAHAHealthCheckService'; +import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore'; import { SessionManagerCore } from './manager.core'; export const IMPORTS = [ @@ -38,6 +42,7 @@ export const IMPORTS = [ }, }), PassportModule, + TerminusModule, ]; export const CONTROLLERS = [ AuthController, @@ -51,12 +56,17 @@ export const CONTROLLERS = [ PresenceController, ScreenshotController, VersionController, + HealthController, ]; const PROVIDERS = [ { provide: SessionManager, useClass: SessionManagerCore, }, + { + provide: WAHAHealthCheckService, + useClass: WAHAHealthCheckServiceCore, + }, WhatsappConfigService, ConsoleLogger, ]; diff --git a/src/core/health/WAHAHealthCheckServiceCore.ts b/src/core/health/WAHAHealthCheckServiceCore.ts new file mode 100644 index 00000000..8d30233a --- /dev/null +++ b/src/core/health/WAHAHealthCheckServiceCore.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { HealthCheckResult } from '@nestjs/terminus'; + +import { WAHAHealthCheckService } from '../abc/WAHAHealthCheckService'; +import { AvailableInPlusVersion } from '../exceptions'; + +@Injectable() +export class WAHAHealthCheckServiceCore extends WAHAHealthCheckService { + check(): Promise { + throw new AvailableInPlusVersion(); + } +} diff --git a/src/utils/promiseTimeout.ts b/src/utils/promiseTimeout.ts new file mode 100644 index 00000000..2daf133c --- /dev/null +++ b/src/utils/promiseTimeout.ts @@ -0,0 +1,34 @@ +/** + * An errors which gets raised when the timeout + * exceeded + * + * @internal + */ +export class TimeoutError extends Error {} + +/** + * Executes a promise in the given timeout. If the promise + * does not finish in the given timeout, it will + * raise a TimeoutError + * + * @param {number} ms The timeout in milliseconds + * @param {Promise} promise The promise which should get executed + * + * @internal + */ +export const promiseTimeout = function ( + ms: number, + promise: Promise, +): Promise { + let timer: NodeJS.Timeout; + return Promise.race([ + promise, + new Promise( + (_, reject) => + (timer = setTimeout( + () => reject(new TimeoutError(`Timed out in ${ms}ms.`)), + ms, + )), + ), + ]).finally(() => clearTimeout(timer)); +}; diff --git a/yarn.lock b/yarn.lock index 43046b81..74067c0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -955,6 +955,17 @@ __metadata: languageName: node linkType: hard +"@nestjs/axios@npm:^3.0.2": + version: 3.0.2 + resolution: "@nestjs/axios@npm:3.0.2" + peerDependencies: + "@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + axios: ^1.3.1 + rxjs: ^6.0.0 || ^7.0.0 + checksum: 285a735fb5db602b63aa4a37e161f609b2cec05b69f4bffe983617c2136ac29c0a33bb96e6276d22a656907bed5d53460e740310bc05c043dcd39c37db7cda29 + languageName: node + linkType: hard + "@nestjs/cli@npm:^9.0.0": version: 9.5.0 resolution: "@nestjs/cli@npm:9.5.0" @@ -1153,6 +1164,61 @@ __metadata: languageName: node linkType: hard +"@nestjs/terminus@npm:^10.2.3": + version: 10.2.3 + resolution: "@nestjs/terminus@npm:10.2.3" + dependencies: + boxen: 5.1.2 + check-disk-space: 3.4.0 + peerDependencies: + "@grpc/grpc-js": "*" + "@grpc/proto-loader": "*" + "@mikro-orm/core": "*" + "@mikro-orm/nestjs": "*" + "@nestjs/axios": ^1.0.0 || ^2.0.0 || ^3.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 + "@nestjs/core": ^9.0.0 || ^10.0.0 + "@nestjs/microservices": ^9.0.0 || ^10.0.0 + "@nestjs/mongoose": ^9.0.0 || ^10.0.0 + "@nestjs/sequelize": ^9.0.0 || ^10.0.0 + "@nestjs/typeorm": ^9.0.0 || ^10.0.0 + "@prisma/client": "*" + mongoose: "*" + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: "*" + typeorm: "*" + peerDependenciesMeta: + "@grpc/grpc-js": + optional: true + "@grpc/proto-loader": + optional: true + "@mikro-orm/core": + optional: true + "@mikro-orm/nestjs": + optional: true + "@nestjs/axios": + optional: true + "@nestjs/microservices": + optional: true + "@nestjs/mongoose": + optional: true + "@nestjs/sequelize": + optional: true + "@nestjs/typeorm": + optional: true + "@prisma/client": + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + checksum: 7a758c845c0a07a75230731d3b638919aae0b503b6cef97fa7f63d4cd23c6c6db0e8d39d5ecf4d8fda6e3f0aa7d4aa95fddcf5868758cf85d201db6acc43ac9d + languageName: node + linkType: hard + "@nestjs/testing@npm:^9.0.9": version: 9.4.3 resolution: "@nestjs/testing@npm:9.4.3" @@ -3216,7 +3282,7 @@ __metadata: languageName: node linkType: hard -"boxen@npm:^5.1.1": +"boxen@npm:5.1.2, boxen@npm:^5.1.1": version: 5.1.2 resolution: "boxen@npm:5.1.2" dependencies: @@ -3592,6 +3658,13 @@ __metadata: languageName: node linkType: hard +"check-disk-space@npm:3.4.0, check-disk-space@npm:^3.4.0": + version: 3.4.0 + resolution: "check-disk-space@npm:3.4.0" + checksum: 0154c149c34a1233fe542f2a9f9ea2526bb90169489259eda8de1cf6870f765e8f5299c3b085085cc1ef33d9559f589fd10609c7e52d2e624ef6b5720dd9743c + languageName: node + linkType: hard + "cheerio-select@npm:^2.1.0": version: 2.1.0 resolution: "cheerio-select@npm:2.1.0" @@ -13216,6 +13289,7 @@ __metadata: dependencies: "@adiwajshing/baileys": "github:WhiskeySockets/Baileys" "@adiwajshing/keyed-db": ^0.2.4 + "@nestjs/axios": ^3.0.2 "@nestjs/cli": ^9.0.0 "@nestjs/common": ^9.0.9 "@nestjs/config": ^0.5.0 @@ -13225,6 +13299,7 @@ __metadata: "@nestjs/schematics": ^9.0.1 "@nestjs/serve-static": ^2.1.3 "@nestjs/swagger": ^7.1.11 + "@nestjs/terminus": ^10.2.3 "@nestjs/testing": ^9.0.9 "@types/express": ^4.17.3 "@types/jest": 26.0.10 @@ -13234,6 +13309,7 @@ __metadata: "@types/ws": ^8.5.4 "@typescript-eslint/eslint-plugin": 3.9.1 "@typescript-eslint/parser": 3.9.1 + check-disk-space: ^3.4.0 class-validator: ^0.12.2 del: ^6.0.0 eslint: 7.7.0