@sera4/essentia 1.1.11 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cache/index.js +188 -0
- package/health/health-logger.js +40 -0
- package/health/index.js +64 -21
- package/package.json +3 -3
- package/package.tar.gz +0 -0
package/cache/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
|
|
5
|
+
const isEmpty = obj => [Object, Array].includes((obj || {}).constructor) && !Object.entries((obj || {})).length;
|
|
6
|
+
|
|
7
|
+
class CacheConfigError extends Error {
|
|
8
|
+
constructor(m) {
|
|
9
|
+
super(m);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Redis cache helper
|
|
14
|
+
* Example use: Place the following code in your project to setup redis use
|
|
15
|
+
*
|
|
16
|
+
* import { cache } from "@sera4/essentia";
|
|
17
|
+
* const cacheHelper = cache({ password: .., database: .., host: .., port: .. }, "service:"));
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const convertRedisKeyValueToJson = (v) => {
|
|
22
|
+
return (v && v.length > 0 && v[0] !== null) ? JSON.parse(v) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const convertRedisHashToJson = (v) => {
|
|
26
|
+
if (v) {
|
|
27
|
+
var result = {};
|
|
28
|
+
for(var k in v) {
|
|
29
|
+
if (v.hasOwnProperty(k)) {
|
|
30
|
+
result[k] = JSON.parse(v[k]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
} else {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function convertJsonToRedisValue(j) {
|
|
40
|
+
var result = {};
|
|
41
|
+
for (var key in j) {
|
|
42
|
+
if (j.hasOwnProperty(key)) {
|
|
43
|
+
result[key] = JSON.stringify(j[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Cache {
|
|
50
|
+
constructor(config, servicePrefix) {
|
|
51
|
+
if ((config == undefined) || isEmpty(config)) {
|
|
52
|
+
throw new CacheConfigError("Invalid cache configuration");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (servicePrefix == null) {
|
|
56
|
+
throw new CacheConfigError("Invalid service prefix");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (config.fake) {
|
|
60
|
+
// used in testing
|
|
61
|
+
this.redisClient = require("fakeredis").createClient("fake backend")
|
|
62
|
+
} else {
|
|
63
|
+
this.redisClient = require("redis").createClient(config);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.redisClient.on("error", (err) => {
|
|
67
|
+
logger.error("Cache/Redis error: ", err);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.servicePrefix = servicePrefix;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ping() {
|
|
74
|
+
return this.redisClient.ping();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
properKey(key, path) {
|
|
78
|
+
let p = this.servicePrefix + key
|
|
79
|
+
if (path)
|
|
80
|
+
p += path
|
|
81
|
+
return p
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
writeHash(path, key, value) {
|
|
85
|
+
return this.redisClient.hsetAsync(properKey(key), key, value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeHashBlocking(path, key, value) {
|
|
89
|
+
return this.redisClient.hset(properKey(key), key, value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// write multiple hash keys and stringify every value
|
|
93
|
+
writeMultiHash(path, j) {
|
|
94
|
+
return this.redisClient.hmsetAsync(properKey(key), convertJsonToRedisValue(j));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
readHashKey(obj, key, autoConvert=true) {
|
|
98
|
+
var k = autoConvert;
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.redisClient.hget(properKey(key, obj), key, (e, r) => {
|
|
102
|
+
if (e) {
|
|
103
|
+
logger.error("Redis hmget error:", e);
|
|
104
|
+
reject(e);
|
|
105
|
+
} else {
|
|
106
|
+
resolve(k ? convertRedisKeyValueToJson(r) : r);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
readHash(path) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
this.redisClient.hgetall(properKey(key, path), (e, r) => {
|
|
115
|
+
if (e) {
|
|
116
|
+
console.log("Redis hget error:", e);
|
|
117
|
+
reject(e);
|
|
118
|
+
} else {
|
|
119
|
+
resolve(convertRedisHashToJson(r));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
deleteHashKey(hash, key) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
this.redisClient.hdel(properKey(key, hash), key, (err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
logger.error("Redis hdel error:", err);
|
|
130
|
+
reject(err);
|
|
131
|
+
} else {
|
|
132
|
+
resolve();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get(key) {
|
|
139
|
+
return this.redisClient.getAsync(properKey(key));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getAsync(key) {
|
|
143
|
+
return promisify(this.redisClient.get(key)).bind(this.redisClient)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/* a single key write with optional expiry */
|
|
148
|
+
// returns bluebird promise
|
|
149
|
+
writeKey(key, value, expiry) {
|
|
150
|
+
key = this.servicePrefix + key;
|
|
151
|
+
if (expiry) {
|
|
152
|
+
return this.redisClient.setAsync(key, value, "EX", expiry);
|
|
153
|
+
} else {
|
|
154
|
+
return this.redisClient.setAsync(key, value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
deleteKey(key) {
|
|
159
|
+
return redisClient.delAsync(properKey(key));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expire(path, expiry) {
|
|
163
|
+
return this.redisClient.expire(path, expiry);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
exists(path) {
|
|
167
|
+
return this.redisClient.exists(path);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
deleteAllKeys() {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
if(process.env.NODE_ENV === "production") {
|
|
173
|
+
throw new Error("Clearing system cache is not permitted in production");
|
|
174
|
+
}
|
|
175
|
+
this.redisClient.flushdb((err, res) => {
|
|
176
|
+
if (err) {
|
|
177
|
+
reject(err);
|
|
178
|
+
} else {
|
|
179
|
+
resolve(res);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default function(config, servicePrefix) {
|
|
187
|
+
return new Cache(config, servicePrefix)
|
|
188
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logger from "../logger/s4-logger.js";
|
|
2
|
+
let enabled = false;
|
|
3
|
+
|
|
4
|
+
const helpers = {
|
|
5
|
+
debug(...args) {
|
|
6
|
+
if (enabled) {
|
|
7
|
+
logger.debug(":: HealthCheck ::", args);
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
log(...args) {
|
|
12
|
+
if (enabled) {
|
|
13
|
+
logger.debug(":: HealthCheck ::", args);
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
warn(...args) {
|
|
18
|
+
if (enabled) {
|
|
19
|
+
logger.warn(":: HealthCheck ::", args);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
info(...args) {
|
|
24
|
+
if (enabled) {
|
|
25
|
+
logger.info(":: HealthCheck ::", args);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
error(...args) {
|
|
30
|
+
if (enabled) {
|
|
31
|
+
logger.error(":: HealthCheck ::", args);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
setEnabled(e) {
|
|
36
|
+
enabled = e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default helpers;
|
package/health/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
3
|
import pathHelper from "path";
|
|
4
|
+
import logger from "./health-logger.js";
|
|
4
5
|
|
|
5
6
|
class HealthCheckError extends Error {
|
|
6
7
|
constructor(m) {
|
|
@@ -15,12 +16,16 @@ class HealthCheckError extends Error {
|
|
|
15
16
|
* const router = require('express').Router();
|
|
16
17
|
*
|
|
17
18
|
* ....
|
|
19
|
+
*
|
|
20
|
+
* const checkRedis = async () => { await cache.ping(); }
|
|
21
|
+
* hc.setupCheck({ name: "redis", checkResult: false, critical: true, healthFunction: checkRedis });
|
|
18
22
|
* app.use(hc.defaultPath(serverConfigs.api_prefix), hc.routes(router));
|
|
19
23
|
*
|
|
20
24
|
*/
|
|
21
|
-
|
|
22
25
|
class HealthCheck {
|
|
23
|
-
constructor(appendData = {}) {
|
|
26
|
+
constructor(appendData = {}, enableLogging = true) {
|
|
27
|
+
logger.setEnabled(enableLogging);
|
|
28
|
+
|
|
24
29
|
this.defaultResponse = {
|
|
25
30
|
...
|
|
26
31
|
appendData,
|
|
@@ -29,11 +34,13 @@ class HealthCheck {
|
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
this.defaultPathRoute = `health_check\(.*\)?`
|
|
37
|
+
this.suppressLogs = false;
|
|
32
38
|
this.lastCallId = 0; // default start point/integer
|
|
33
39
|
this.healthChecks = [];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
/*
|
|
42
|
+
/* Deprecated and replaced by setupCheck
|
|
43
|
+
* call this with an async function, must support wait capability
|
|
37
44
|
*
|
|
38
45
|
* const dbCheck = async () => {
|
|
39
46
|
* return { name: "db", "result": "success", critical: false };
|
|
@@ -60,15 +67,40 @@ class HealthCheck {
|
|
|
60
67
|
return router
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
// v2 version of health check, replaced addCheck
|
|
71
|
+
// example:
|
|
72
|
+
// const checkRedisFcn = async () => { await cache.ping().then(() => true) }
|
|
73
|
+
// setupCheck({ name: "redis", checkResult: false, critical: false, healthFunction: checkRedisFcn });
|
|
74
|
+
setupCheck(config) {
|
|
75
|
+
this.healthChecks.push(config);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* performs health checks and returns 200 if true and 500 if not */
|
|
64
79
|
async checkHealth(req, res) {
|
|
65
80
|
const checkFormat = pathHelper.extname(req.baseUrl);
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
const result = await Promise.all(this.healthChecks.map(async hc => {
|
|
83
|
+
if (typeof(hc) === "function") {
|
|
84
|
+
try {
|
|
85
|
+
return await hc();
|
|
86
|
+
return { "result": "success", critical: false };
|
|
87
|
+
} catch(e) {
|
|
88
|
+
return { name: "unknown", result: 'fail', status: e.status }
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
let res = { name: hc.name, critical: hc.critical }
|
|
92
|
+
try {
|
|
93
|
+
await hc.healthFunction();
|
|
94
|
+
return { ...res, "result": "success" };
|
|
95
|
+
} catch(e) {
|
|
96
|
+
const appendStatus = e.status ? ` (${e.status})` : ""
|
|
97
|
+
logger.warn(`${hc.name} service failed${appendStatus}` )
|
|
98
|
+
return { ...res, "result": "fail", status: e.status }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
71
102
|
|
|
103
|
+
try {
|
|
72
104
|
let callId = ++this.lastCallId;
|
|
73
105
|
|
|
74
106
|
// start the counter over
|
|
@@ -78,10 +110,18 @@ class HealthCheck {
|
|
|
78
110
|
|
|
79
111
|
let status = this.defaultResponse.status;
|
|
80
112
|
|
|
113
|
+
let nonCriticalFail = false;
|
|
114
|
+
let criticalFail = false;
|
|
115
|
+
|
|
81
116
|
result.some(e => {
|
|
82
117
|
if (e.result.match(/fail/)) {
|
|
83
|
-
|
|
84
|
-
|
|
118
|
+
if (e.critical == false) {
|
|
119
|
+
nonCriticalFail = true;
|
|
120
|
+
} else {
|
|
121
|
+
criticalFail = true;
|
|
122
|
+
status = 500;
|
|
123
|
+
return true; // break
|
|
124
|
+
}
|
|
85
125
|
}
|
|
86
126
|
})
|
|
87
127
|
|
|
@@ -89,33 +129,36 @@ class HealthCheck {
|
|
|
89
129
|
res.setHeader('check', callId)
|
|
90
130
|
|
|
91
131
|
if (checkFormat === ".json") {
|
|
92
|
-
const payload = { "check": callId }
|
|
132
|
+
const payload = { "check": callId, healthy: "true", message: "success" }
|
|
93
133
|
|
|
94
|
-
if (
|
|
134
|
+
if (criticalFail) {
|
|
95
135
|
res.status(status).json({
|
|
96
136
|
... payload,
|
|
97
|
-
"healthy":
|
|
98
|
-
"message":"
|
|
137
|
+
"healthy": false,
|
|
138
|
+
"message": "fail",
|
|
139
|
+
"services": result
|
|
99
140
|
});
|
|
100
|
-
} else {
|
|
141
|
+
} else if (nonCriticalFail) {
|
|
101
142
|
res.status(status).json({
|
|
102
143
|
... payload,
|
|
103
|
-
"healthy": false,
|
|
104
144
|
"message": "fail",
|
|
105
|
-
"services":
|
|
145
|
+
"services": result
|
|
106
146
|
});
|
|
147
|
+
} else if (status === 200) {
|
|
148
|
+
res.status(status).json(payload);
|
|
107
149
|
}
|
|
108
150
|
} else {
|
|
109
151
|
let status_message;
|
|
110
|
-
if (
|
|
111
|
-
status_message = "success"
|
|
112
|
-
} else {
|
|
152
|
+
if (criticalFail) {
|
|
113
153
|
status_message = "fail"
|
|
114
|
-
}
|
|
154
|
+
} else if ((status === 200) || nonCriticalFail) {
|
|
155
|
+
status_message = "success"
|
|
156
|
+
}
|
|
115
157
|
|
|
116
158
|
res.status(status).send(status_message);
|
|
117
159
|
}
|
|
118
160
|
} catch(error) {
|
|
161
|
+
console.debug("error: ", error)
|
|
119
162
|
res.status(500).send();
|
|
120
163
|
}
|
|
121
164
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sera4/essentia",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "A library of utilities for Teleporte Web Services",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "NODE_ENV=test ./node_modules/.bin/mocha --forbid-only --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/result.xml --exit",
|
|
9
|
-
"test:dev": "NODE_ENV=test ./node_modules/.bin/mocha --
|
|
9
|
+
"test:dev": "NODE_ENV=test ./node_modules/.bin/mocha --forbid-only",
|
|
10
10
|
"test:only": "NODE_ENV=test ./node_modules/.bin/mocha --watch",
|
|
11
11
|
"test:pretty": "NODE_ENV=test ./node_modules/.bin/mocha --reporter mocha-simple-html-reporter --reporter-options output=./test-results/result.html --exit",
|
|
12
12
|
"last-commit": "node last-commit.js"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"fast-safe-stringify": "^2.0.7",
|
|
35
35
|
"git-rev": "^0.2.1",
|
|
36
36
|
"lodash": "^4.17.21",
|
|
37
|
-
"url-parse": "^1.5.
|
|
37
|
+
"url-parse": "^1.5.3",
|
|
38
38
|
"uuid": "^3.4.0",
|
|
39
39
|
"winston": "^3.3.3",
|
|
40
40
|
"winston-logstash": "^0.4.0"
|
package/package.tar.gz
CHANGED
|
Binary file
|