diff --git a/website/client/vite.config.mjs b/website/client/vite.config.mjs index 2b7a6191ab..94701d68d4 100644 --- a/website/client/vite.config.mjs +++ b/website/client/vite.config.mjs @@ -122,7 +122,7 @@ export default defineConfig({ }, rollupOptions: { output: { - experimentalMinChunkSize: 1000 + experimentalMinChunkSize: 20000 } } }, diff --git a/website/server/controllers/api-v4/status.js b/website/server/controllers/api-v4/status.js new file mode 100644 index 0000000000..980a215d1b --- /dev/null +++ b/website/server/controllers/api-v4/status.js @@ -0,0 +1,39 @@ +import { + disableCache, +} from '../../middlewares/cache'; +import SERVER_STATUS from '../../libs/serverStatus'; + +const api = {}; + +/** + * @api {get} /api/v3/ready Get Habitica's Server readiness status + * @apiName GetReady + * @apiGroup Status + * + * @apiSuccess {String} data.status 'ready' if everything is ok + * + * @apiSuccessExample {JSON} Server is Ready + * { + * 'status': 'ready', + * } + */ +api.getReady = { + method: 'GET', + url: '/ready', + // explicitly disable caching so that the server is always checked + middlewares: [disableCache], + async handler (req, res) { + // This allows kubernetes to determine if the server is ready to receive traffic + if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) { + res.respond(503, { + status: 'not ready', + }); + } else { + res.respond(200, { + status: 'ready', + }); + } + }, +}; + +export default api; diff --git a/website/server/libs/logger.js b/website/server/libs/logger.js index 424d282bdb..23215a9572 100644 --- a/website/server/libs/logger.js +++ b/website/server/libs/logger.js @@ -3,6 +3,7 @@ import winston from 'winston'; import { Loggly } from 'winston-loggly-bulk'; import nconf from 'nconf'; import _ from 'lodash'; +import os from 'os'; import { CustomError, } from './errors'; @@ -65,9 +66,8 @@ if (IS_PROD) { ), })); } - if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) { - const tags = ['Winston-NodeJS']; + const tags = ['Winston-NodeJS', os.hostname()]; if (nconf.get('SERVER_EMOJI')) { tags.push(nconf.get('SERVER_EMOJI')); } diff --git a/website/server/libs/mongoose.js b/website/server/libs/mongoose.js index 661008dd7e..e8884dcbd8 100644 --- a/website/server/libs/mongoose.js +++ b/website/server/libs/mongoose.js @@ -5,6 +5,7 @@ import { getDevelopmentConnectionUrl, getDefaultConnectionOptions, } from './mongodb'; +import SERVER_STATUS from './serverStatus'; const IS_PROD = nconf.get('IS_PROD'); const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE'); @@ -24,6 +25,13 @@ const connectionUrl = IS_PROD ? DB_URI : getDevelopmentConnectionUrl(DB_URI); export default async function connectToMongoDB () { // Do not connect to MongoDB when in maintenance mode if (MAINTENANCE_MODE !== 'true') { + mongoose.connection.on('open', () => { + SERVER_STATUS.MONGODB = true; + }); + mongoose.connection.on('disconnected', () => { + SERVER_STATUS.MONGODB = false; + }); + return mongoose.connect(connectionUrl, mongooseOptions).then(() => { logger.info('Connected with Mongoose.'); }); diff --git a/website/server/libs/serverStatus.js b/website/server/libs/serverStatus.js new file mode 100644 index 0000000000..ecfb6423b9 --- /dev/null +++ b/website/server/libs/serverStatus.js @@ -0,0 +1,7 @@ +const SERVER_STATUS = { + MONGODB: false, + REDIS: false, + EXPRESS: false, +}; + +export default SERVER_STATUS; diff --git a/website/server/middlewares/rateLimiter.js b/website/server/middlewares/rateLimiter.js index 97024fa107..b47e9e9eec 100644 --- a/website/server/middlewares/rateLimiter.js +++ b/website/server/middlewares/rateLimiter.js @@ -10,6 +10,7 @@ import { } from '../libs/errors'; import logger from '../libs/logger'; import { apiError } from '../libs/apiError'; +import SERVER_STATUS from '../libs/serverStatus'; // Middleware to rate limit requests to the API @@ -47,6 +48,14 @@ if (RATE_LIMITER_ENABLED) { enable_offline_queue: false, }); + redisClient.on('ready', () => { + SERVER_STATUS.REDIS = true; + }); + + redisClient.on('reconnecting', () => { + SERVER_STATUS.REDIS = false; + }); + redisClient.on('error', error => { logger.error(error, 'Redis Error'); }); @@ -56,6 +65,8 @@ if (RATE_LIMITER_ENABLED) { storeClient: redisClient, }); } +} else { + SERVER_STATUS.REDIS = true; } function setResponseHeaders (res, rateLimiterRes) { diff --git a/website/server/middlewares/requestLogHandler.js b/website/server/middlewares/requestLogHandler.js index c398441103..ab2d4e9582 100644 --- a/website/server/middlewares/requestLogHandler.js +++ b/website/server/middlewares/requestLogHandler.js @@ -41,7 +41,7 @@ export const logRequestData = (req, res, next) => { export const logSlowRequests = (req, res, next) => { req.requestStartTime = Date.now(); - req.on('close', () => { + req.once('close', () => { const requestTime = Date.now() - req.requestStartTime; if (requestTime > SLOW_REQUEST_THRESHOLD) { const data = buildBaseLogData(req); diff --git a/website/server/server.js b/website/server/server.js index 24aec935bd..06326bca90 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -1,6 +1,8 @@ import nconf from 'nconf'; import express from 'express'; import http from 'http'; +import mongoose from 'mongoose'; +import redis from 'redis'; import logger from './libs/logger'; // Setup translations @@ -18,12 +20,22 @@ import './libs/setupFirebase'; import './models/challenge'; import './models/group'; import './models/user'; +import SERVER_STATUS from './libs/serverStatus'; connectToMongoDB(); const server = http.createServer(); const app = express(); +process.on('SIGTERM', async () => { + console.log('SIGTERM signal received: closing HTTP server'); + server.close(async () => { + await mongoose.disconnect(); + await redis.quit(); + process.exit(0); + }); +}); + app.set('port', nconf.get('PORT')); attachMiddlewares(app, server); @@ -31,6 +43,7 @@ attachMiddlewares(app, server); server.on('request', app); server.listen(app.get('port'), () => { logger.info(`Express server listening on port ${app.get('port')}`); + SERVER_STATUS.EXPRESS = true; }); export default server;