Просмотр исходного кода

Merge pull request #1565 from weseek/imprv/healthcheck

Imprv/healthcheck
Yuki Takei 6 лет назад
Родитель
Сommit
91539ee65e

+ 19 - 0
src/server/models/vo/error-apiv3.js

@@ -1,3 +1,22 @@
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      ErrorV3:
+ *        description: Error for APIv3
+ *        type: object
+ *        properties:
+ *          message:
+ *            type: string
+ *            example: 'error message'
+ *          code:
+ *            type: string
+ *            example: 'someapi-error-with-something'
+ *          stack:
+ *            type: object
+ */
+
 class ErrorV3 extends Error {
 
   constructor(message = '', code = '', stack = undefined) {

+ 84 - 15
src/server/routes/apiv3/healthcheck.js

@@ -7,6 +7,7 @@ const express = require('express');
 const router = express.Router();
 
 const helmet = require('helmet');
+const ErrorV3 = require('../../models/vo/error-apiv3');
 
 /**
  * @swagger
@@ -14,6 +15,38 @@ const helmet = require('helmet');
  *    name: Healthcheck
  */
 
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      HealthcheckInfo:
+ *        description: Information of middlewares
+ *        type: object
+ *        properties:
+ *          mongo:
+ *            type: string
+ *            description: 'OK'
+ *            example: 'OK'
+ *          searchInfo:
+ *            type: object
+ *            example: {
+ *              "esVersion":"6.6.1",
+ *              "esNodeInfos":{
+ *                "6pnILIqFT_Cjbs4mwQfcmA": {
+ *                  "name":"6pnILIq",
+ *                  "version":"6.6.1",
+ *                  "plugins":[
+ *                    {"name":"analysis-icu","version":"6.6.1"},
+ *                    {"name":"analysis-kuromoji","version":"6.6.1"},
+ *                    {"name":"ingest-geoip","version":"6.6.1"},
+ *                    {"name":"ingest-user-agent","version":"6.6.1"}
+ *                  ]
+ *                }
+ *              }
+ *            }
+ */
+
 module.exports = (crowi) => {
   /**
    * @swagger
@@ -27,44 +60,80 @@ module.exports = (crowi) => {
    *      parameters:
    *        - name: connectToMiddlewares
    *          in: query
-   *          description: Check also MongoDB and SearchService
+   *          description: Check MongoDB and SearchService (consider as healthy even if any of middleware is available or not)
+   *          schema:
+   *            type: boolean
+   *        - name: checkMiddlewaresStrictly
+   *          in: query
+   *          description: Check MongoDB and SearchService and responds 503 if either of these is unhealthy
    *          schema:
    *            type: boolean
    *      responses:
    *        200:
-   *          description: Resources are available
+   *          description: Healthy
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  info:
+   *                    $ref: '#/components/schemas/HealthcheckInfo'
+   *        503:
+   *          description: Unhealthy
    *          content:
    *            application/json:
    *              schema:
    *                properties:
-   *                  mongo:
-   *                    type: string
-   *                    description: 'OK'
-   *                  searchInfo:
-   *                    type: object
+   *                  errors:
+   *                    type: array
+   *                    description: Errors
+   *                    items:
+   *                      $ref: '#/components/schemas/ErrorV3'
+   *                  info:
+   *                    $ref: '#/components/schemas/HealthcheckInfo'
    */
   router.get('/', helmet.noCache(), async(req, res) => {
-    const connectToMiddlewares = req.query.connectToMiddlewares;
+    const connectToMiddlewares = req.query.connectToMiddlewares != null;
+    const checkMiddlewaresStrictly = req.query.checkMiddlewaresStrictly != null;
 
     // return 200 w/o connecting to MongoDB and SearchService
-    if (connectToMiddlewares == null) {
+    if (!connectToMiddlewares && !checkMiddlewaresStrictly) {
       res.status(200).send({ status: 'OK' });
       return;
     }
 
+    const errors = [];
+    const info = {};
+
+    // connect to MongoDB
     try {
-      // connect to MongoDB
       const Config = crowi.models.Config;
       await Config.findOne({});
-      // connect to Elasticsearch
-      const search = crowi.getSearcher();
-      const searchInfo = await search.getInfo();
 
-      res.status(200).send({ mongo: 'OK', searchInfo });
+      info.mongo = 'OK';
     }
     catch (err) {
-      res.status(503).send({ err });
+      errors.push(new ErrorV3(`MongoDB is not connectable - ${err.message}`, 'healthcheck-mongodb-unhealthy', err.stack));
+    }
+
+    // connect to search service
+    try {
+      const search = crowi.getSearcher();
+      info.searchInfo = await search.getInfo();
     }
+    catch (err) {
+      errors.push(new ErrorV3(`The Search Service is not connectable - ${err.message}`, 'healthcheck-search-unhealthy', err.stack));
+    }
+
+    if (errors.length > 0) {
+      let httpStatus = 200;
+      if (checkMiddlewaresStrictly) {
+        httpStatus = 503;
+      }
+
+      return res.apiv3Err(errors, httpStatus, info);
+    }
+
+    res.status(200).send({ info });
   });
 
   return router;

+ 2 - 2
src/server/routes/apiv3/response.js

@@ -13,7 +13,7 @@ const addCustomFunctionToResponse = (express, crowi) => {
     this.json({ data: obj });
   };
 
-  express.response.apiv3Err = function(_err, status = 400) { // not arrow function
+  express.response.apiv3Err = function(_err, status = 400, info) { // not arrow function
     if (!Number.isInteger(status)) {
       throw new Error('invalid status supplied to res.apiv3Err');
     }
@@ -33,7 +33,7 @@ const addCustomFunctionToResponse = (express, crowi) => {
       throw new Error('invalid error supplied to res.apiv3Err');
     });
 
-    this.status(status).json({ errors });
+    this.status(status).json({ errors, info });
   };
 };
 

+ 27 - 42
src/server/service/search-delegator/elasticsearch.js

@@ -21,9 +21,6 @@ class ElasticsearchDelegator {
     this.configManager = configManager;
     this.searchEvent = searchEvent;
 
-    this.esVersion = 'unknown';
-    this.esNodeInfos = {};
-
     this.client = null;
 
     // In Elasticsearch RegExp, we don't need to used ^ and $.
@@ -68,11 +65,33 @@ class ElasticsearchDelegator {
     this.indexName = indexName;
   }
 
-  getInfo() {
-    return {
-      esVersion: this.esVersion,
-      esNodeInfos: this.esNodeInfos,
-    };
+  async getInfo() {
+    const info = await this.client.nodes.info();
+    if (!info._nodes || !info.nodes) {
+      throw new Error('There is no nodes');
+    }
+
+    let esVersion = 'unknown';
+    const esNodeInfos = {};
+
+    for (const [nodeName, nodeInfo] of Object.entries(info.nodes)) {
+      esVersion = nodeInfo.version;
+
+      const filteredInfo = {
+        name: nodeInfo.name,
+        version: nodeInfo.version,
+        plugins: nodeInfo.plugins.map((pluginInfo) => {
+          return {
+            name: pluginInfo.name,
+            version: pluginInfo.version,
+          };
+        }),
+      };
+
+      esNodeInfos[nodeName] = filteredInfo;
+    }
+
+    return { esVersion, esNodeInfos };
   }
 
   /**
@@ -156,41 +175,7 @@ class ElasticsearchDelegator {
     await client.indices.delete({ index: tmpIndexName });
   }
 
-  /**
-   * retrieve elasticsearch node information
-   */
-  async checkESVersion() {
-    try {
-      const info = await this.client.nodes.info();
-      if (!info._nodes || !info.nodes) {
-        throw new Error('no nodes info');
-      }
-
-      for (const [nodeName, nodeInfo] of Object.entries(info.nodes)) {
-        this.esVersion = nodeInfo.version;
-
-        const filteredInfo = {
-          name: nodeInfo.name,
-          version: nodeInfo.version,
-          plugins: nodeInfo.plugins.map((pluginInfo) => {
-            return {
-              name: pluginInfo.name,
-              version: pluginInfo.version,
-            };
-          }),
-        };
-
-        this.esNodeInfos[nodeName] = filteredInfo;
-      }
-    }
-    catch (error) {
-      logger.error('Couldn\'t check ES version:', error);
-    }
-  }
-
   async initIndices() {
-    await this.checkESVersion();
-
     const { client, indexName, aliasName } = this;
 
     const tmpIndexName = `${indexName}-tmp`;

+ 1 - 1
src/server/service/search.js

@@ -64,7 +64,7 @@ class SearchService {
     tagEvent.on('update', this.delegator.syncTagChanged.bind(this.delegator));
   }
 
-  getInfo() {
+  async getInfo() {
     return this.delegator.getInfo();
   }