soajs.controller 4.3.10 → 4.3.12
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/lib/logOnce.js +45 -0
- package/mw/awareness/custom.js +2 -2
- package/mw/awareness/ha.js +3 -1
- package/mw/awarenessEnv/custom.js +2 -2
- package/mw/cache/index.js +5 -2
- package/mw/cache/model/mongo.js +10 -2
- package/mw/gotoService/index.js +4 -2
- package/mw/gotoService/lib/preRedirect.js +10 -8
- package/mw/gotoService/redirectToService.js +3 -2
- package/mw/idempotency/index.js +3 -1
- package/mw/idempotency/model/mongo.js +10 -2
- package/mw/mt/index.js +3 -1
- package/mw/mt/utils.js +2 -4
- package/mw/traffic/index.js +3 -1
- package/mw/traffic/model/mongo.js +5 -1
- package/mw/url/index.js +3 -1
- package/package.json +1 -1
- package/server/controller.js +2 -2
- package/server/maintenance.js +3 -3
- package/test/unit/index.js +1 -0
- package/test/unit/lib/logOnce.js +81 -0
- package/test/unit/utilities/utils.js +206 -19
- package/utilities/utils.js +77 -6
package/lib/logOnce.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const reported = new Set();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log a message once per distinct key.
|
|
15
|
+
*
|
|
16
|
+
* Static misconfiguration is discovered inside the request path, so reporting it
|
|
17
|
+
* normally would repeat on every request for the lifetime of the process. The
|
|
18
|
+
* condition cannot resolve itself without a registry change, so the first report
|
|
19
|
+
* carries all the information the rest would.
|
|
20
|
+
*
|
|
21
|
+
* @param log the soajs logger
|
|
22
|
+
* @param level one of the logger levels, ex: error
|
|
23
|
+
* @param key identifies the condition, repeats with the same key are dropped
|
|
24
|
+
* @param message
|
|
25
|
+
*/
|
|
26
|
+
function logOnce(log, level, key, message) {
|
|
27
|
+
if (reported.has(key)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (!log || typeof log[level] !== "function") {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
reported.add(key);
|
|
34
|
+
log[level](message);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Forget what has been reported. Used by the unit tests.
|
|
40
|
+
*/
|
|
41
|
+
function resetLogOnce() {
|
|
42
|
+
reported.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { logOnce, resetLogOnce };
|
package/mw/awareness/custom.js
CHANGED
|
@@ -145,7 +145,7 @@ let awareness_healthCheck = function (core, log) {
|
|
|
145
145
|
|
|
146
146
|
let stopLoggingAfter = registry.serviceConfig.awareness.maxLogCount;
|
|
147
147
|
if (statusObj.downCount <= stopLoggingAfter) {
|
|
148
|
-
log.
|
|
148
|
+
log.error("Self Awareness health check for service [" + sObj.name + "] for host [" + sObj.host + "] is NOT healthy");
|
|
149
149
|
}
|
|
150
150
|
if (serviceAwarenessObj[sObj.name].healthy[sObj.version].indexOf(sObj.host) !== -1) {
|
|
151
151
|
//TODO: if we guarantee uniqueness we will not need the for loop
|
|
@@ -192,7 +192,7 @@ let awareness_healthCheck = function (core, log) {
|
|
|
192
192
|
}
|
|
193
193
|
}, function (err) {
|
|
194
194
|
if (err) {
|
|
195
|
-
log.
|
|
195
|
+
log.error('Unable to build awareness structure for services: ' + err);
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
198
|
setTimeout(() => {
|
package/mw/awareness/ha.js
CHANGED
|
@@ -176,7 +176,9 @@ let lib = {
|
|
|
176
176
|
}, function () {
|
|
177
177
|
awarenessCache = myCache;
|
|
178
178
|
param.log.debug("Awareness cache rebuilt successfully");
|
|
179
|
-
param.log.debug(
|
|
179
|
+
if (param.log.debug()) {
|
|
180
|
+
param.log.debug(JSON.stringify(awarenessCache));
|
|
181
|
+
}
|
|
180
182
|
|
|
181
183
|
let cacheTTL = registryModule.get().serviceConfig.awareness.cacheTTL;
|
|
182
184
|
if (cacheTTL) {
|
|
@@ -26,7 +26,7 @@ let fetchControllerHosts = function (core, log, next) {
|
|
|
26
26
|
registry = registryModule.get();
|
|
27
27
|
registryModule.loadOtherEnvControllerHosts(gatewayServiceName, function (error, hosts) {
|
|
28
28
|
if (error) {
|
|
29
|
-
log.
|
|
29
|
+
log.error("Failed to load controller hosts. reusing from previous load. Reason: " + error.message);
|
|
30
30
|
} else {
|
|
31
31
|
controllerHosts = hosts;
|
|
32
32
|
}
|
|
@@ -107,7 +107,7 @@ let awareness_healthCheck = function (core, log) {
|
|
|
107
107
|
// });
|
|
108
108
|
}, function (err) {
|
|
109
109
|
if (err) {
|
|
110
|
-
log.
|
|
110
|
+
log.error('Unable to build awareness ENV structure for controllers: ' + err);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
}
|
package/mw/cache/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const crypto = require('crypto');
|
|
|
12
12
|
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
13
13
|
const memory = require('./model/memory.js');
|
|
14
14
|
const mongo = require('./model/mongo.js');
|
|
15
|
+
const { logOnce } = require('../../lib/logOnce.js');
|
|
15
16
|
|
|
16
17
|
let model = { memory, mongo };
|
|
17
18
|
|
|
@@ -102,7 +103,8 @@ module.exports = function (configuration) {
|
|
|
102
103
|
|
|
103
104
|
const modelType = cacheConfig.model || 'memory';
|
|
104
105
|
if (!model[modelType]) {
|
|
105
|
-
configuration.log
|
|
106
|
+
logOnce(configuration.log, "error", "cache_model_" + modelType,
|
|
107
|
+
'Cache: Unknown model [' + modelType + ']. It can only be [memory || mongo]. Caching is off.');
|
|
106
108
|
return next();
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -119,7 +121,8 @@ module.exports = function (configuration) {
|
|
|
119
121
|
let scope = apiConfig.scope;
|
|
120
122
|
if (scope !== 'tenant' && scope !== 'tenant_user') {
|
|
121
123
|
if (scope) {
|
|
122
|
-
configuration.log
|
|
124
|
+
logOnce(configuration.log, "error", "cache_scope_" + scope + "_" + method + "_" + apiPath,
|
|
125
|
+
'Cache: Unknown scope [' + scope + '] for API [' + method + ' ' + apiPath + ']. It can only be [tenant || tenant_user]. Falling back to default.');
|
|
123
126
|
}
|
|
124
127
|
scope = isAPIPublic ? 'tenant' : 'tenant_user';
|
|
125
128
|
}
|
package/mw/cache/model/mongo.js
CHANGED
|
@@ -22,10 +22,18 @@ let model = {
|
|
|
22
22
|
if (!model.mongo) {
|
|
23
23
|
model.mongo = new Mongo(dbConfiguration);
|
|
24
24
|
model.mongo.createIndex(cache_store, { 'l1': 1, 'l2': 1 }, { unique: true }, (err, index) => {
|
|
25
|
-
|
|
25
|
+
if (err) {
|
|
26
|
+
log.error("Cache index creation failed: " + err.message);
|
|
27
|
+
} else {
|
|
28
|
+
log.debug("Cache index created: " + index);
|
|
29
|
+
}
|
|
26
30
|
});
|
|
27
31
|
model.mongo.createIndex(cache_store, { 'expiresAt': 1 }, { expireAfterSeconds: 0 }, (err, index) => {
|
|
28
|
-
|
|
32
|
+
if (err) {
|
|
33
|
+
log.error("Cache TTL index creation failed: " + err.message);
|
|
34
|
+
} else {
|
|
35
|
+
log.debug("Cache TTL index created: " + index);
|
|
36
|
+
}
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
},
|
package/mw/gotoService/index.js
CHANGED
|
@@ -50,7 +50,7 @@ module.exports = (configuration) => {
|
|
|
50
50
|
//create proxy info object before calling extractbuildparams
|
|
51
51
|
let proxy = false;
|
|
52
52
|
if (serviceInfo[1] === 'proxy' && serviceInfo[2] === 'redirect') {
|
|
53
|
-
req.soajs.log.
|
|
53
|
+
req.soajs.log.info("Route: [/proxy/redirect] is deprecated. You should use [/soajs/proxy].");
|
|
54
54
|
proxy = true;
|
|
55
55
|
} else if (serviceInfo[1] === 'soajs' && serviceInfo[2] === 'proxy') {
|
|
56
56
|
proxy = true;
|
|
@@ -82,7 +82,9 @@ module.exports = (configuration) => {
|
|
|
82
82
|
req.soajs.registry.services[service_n]) {
|
|
83
83
|
req.soajs.log.debug(req.soajs.registry.services[service_n]);
|
|
84
84
|
}
|
|
85
|
-
req.soajs.log.warn(
|
|
85
|
+
if (req.soajs.log.warn()) {
|
|
86
|
+
req.soajs.log.warn(JSON.stringify({ "url": req.url, "headers": req.headers }));
|
|
87
|
+
}
|
|
86
88
|
return req.soajs.controllerResponse(core.error.getError(130));
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -28,14 +28,16 @@ module.exports = (req, res, core, cb) => {
|
|
|
28
28
|
return req.soajs.controllerResponse(core.error.getError(131));
|
|
29
29
|
}
|
|
30
30
|
let nextStep = function (host, port, fullURI) {
|
|
31
|
-
req.soajs.log.info(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
if (req.soajs.log.info()) {
|
|
32
|
+
req.soajs.log.info(JSON.stringify({
|
|
33
|
+
"serviceName": restServiceParams.name,
|
|
34
|
+
"host": host,
|
|
35
|
+
"version": restServiceParams.version,
|
|
36
|
+
"url": restServiceParams.url,
|
|
37
|
+
"port": restServiceParams.registry.port,
|
|
38
|
+
"header": req.headers
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
39
41
|
|
|
40
42
|
let requestTOR = restServiceParams.registry.requestTimeoutRenewal || config.requestTimeoutRenewal;
|
|
41
43
|
let requestTO = restServiceParams.registry.requestTimeout || config.requestTimeout;
|
|
@@ -81,7 +81,8 @@ function flushMonitorBuffer() {
|
|
|
81
81
|
}
|
|
82
82
|
})
|
|
83
83
|
.catch((error) => {
|
|
84
|
-
|
|
84
|
+
//NOTE: the buffer was already cleared, these items are lost
|
|
85
|
+
req.soajs.log.error('Monitor buffer flush failed, ' + itemsToSend.length + ' items lost: ' + error.message);
|
|
85
86
|
});
|
|
86
87
|
});
|
|
87
88
|
}
|
|
@@ -225,7 +226,7 @@ module.exports = (configuration) => {
|
|
|
225
226
|
if (monitor && !monitor_service_blacklist && monitor.req_info) {
|
|
226
227
|
monitoObj.port = req.soajs.controller.serviceParams.registry.port;
|
|
227
228
|
monitoObj.url = req.soajs.controller.serviceParams.url;
|
|
228
|
-
monitoObj.host =
|
|
229
|
+
monitoObj.host = obj.host;
|
|
229
230
|
}
|
|
230
231
|
if (monitor && !monitor_service_blacklist && monitor.req_header) {
|
|
231
232
|
monitoObj.headers = req.headers;
|
package/mw/idempotency/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
12
12
|
const memory = require('./model/memory.js');
|
|
13
13
|
const mongo = require('./model/mongo.js');
|
|
14
|
+
const { logOnce } = require('../../lib/logOnce.js');
|
|
14
15
|
|
|
15
16
|
let model = { memory, mongo };
|
|
16
17
|
|
|
@@ -119,7 +120,8 @@ module.exports = function (configuration) {
|
|
|
119
120
|
|
|
120
121
|
const modelType = idempotencyConfig.model || 'memory';
|
|
121
122
|
if (!model[modelType]) {
|
|
122
|
-
configuration.log
|
|
123
|
+
logOnce(configuration.log, "error", "idempotency_model_" + modelType,
|
|
124
|
+
'Idempotency: Unknown model [' + modelType + ']. It can only be [memory || mongo]. Idempotency is off.');
|
|
123
125
|
return next();
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -22,10 +22,18 @@ let model = {
|
|
|
22
22
|
if (!model.mongo) {
|
|
23
23
|
model.mongo = new Mongo(dbConfiguration);
|
|
24
24
|
model.mongo.createIndex(idempotency_store, { 'l1': 1, 'l2': 1 }, { unique: true }, (err, index) => {
|
|
25
|
-
|
|
25
|
+
if (err) {
|
|
26
|
+
log.error("Idempotency index creation failed: " + err.message);
|
|
27
|
+
} else {
|
|
28
|
+
log.debug("Idempotency index created: " + index);
|
|
29
|
+
}
|
|
26
30
|
});
|
|
27
31
|
model.mongo.createIndex(idempotency_store, { 'expiresAt': 1 }, { expireAfterSeconds: 0 }, (err, index) => {
|
|
28
|
-
|
|
32
|
+
if (err) {
|
|
33
|
+
log.error("Idempotency TTL index creation failed: " + err.message);
|
|
34
|
+
} else {
|
|
35
|
+
log.debug("Idempotency TTL index created: " + index);
|
|
36
|
+
}
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
},
|
package/mw/mt/index.js
CHANGED
|
@@ -330,7 +330,9 @@ module.exports = (configuration) => {
|
|
|
330
330
|
return callback();
|
|
331
331
|
});
|
|
332
332
|
} else {
|
|
333
|
-
|
|
333
|
+
//NOTE: the peer is dropped from the injected address book, the calling
|
|
334
|
+
// service will behave as if it was never configured
|
|
335
|
+
req.soajs.log.error(serviceName + " interConnect failed for [" + item.name + "@" + item.version + "], peer dropped from awareness");
|
|
334
336
|
return callback();
|
|
335
337
|
}
|
|
336
338
|
});
|
package/mw/mt/utils.js
CHANGED
|
@@ -190,8 +190,7 @@ let utils = {
|
|
|
190
190
|
let block = getCachedNetmask(addr);
|
|
191
191
|
return block && block.contains(ip);
|
|
192
192
|
} catch (err) {
|
|
193
|
-
obj.req.soajs.log.error('IP whitelist security configuration failed: '
|
|
194
|
-
obj.req.soajs.log.error(err);
|
|
193
|
+
obj.req.soajs.log.error('IP whitelist security configuration failed for [' + addr + ']: ' + (err.message || err));
|
|
195
194
|
}
|
|
196
195
|
return false;
|
|
197
196
|
}));
|
|
@@ -320,8 +319,7 @@ let utils = {
|
|
|
320
319
|
let block = getCachedNetmask(addr);
|
|
321
320
|
return block && block.contains(ip);
|
|
322
321
|
} catch (err) {
|
|
323
|
-
obj.req.soajs.log.error('Geographic security configuration failed: '
|
|
324
|
-
obj.req.soajs.log.error(err.message);
|
|
322
|
+
obj.req.soajs.log.error('Geographic security configuration failed for [' + addr + ']: ' + (err.message || err));
|
|
325
323
|
}
|
|
326
324
|
return false;
|
|
327
325
|
}));
|
package/mw/traffic/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
|
|
13
13
|
const memory = require('./model/memory.js');
|
|
14
14
|
const mongo = require('./model/mongo.js');
|
|
15
|
+
const { logOnce } = require('../../lib/logOnce.js');
|
|
15
16
|
|
|
16
17
|
let model = {
|
|
17
18
|
memory, mongo
|
|
@@ -175,7 +176,8 @@ module.exports = function (configuration) {
|
|
|
175
176
|
if (trafficModel) {
|
|
176
177
|
throttling.model = trafficModel;
|
|
177
178
|
if (!model[throttling.model]) {
|
|
178
|
-
req.soajs.log
|
|
179
|
+
logOnce(req.soajs.log, "error", "throttling_model_" + throttling.model,
|
|
180
|
+
'Throttling: Unknown model [' + throttling.model + ']. It can only be [memory || mongo]. Throttling is off.');
|
|
179
181
|
return next();
|
|
180
182
|
}
|
|
181
183
|
}
|
|
@@ -23,7 +23,11 @@ let model = {
|
|
|
23
23
|
if (!model.mongo) {
|
|
24
24
|
model.mongo = new Mongo(dbConfiguration);
|
|
25
25
|
model.mongo.createIndex(throttling_monitor, { 'l1': 1, 'l2': 1 }, {}, (err, index) => {
|
|
26
|
-
|
|
26
|
+
if (err) {
|
|
27
|
+
log.error("Throttling index creation failed: " + err.message);
|
|
28
|
+
} else {
|
|
29
|
+
log.debug("Throttling index created: " + index);
|
|
30
|
+
}
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
},
|
package/mw/url/index.js
CHANGED
|
@@ -64,7 +64,9 @@ module.exports = (configuration) => {
|
|
|
64
64
|
!req.soajs.registry.services[req.soajs.controller.serviceParams.service_n]
|
|
65
65
|
)) {
|
|
66
66
|
req.soajs.log.fatal("Service [" + req.soajs.controller.serviceParams.service_n + "] URL [" + req.url + "] couldn't be matched to a service or the service entry in registry is missing [port || hosts]");
|
|
67
|
-
req.soajs.log.warn(
|
|
67
|
+
if (req.soajs.log.warn()) {
|
|
68
|
+
req.soajs.log.warn(JSON.stringify({ "url": req.url, "headers": req.headers }));
|
|
69
|
+
}
|
|
68
70
|
return req.soajs.controllerResponse(core.error.getError(130));
|
|
69
71
|
}
|
|
70
72
|
|
package/package.json
CHANGED
package/server/controller.js
CHANGED
|
@@ -74,7 +74,7 @@ Controller.prototype.init = function (callback) {
|
|
|
74
74
|
let log = core.getLogger(_self.soajs.param.init.serviceName, registry.serviceConfig.logger);
|
|
75
75
|
if (service.fetched) {
|
|
76
76
|
if (!service.fetched.result) {
|
|
77
|
-
log.
|
|
77
|
+
log.error("Unable to find the service host ip. The service will NOT be registered for awareness.");
|
|
78
78
|
log.info("IPs found: ", service.fetched.extra.ips);
|
|
79
79
|
log.warn("The default service IP has been used [" + service.ip + "]");
|
|
80
80
|
} else {
|
|
@@ -316,7 +316,7 @@ Controller.prototype.start = function (registry, log, service, server, serverMai
|
|
|
316
316
|
});
|
|
317
317
|
});
|
|
318
318
|
} else {
|
|
319
|
-
log.
|
|
319
|
+
log.error("Unable to register host IP [" + service.ip + "] for service [" + _self.soajs.param.init.serviceName + "@" + _self.soajs.param.init.serviceVersion + "]");
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
322
|
}
|
package/server/maintenance.js
CHANGED
|
@@ -145,7 +145,7 @@ let reloadRegistry = (parsedUrl, core, log, param, serviceIp, cb) => {
|
|
|
145
145
|
}, function (err, reg) {
|
|
146
146
|
let response = maintenanceResponse(parsedUrl, param);
|
|
147
147
|
if (err) {
|
|
148
|
-
log.
|
|
148
|
+
log.error("Failed to load registry. reusing from previous load. Reason: " + err.message);
|
|
149
149
|
} else {
|
|
150
150
|
response.result = true;
|
|
151
151
|
response.data = cloneAndFilterRegistry(reg);
|
|
@@ -236,7 +236,7 @@ let Maintenance = (core, log, param, serviceIp, regEnvironment, awareness_mw, so
|
|
|
236
236
|
infoObj = body;
|
|
237
237
|
}
|
|
238
238
|
if (!soajsLib.version.validate(infoObj.version)) {
|
|
239
|
-
log.
|
|
239
|
+
log.error("Failed to register service for [" + infoObj.name + "@" + infoObj.version + "] version should be of format [1.1]");
|
|
240
240
|
// res.writeHead(200, {'Content-Type': 'application/json'});
|
|
241
241
|
return res.end(JSON.stringify(response));
|
|
242
242
|
}
|
|
@@ -287,7 +287,7 @@ let Maintenance = (core, log, param, serviceIp, regEnvironment, awareness_mw, so
|
|
|
287
287
|
response.result = true;
|
|
288
288
|
response.data = data;
|
|
289
289
|
} else {
|
|
290
|
-
log.
|
|
290
|
+
log.error("Failed to register service for [" + infoObj.name + "] " + err.message);
|
|
291
291
|
}
|
|
292
292
|
return res.end(JSON.stringify(response));
|
|
293
293
|
});
|
package/test/unit/index.js
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright SOAJS All Rights Reserved.
|
|
6
|
+
*
|
|
7
|
+
* Use of this source code is governed by an Apache license that can be
|
|
8
|
+
* found in the LICENSE file at the root of this repository
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const assert = require('assert');
|
|
12
|
+
const helper = require("../../helper.js");
|
|
13
|
+
|
|
14
|
+
let logOnceLib = helper.requireModule('./lib/logOnce.js');
|
|
15
|
+
const { logOnce, resetLogOnce } = logOnceLib;
|
|
16
|
+
|
|
17
|
+
describe("Unit test for: lib - logOnce", () => {
|
|
18
|
+
|
|
19
|
+
let emitted = [];
|
|
20
|
+
let log = {
|
|
21
|
+
error: (input) => {
|
|
22
|
+
emitted.push({ "level": "error", "input": input });
|
|
23
|
+
},
|
|
24
|
+
warn: (input) => {
|
|
25
|
+
emitted.push({ "level": "warn", "input": input });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
emitted = [];
|
|
31
|
+
resetLogOnce();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("emits the first time", (done) => {
|
|
35
|
+
let did = logOnce(log, "error", "k1", "boom");
|
|
36
|
+
assert.strictEqual(did, true);
|
|
37
|
+
assert.strictEqual(emitted.length, 1);
|
|
38
|
+
assert.strictEqual(emitted[0].level, "error");
|
|
39
|
+
assert.strictEqual(emitted[0].input, "boom");
|
|
40
|
+
done();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("drops repeats of the same key", (done) => {
|
|
44
|
+
for (let i = 0; i < 100; i++) {
|
|
45
|
+
logOnce(log, "error", "k1", "boom");
|
|
46
|
+
}
|
|
47
|
+
assert.strictEqual(emitted.length, 1);
|
|
48
|
+
done();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("keeps distinct keys separate", (done) => {
|
|
52
|
+
logOnce(log, "error", "k1", "one");
|
|
53
|
+
logOnce(log, "error", "k2", "two");
|
|
54
|
+
logOnce(log, "error", "k1", "one again");
|
|
55
|
+
assert.strictEqual(emitted.length, 2);
|
|
56
|
+
assert.strictEqual(emitted[0].input, "one");
|
|
57
|
+
assert.strictEqual(emitted[1].input, "two");
|
|
58
|
+
done();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("honours the requested level", (done) => {
|
|
62
|
+
logOnce(log, "warn", "k3", "careful");
|
|
63
|
+
assert.strictEqual(emitted[0].level, "warn");
|
|
64
|
+
done();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does not throw on a missing logger or unknown level", (done) => {
|
|
68
|
+
assert.strictEqual(logOnce(null, "error", "k4", "x"), false);
|
|
69
|
+
assert.strictEqual(logOnce(log, "nosuchlevel", "k5", "x"), false);
|
|
70
|
+
assert.strictEqual(emitted.length, 0);
|
|
71
|
+
done();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not consume the key when it could not emit", (done) => {
|
|
75
|
+
logOnce(null, "error", "k6", "x");
|
|
76
|
+
let did = logOnce(log, "error", "k6", "now it works");
|
|
77
|
+
assert.strictEqual(did, true);
|
|
78
|
+
assert.strictEqual(emitted.length, 1);
|
|
79
|
+
done();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,10 +1,82 @@
|
|
|
1
1
|
"use strict"
|
|
2
|
+
const assert = require('assert');
|
|
2
3
|
const helper = require("../../helper.js");
|
|
3
4
|
|
|
4
5
|
let utils = helper.requireModule('./utilities/utils.js');
|
|
5
6
|
|
|
6
7
|
describe("Testing utilities", () => {
|
|
7
8
|
|
|
9
|
+
const TOKEN = "e2d0ffb0f0e34bd6a8e0a0b4a1d1b6c1f2a3b4c5";
|
|
10
|
+
const EKEY = "aa39b5490c4a4ed0e56d7ec1232a428f771e8bb83cfcee16de14f735d0f5da58";
|
|
11
|
+
const IKEY = "38145c67717c73d3febd16df38abf311";
|
|
12
|
+
|
|
13
|
+
let logged = {
|
|
14
|
+
"error": [],
|
|
15
|
+
"warn": []
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* bunyan semantics: log.<level>() with no argument returns whether that level is
|
|
20
|
+
* enabled, log.<level>(input) writes the record. The gateway relies on the first
|
|
21
|
+
* form to skip building log payloads that would be discarded.
|
|
22
|
+
*/
|
|
23
|
+
let makeLog = (enabled) => {
|
|
24
|
+
return {
|
|
25
|
+
error: (input) => {
|
|
26
|
+
if (input === undefined) {
|
|
27
|
+
return enabled;
|
|
28
|
+
}
|
|
29
|
+
logged.error.push(input);
|
|
30
|
+
},
|
|
31
|
+
warn: (input) => {
|
|
32
|
+
if (input === undefined) {
|
|
33
|
+
return enabled;
|
|
34
|
+
}
|
|
35
|
+
logged.warn.push(input);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
let makeReq = (enabled) => {
|
|
41
|
+
return {
|
|
42
|
+
method: "GET",
|
|
43
|
+
url: "/urac/user/getprofile?access_token=" + TOKEN + "&key=" + EKEY + "&id=42",
|
|
44
|
+
headers: {
|
|
45
|
+
"access_token": TOKEN,
|
|
46
|
+
"key": EKEY,
|
|
47
|
+
"authorization": "Bearer " + TOKEN
|
|
48
|
+
},
|
|
49
|
+
getClientIP: () => {
|
|
50
|
+
return "196.218.44.17";
|
|
51
|
+
},
|
|
52
|
+
getClientUserAgent: () => {
|
|
53
|
+
return "mocha-agent";
|
|
54
|
+
},
|
|
55
|
+
soajs: {
|
|
56
|
+
log: makeLog(enabled),
|
|
57
|
+
controller: {
|
|
58
|
+
serviceParams: {
|
|
59
|
+
name: "urac",
|
|
60
|
+
version: "3",
|
|
61
|
+
parsedUrl: { pathname: "/urac/user/getprofile" }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
tenant: {
|
|
65
|
+
id: "5c0e74ba9acc3c5a84a51259",
|
|
66
|
+
code: "TSTM",
|
|
67
|
+
key: { iKey: IKEY, eKey: EKEY },
|
|
68
|
+
application: { product: "TPROD", package: "TPROD_BASIC" }
|
|
69
|
+
},
|
|
70
|
+
buildResponse: (input) => {
|
|
71
|
+
return input;
|
|
72
|
+
},
|
|
73
|
+
controllerResponse: (input) => {
|
|
74
|
+
return input;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
8
80
|
let res = {
|
|
9
81
|
status: () => {
|
|
10
82
|
return {
|
|
@@ -18,24 +90,12 @@ describe("Testing utilities", () => {
|
|
|
18
90
|
}
|
|
19
91
|
};
|
|
20
92
|
|
|
21
|
-
let req =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
warn: (input) => {
|
|
28
|
-
// console.error(input);
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
buildResponse: (input) => {
|
|
32
|
-
return input;
|
|
33
|
-
},
|
|
34
|
-
controllerResponse: (input) => {
|
|
35
|
-
return input;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
93
|
+
let req = makeReq(true);
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
logged.error = [];
|
|
97
|
+
logged.warn = [];
|
|
98
|
+
});
|
|
39
99
|
|
|
40
100
|
it("logErrors - number error", (done) => {
|
|
41
101
|
utils.logErrors(123, req, res, (error) => {
|
|
@@ -49,6 +109,43 @@ describe("Testing utilities", () => {
|
|
|
49
109
|
});
|
|
50
110
|
});
|
|
51
111
|
|
|
112
|
+
it("logErrors - no longer duplicates what the error context reports", (done) => {
|
|
113
|
+
// the full chain: logErrors -> controllerClientErrorHandler -> controllerErrorHandler
|
|
114
|
+
let r = makeReq(true);
|
|
115
|
+
let oauthErr = new Error("The access token provided is invalid.");
|
|
116
|
+
oauthErr.name = "OAuth2Error";
|
|
117
|
+
oauthErr.code = 401;
|
|
118
|
+
|
|
119
|
+
utils.logErrors(oauthErr, r, res, (e1) => {
|
|
120
|
+
utils.controllerClientErrorHandler(e1, r, res, (e2) => {
|
|
121
|
+
utils.controllerErrorHandler(e2, r, res, null);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(logged.error.length, 1, "expected exactly one error line for one failure");
|
|
126
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
127
|
+
assert.strictEqual(ctx.code, 401);
|
|
128
|
+
assert.strictEqual(ctx.msg, "The access token provided is invalid.");
|
|
129
|
+
done();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("logErrors - keeps the underlying cause when it becomes a generic 164", (done) => {
|
|
133
|
+
// an error with neither code nor msg is replaced by 164, so the context line
|
|
134
|
+
// cannot report the original. logErrors must still record it.
|
|
135
|
+
let r = makeReq(true);
|
|
136
|
+
utils.logErrors({ "message": "socket hang up" }, r, res, (e1) => {
|
|
137
|
+
utils.controllerClientErrorHandler(e1, r, res, (e2) => {
|
|
138
|
+
utils.controllerErrorHandler(e2, r, res, null);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
assert.strictEqual(logged.error.length, 2);
|
|
143
|
+
assert.strictEqual(logged.error[0], "socket hang up", "underlying cause must survive");
|
|
144
|
+
let ctx = JSON.parse(logged.error[1]);
|
|
145
|
+
assert.strictEqual(ctx.code, 164);
|
|
146
|
+
done();
|
|
147
|
+
});
|
|
148
|
+
|
|
52
149
|
it("logErrors - string error", (done) => {
|
|
53
150
|
utils.logErrors("error", req, res, (error) => {
|
|
54
151
|
done();
|
|
@@ -76,4 +173,94 @@ describe("Testing utilities", () => {
|
|
|
76
173
|
utils.controllerErrorHandler({ "code": 200, "msg": "dummy200" }, req, res, null);
|
|
77
174
|
done();
|
|
78
175
|
});
|
|
79
|
-
|
|
176
|
+
|
|
177
|
+
it("controllerErrorHandler - emits the redacted context at error level", (done) => {
|
|
178
|
+
let r = makeReq(true);
|
|
179
|
+
utils.controllerErrorHandler({ "code": 401, "msg": "The access token provided is invalid." }, r, res, null);
|
|
180
|
+
|
|
181
|
+
assert.strictEqual(logged.error.length, 1);
|
|
182
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
183
|
+
assert.strictEqual(ctx.code, 401);
|
|
184
|
+
assert.strictEqual(ctx.msg, "The access token provided is invalid.");
|
|
185
|
+
assert.strictEqual(ctx.method, "GET");
|
|
186
|
+
assert.strictEqual(ctx.service, "urac");
|
|
187
|
+
assert.strictEqual(ctx.version, "3");
|
|
188
|
+
assert.strictEqual(ctx.api, "/urac/user/getprofile");
|
|
189
|
+
assert.strictEqual(ctx.ip, "196.218.44.17");
|
|
190
|
+
assert.strictEqual(ctx.ua, "mocha-agent");
|
|
191
|
+
assert.deepStrictEqual(ctx.tenant, { "id": "5c0e74ba9acc3c5a84a51259", "code": "TSTM" });
|
|
192
|
+
assert.strictEqual(ctx.iKey, IKEY);
|
|
193
|
+
assert.deepStrictEqual(ctx.application, { "product": "TPROD", "package": "TPROD_BASIC" });
|
|
194
|
+
done();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("controllerErrorHandler - masks credentials and never logs headers", (done) => {
|
|
198
|
+
let r = makeReq(true);
|
|
199
|
+
utils.controllerErrorHandler({ "code": 401, "msg": "bad token" }, r, res, null);
|
|
200
|
+
|
|
201
|
+
let line = logged.error[0];
|
|
202
|
+
assert.strictEqual(line.indexOf(TOKEN), -1, "access_token leaked into the error context");
|
|
203
|
+
assert.strictEqual(line.indexOf(EKEY), -1, "eKey leaked into the error context");
|
|
204
|
+
assert.strictEqual(line.indexOf("authorization"), -1, "headers leaked into the error context");
|
|
205
|
+
|
|
206
|
+
let ctx = JSON.parse(line);
|
|
207
|
+
assert.strictEqual(ctx.url, "/urac/user/getprofile?access_token=***&key=***&id=42");
|
|
208
|
+
done();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("controllerErrorHandler - numeric error resolves code and message", (done) => {
|
|
212
|
+
let r = makeReq(true);
|
|
213
|
+
utils.controllerErrorHandler(143, r, res, null);
|
|
214
|
+
|
|
215
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
216
|
+
assert.strictEqual(ctx.code, 143);
|
|
217
|
+
assert.strictEqual(ctx.msg, "invalid_request: Malformed auth header");
|
|
218
|
+
done();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("controllerErrorHandler - includes the resolved username when present", (done) => {
|
|
222
|
+
let r = makeReq(true);
|
|
223
|
+
r.soajs.uracDriver = { "username": "owner" };
|
|
224
|
+
utils.controllerErrorHandler({ "code": 401, "msg": "bad token" }, r, res, null);
|
|
225
|
+
|
|
226
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
227
|
+
assert.strictEqual(ctx.username, "owner");
|
|
228
|
+
done();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("controllerErrorHandler - degraded request without tenant or serviceParams", (done) => {
|
|
232
|
+
let r = makeReq(true);
|
|
233
|
+
r.soajs.controller = {};
|
|
234
|
+
delete r.soajs.tenant;
|
|
235
|
+
utils.controllerErrorHandler(130, r, res, null);
|
|
236
|
+
|
|
237
|
+
let ctx = JSON.parse(logged.error[0]);
|
|
238
|
+
assert.strictEqual(ctx.service, null);
|
|
239
|
+
assert.strictEqual(ctx.version, null);
|
|
240
|
+
assert.strictEqual(ctx.api, null);
|
|
241
|
+
assert.strictEqual(ctx.tenant, undefined);
|
|
242
|
+
assert.strictEqual(ctx.iKey, undefined);
|
|
243
|
+
assert.strictEqual(ctx.application, undefined);
|
|
244
|
+
done();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("controllerErrorHandler - skips both payloads when the levels are off", (done) => {
|
|
248
|
+
let r = makeReq(false);
|
|
249
|
+
utils.controllerErrorHandler({ "code": 401, "msg": "bad token" }, r, res, null);
|
|
250
|
+
|
|
251
|
+
assert.strictEqual(logged.error.length, 0);
|
|
252
|
+
assert.strictEqual(logged.warn.length, 0);
|
|
253
|
+
done();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("controllerErrorHandler - the warn dump still carries the raw headers", (done) => {
|
|
257
|
+
let r = makeReq(true);
|
|
258
|
+
utils.controllerErrorHandler({ "code": 401, "msg": "bad token" }, r, res, null);
|
|
259
|
+
|
|
260
|
+
assert.strictEqual(logged.warn.length, 1);
|
|
261
|
+
let dump = JSON.parse(logged.warn[0]);
|
|
262
|
+
assert.strictEqual(dump.headers.access_token, TOKEN);
|
|
263
|
+
assert.strictEqual(dump.url, r.url);
|
|
264
|
+
done();
|
|
265
|
+
});
|
|
266
|
+
});
|
package/utilities/utils.js
CHANGED
|
@@ -11,6 +11,72 @@
|
|
|
11
11
|
const coreModules = require("soajs.core.modules");
|
|
12
12
|
let core = coreModules.core;
|
|
13
13
|
|
|
14
|
+
//NOTE: credentials that must never reach the logs, they are masked in the query string
|
|
15
|
+
const SENSITIVE_QS = /([?&](?:access_token|refresh_token|key)=)[^&]*/gi;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mask credentials in a url query string
|
|
19
|
+
*
|
|
20
|
+
* @param url
|
|
21
|
+
* @returns {string|null}
|
|
22
|
+
*/
|
|
23
|
+
function redactUrl(url) {
|
|
24
|
+
return url ? url.replace(SENSITIVE_QS, '$1***') : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a compact and redacted context for an error response.
|
|
29
|
+
* NOTE: headers are never included, they carry access_token, key, authorization and soajsinjectobj.
|
|
30
|
+
*
|
|
31
|
+
* @param err
|
|
32
|
+
* @param req
|
|
33
|
+
* @returns {Object}
|
|
34
|
+
*/
|
|
35
|
+
function errorContext(err, req) {
|
|
36
|
+
let soajs = req.soajs || {};
|
|
37
|
+
let serviceParams = (soajs.controller && soajs.controller.serviceParams) || {};
|
|
38
|
+
let tenant = soajs.tenant || {};
|
|
39
|
+
|
|
40
|
+
let code = null;
|
|
41
|
+
let msg = null;
|
|
42
|
+
if (typeof err === "number") {
|
|
43
|
+
code = err;
|
|
44
|
+
msg = core.error.generate(err).message;
|
|
45
|
+
} else if (err && typeof err === "object") {
|
|
46
|
+
code = err.code || err.status || null;
|
|
47
|
+
msg = err.msg || err.message || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let context = {
|
|
51
|
+
"code": code,
|
|
52
|
+
"msg": msg,
|
|
53
|
+
"method": req.method,
|
|
54
|
+
"service": serviceParams.name || serviceParams.service_n || null,
|
|
55
|
+
"version": serviceParams.version || null,
|
|
56
|
+
"api": (serviceParams.parsedUrl && serviceParams.parsedUrl.pathname) || null,
|
|
57
|
+
"url": redactUrl(req.url),
|
|
58
|
+
"ip": req.getClientIP ? req.getClientIP() : null,
|
|
59
|
+
"ua": req.getClientUserAgent ? req.getClientUserAgent() : null
|
|
60
|
+
};
|
|
61
|
+
if (tenant.id) {
|
|
62
|
+
context.tenant = { "id": tenant.id, "code": tenant.code };
|
|
63
|
+
}
|
|
64
|
+
//NOTE: iKey only, the eKey is the credential the client authenticates with
|
|
65
|
+
if (tenant.key && tenant.key.iKey) {
|
|
66
|
+
context.iKey = tenant.key.iKey;
|
|
67
|
+
}
|
|
68
|
+
if (tenant.application) {
|
|
69
|
+
context.application = {
|
|
70
|
+
"product": tenant.application.product,
|
|
71
|
+
"package": tenant.application.package
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (soajs.uracDriver && soajs.uracDriver.username) {
|
|
75
|
+
context.username = soajs.uracDriver.username;
|
|
76
|
+
}
|
|
77
|
+
return context;
|
|
78
|
+
}
|
|
79
|
+
|
|
14
80
|
//-------------------------- ERROR Handling MW - service & controller
|
|
15
81
|
/**
|
|
16
82
|
*
|
|
@@ -20,13 +86,15 @@ let core = coreModules.core;
|
|
|
20
86
|
* @param next
|
|
21
87
|
*/
|
|
22
88
|
function logErrors(err, req, res, next) {
|
|
89
|
+
//NOTE: every error routed here reaches controllerErrorHandler, which logs a
|
|
90
|
+
// structured context carrying the code and the message. The cases below
|
|
91
|
+
// only log what that context cannot reproduce, which is the underlying
|
|
92
|
+
// cause of an error that gets replaced by the generic 164.
|
|
23
93
|
if (typeof err === "number") {
|
|
24
|
-
req.soajs.log.error(core.error.generate(err).message);
|
|
25
94
|
return next(err);
|
|
26
95
|
}
|
|
27
96
|
if (typeof err === "object") {
|
|
28
97
|
if (err.code && err.message) {
|
|
29
|
-
req.soajs.log.error(err.message);
|
|
30
98
|
if (err.name === "OAuth2Error") {
|
|
31
99
|
return next({ "code": err.code, "status": err.code, "msg": err.message });
|
|
32
100
|
} else {
|
|
@@ -34,15 +102,12 @@ function logErrors(err, req, res, next) {
|
|
|
34
102
|
}
|
|
35
103
|
} else if (err.code && err.msg) {
|
|
36
104
|
err.message = err.msg;
|
|
37
|
-
req.soajs.log.error(err.message);
|
|
38
105
|
return next(err);
|
|
39
106
|
} else {
|
|
40
107
|
req.soajs.log.error(err.message || err);
|
|
41
|
-
req.soajs.log.error(core.error.generate(164).message);
|
|
42
108
|
}
|
|
43
109
|
} else {
|
|
44
110
|
req.soajs.log.error(err);
|
|
45
|
-
req.soajs.log.error(core.error.generate(164).message);
|
|
46
111
|
}
|
|
47
112
|
|
|
48
113
|
return next(core.error.getError(164));
|
|
@@ -77,7 +142,13 @@ function controllerClientErrorHandler(err, req, res, next) {
|
|
|
77
142
|
*/
|
|
78
143
|
function controllerErrorHandler(err, req, res, next) {
|
|
79
144
|
|
|
80
|
-
req.soajs.log.
|
|
145
|
+
if (req.soajs.log.error()) {
|
|
146
|
+
req.soajs.log.error(JSON.stringify(errorContext(err, req)));
|
|
147
|
+
}
|
|
148
|
+
//NOTE: the full header dump stays at warn, it is a debugging aid and it is not redacted
|
|
149
|
+
if (req.soajs.log.warn()) {
|
|
150
|
+
req.soajs.log.warn(JSON.stringify({ "url": req.url, "headers": req.headers }));
|
|
151
|
+
}
|
|
81
152
|
|
|
82
153
|
if (err.code && err.msg) {
|
|
83
154
|
err.status = err.status || 500;
|