@mojaloop/sdk-scheme-adapter 12.2.3 → 12.3.0
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/CHANGELOG.md +8 -0
- package/audit-resolve.json +71 -1
- package/package.json +3 -1
- package/src/OutboundServer/handlers.js +3 -0
- package/src/OutboundServer/index.js +6 -4
- package/src/index.js +198 -8
- package/src/lib/cache.js +110 -52
- package/src/lib/metrics.js +148 -0
- package/src/lib/model/OutboundTransfersModel.js +57 -0
- package/test/__mocks__/redis.js +4 -0
- package/test/unit/api/utils.js +4 -1
- package/test/unit/index.test.js +95 -3
- package/test/unit/lib/model/OutboundTransfersModel.test.js +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# Changelog: [mojaloop/thirdparty-api-svc](https://github.com/mojaloop/thirdparty-api-svc)
|
|
2
|
+
## [12.3.0](https://github.com/mojaloop/sdk-scheme-adapter/compare/v12.2.3...v12.3.0) (2022-05-04)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* port over prom client metrics ([#312](https://github.com/mojaloop/sdk-scheme-adapter/issues/312)) ([8de66d5](https://github.com/mojaloop/sdk-scheme-adapter/commit/8de66d505b94cddb5e3b8e857ae491f85058d395))
|
|
8
|
+
* pull in live reconfiguration logic ([#313](https://github.com/mojaloop/sdk-scheme-adapter/issues/313)) ([ae5648a](https://github.com/mojaloop/sdk-scheme-adapter/commit/ae5648a500eaab80804db0298facc1e352482fb9))
|
|
9
|
+
|
|
2
10
|
### [12.2.3](https://github.com/mojaloop/sdk-scheme-adapter/compare/v12.2.2...v12.2.3) (2022-04-26)
|
|
3
11
|
|
|
4
12
|
|
package/audit-resolve.json
CHANGED
|
@@ -434,8 +434,78 @@
|
|
|
434
434
|
"decision": "ignore",
|
|
435
435
|
"madeAt": 1650459475376,
|
|
436
436
|
"expiresAt": 1653051469252
|
|
437
|
+
},
|
|
438
|
+
"1070245|@mojaloop/central-services-shared>@mojaloop/event-sdk>moment": {
|
|
439
|
+
"decision": "ignore",
|
|
440
|
+
"madeAt": 1651072086111,
|
|
441
|
+
"expiresAt": 1653664079920
|
|
442
|
+
},
|
|
443
|
+
"1070245|@mojaloop/event-sdk>moment": {
|
|
444
|
+
"decision": "ignore",
|
|
445
|
+
"madeAt": 1651072086111,
|
|
446
|
+
"expiresAt": 1653664079920
|
|
447
|
+
},
|
|
448
|
+
"1070030|@mojaloop/central-services-shared>@mojaloop/event-sdk>moment>shins>markdown-it": {
|
|
449
|
+
"decision": "ignore",
|
|
450
|
+
"madeAt": 1651072088351,
|
|
451
|
+
"expiresAt": 1653664079920
|
|
452
|
+
},
|
|
453
|
+
"1070030|widdershins>markdown-it": {
|
|
454
|
+
"decision": "ignore",
|
|
455
|
+
"madeAt": 1651072088351,
|
|
456
|
+
"expiresAt": 1653664079920
|
|
457
|
+
},
|
|
458
|
+
"1068154|@mojaloop/central-services-shared>@mojaloop/event-sdk>moment>shins>markdown-it>sanitize-html": {
|
|
459
|
+
"decision": "ignore",
|
|
460
|
+
"madeAt": 1651072089481,
|
|
461
|
+
"expiresAt": 1653664079920
|
|
462
|
+
},
|
|
463
|
+
"1068155|@mojaloop/central-services-shared>@mojaloop/event-sdk>moment>shins>markdown-it>sanitize-html": {
|
|
464
|
+
"decision": "ignore",
|
|
465
|
+
"madeAt": 1651072090420,
|
|
466
|
+
"expiresAt": 1653664079920
|
|
467
|
+
},
|
|
468
|
+
"1070250|ansi-regex": {
|
|
469
|
+
"decision": "ignore",
|
|
470
|
+
"madeAt": 1651072091389,
|
|
471
|
+
"expiresAt": 1653664079920
|
|
472
|
+
},
|
|
473
|
+
"1070252|ansi-regex": {
|
|
474
|
+
"decision": "ignore",
|
|
475
|
+
"madeAt": 1651072092335,
|
|
476
|
+
"expiresAt": 1653664079920
|
|
477
|
+
},
|
|
478
|
+
"1070256|ejs": {
|
|
479
|
+
"decision": "ignore",
|
|
480
|
+
"madeAt": 1651072093304,
|
|
481
|
+
"expiresAt": 1653664079920
|
|
482
|
+
},
|
|
483
|
+
"1067536|json-pointer": {
|
|
484
|
+
"decision": "ignore",
|
|
485
|
+
"madeAt": 1651072094695,
|
|
486
|
+
"expiresAt": 1653664079920
|
|
487
|
+
},
|
|
488
|
+
"1067553|swagger2openapi>better-ajv-errors>jsonpointer": {
|
|
489
|
+
"decision": "ignore",
|
|
490
|
+
"madeAt": 1651072095680,
|
|
491
|
+
"expiresAt": 1653664079920
|
|
492
|
+
},
|
|
493
|
+
"1067946|swagger2openapi>better-ajv-errors>jsonpointer>oas-validator>ajv": {
|
|
494
|
+
"decision": "ignore",
|
|
495
|
+
"madeAt": 1651072096735,
|
|
496
|
+
"expiresAt": 1653664079920
|
|
497
|
+
},
|
|
498
|
+
"1068310|widdershins>markdown-it>yargs>yargs-parser": {
|
|
499
|
+
"decision": "ignore",
|
|
500
|
+
"madeAt": 1651072098082,
|
|
501
|
+
"expiresAt": 1653664079920
|
|
502
|
+
},
|
|
503
|
+
"1070260|@mojaloop/central-services-shared>shins>sanitize-html": {
|
|
504
|
+
"decision": "ignore",
|
|
505
|
+
"madeAt": 1651249549067,
|
|
506
|
+
"expiresAt": 1653841542147
|
|
437
507
|
}
|
|
438
508
|
},
|
|
439
509
|
"rules": {},
|
|
440
510
|
"version": 1
|
|
441
|
-
}
|
|
511
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/sdk-scheme-adapter",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.3.0",
|
|
4
4
|
"description": "An adapter for connecting to Mojaloop API enabled switches.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -74,9 +74,11 @@
|
|
|
74
74
|
"json-schema-ref-parser": "^9.0.9",
|
|
75
75
|
"koa": "^2.13.1",
|
|
76
76
|
"koa-body": "^4.2.0",
|
|
77
|
+
"lodash": "^4.17.21",
|
|
77
78
|
"module-alias": "^2.2.2",
|
|
78
79
|
"oauth2-server": "^4.0.0-dev.2",
|
|
79
80
|
"openapi-jsonschema-parameters": "^9.3.0",
|
|
81
|
+
"prom-client": "^12.0.0",
|
|
80
82
|
"promise-timeout": "^1.3.0",
|
|
81
83
|
"random-word-slugs": "^0.1.6",
|
|
82
84
|
"redis": "^3.1.2",
|
|
@@ -112,6 +112,7 @@ const postTransfers = async (ctx) => {
|
|
|
112
112
|
cache: ctx.state.cache,
|
|
113
113
|
logger: ctx.state.logger,
|
|
114
114
|
wso2: ctx.state.wso2,
|
|
115
|
+
metricsClient: ctx.state.metricsClient,
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
// initialize the transfer model and start it running
|
|
@@ -144,6 +145,7 @@ const getTransfers = async (ctx) => {
|
|
|
144
145
|
cache: ctx.state.cache,
|
|
145
146
|
logger: ctx.state.logger,
|
|
146
147
|
wso2: ctx.state.wso2,
|
|
148
|
+
metricsClient: ctx.state.metricsClient,
|
|
147
149
|
});
|
|
148
150
|
|
|
149
151
|
// initialize the transfer model and start it running
|
|
@@ -172,6 +174,7 @@ const putTransfers = async (ctx) => {
|
|
|
172
174
|
cache: ctx.state.cache,
|
|
173
175
|
logger: ctx.state.logger,
|
|
174
176
|
wso2: ctx.state.wso2,
|
|
177
|
+
metricsClient: ctx.state.metricsClient,
|
|
175
178
|
});
|
|
176
179
|
|
|
177
180
|
// TODO: check the incoming body to reject party or quote when requested to do so
|
|
@@ -28,12 +28,13 @@ const middlewares = require('./middlewares');
|
|
|
28
28
|
const endpointRegex = /\/.*/g;
|
|
29
29
|
|
|
30
30
|
class OutboundApi extends EventEmitter {
|
|
31
|
-
constructor(conf, logger, cache, validator) {
|
|
31
|
+
constructor(conf, logger, cache, validator, metricsClient) {
|
|
32
32
|
super({ captureExceptions: true });
|
|
33
33
|
this._logger = logger;
|
|
34
34
|
this._api = new Koa();
|
|
35
35
|
this._conf = conf;
|
|
36
36
|
this._cache = cache;
|
|
37
|
+
this._metricsClient = metricsClient;
|
|
37
38
|
|
|
38
39
|
this._wso2 = {
|
|
39
40
|
auth: new WSO2Auth({
|
|
@@ -54,7 +55,7 @@ class OutboundApi extends EventEmitter {
|
|
|
54
55
|
this._api.use(middlewares.createErrorHandler(this._logger));
|
|
55
56
|
this._api.use(middlewares.createRequestIdGenerator());
|
|
56
57
|
this._api.use(koaBody()); // outbound always expects application/json
|
|
57
|
-
this._api.use(middlewares.applyState({ cache, wso2: this._wso2, conf }));
|
|
58
|
+
this._api.use(middlewares.applyState({ cache, wso2: this._wso2, conf, metricsClient }));
|
|
58
59
|
this._api.use(middlewares.createLogger(this._logger));
|
|
59
60
|
|
|
60
61
|
//Note that we strip off any path on peerEndpoint config after the origin.
|
|
@@ -91,7 +92,7 @@ class OutboundApi extends EventEmitter {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
class OutboundServer extends EventEmitter {
|
|
94
|
-
constructor(conf, logger, cache) {
|
|
95
|
+
constructor(conf, logger, cache, metricsClient) {
|
|
95
96
|
super({ captureExceptions: true });
|
|
96
97
|
this._validator = new Validate();
|
|
97
98
|
this._conf = conf;
|
|
@@ -101,7 +102,8 @@ class OutboundServer extends EventEmitter {
|
|
|
101
102
|
conf,
|
|
102
103
|
this._logger.push({ component: 'api' }),
|
|
103
104
|
cache,
|
|
104
|
-
this._validator
|
|
105
|
+
this._validator,
|
|
106
|
+
metricsClient
|
|
105
107
|
);
|
|
106
108
|
this._api.on('error', (...args) => {
|
|
107
109
|
this.emit('error', ...args);
|
package/src/index.js
CHANGED
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
+
const assert = require('assert/strict');
|
|
13
14
|
const { hostname } = require('os');
|
|
14
15
|
const config = require('./config');
|
|
15
16
|
const EventEmitter = require('events');
|
|
17
|
+
const _ = require('lodash');
|
|
16
18
|
|
|
17
19
|
const InboundServer = require('./InboundServer');
|
|
18
20
|
const OutboundServer = require('./OutboundServer');
|
|
19
21
|
const OAuthTestServer = require('./OAuthTestServer');
|
|
20
22
|
const TestServer = require('./TestServer');
|
|
23
|
+
const { MetricsServer, MetricsClient } = require('./lib/metrics');
|
|
24
|
+
const ControlAgent = require('./ControlAgent');
|
|
21
25
|
|
|
22
26
|
// import things we want to expose e.g. for unit tests and users who dont want to use the entire
|
|
23
27
|
// scheme adapter as a service
|
|
@@ -26,8 +30,19 @@ const OutboundServerMiddleware = require('./OutboundServer/middlewares.js');
|
|
|
26
30
|
const Router = require('./lib/router');
|
|
27
31
|
const Validate = require('./lib/validate');
|
|
28
32
|
const Cache = require('./lib/cache');
|
|
33
|
+
const check = require('./lib/check');
|
|
29
34
|
const { Logger } = require('@mojaloop/sdk-standard-components');
|
|
30
35
|
|
|
36
|
+
const LOG_ID = {
|
|
37
|
+
INBOUND: { app: 'mojaloop-connector-inbound-api' },
|
|
38
|
+
OUTBOUND: { app: 'mojaloop-connector-outbound-api' },
|
|
39
|
+
TEST: { app: 'mojaloop-connector-test-api' },
|
|
40
|
+
OAUTHTEST: { app: 'mojaloop-connector-oauth-test-server' },
|
|
41
|
+
CONTROL: { app: 'mojaloop-connector-control-client' },
|
|
42
|
+
METRICS: { app: 'mojaloop-connector-metrics' },
|
|
43
|
+
CACHE: { component: 'cache' },
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* Class that creates and manages http servers that expose the scheme adapter APIs.
|
|
33
48
|
*/
|
|
@@ -38,14 +53,22 @@ class Server extends EventEmitter {
|
|
|
38
53
|
this.logger = logger;
|
|
39
54
|
this.cache = new Cache({
|
|
40
55
|
...conf.cacheConfig,
|
|
41
|
-
logger: this.logger.push(
|
|
56
|
+
logger: this.logger.push(LOG_ID.CACHE),
|
|
42
57
|
enableTestFeatures: conf.enableTestFeatures,
|
|
43
58
|
});
|
|
44
59
|
|
|
60
|
+
this.metricsClient = new MetricsClient();
|
|
61
|
+
|
|
62
|
+
this.metricsServer = new MetricsServer({
|
|
63
|
+
port: this.conf.metrics.port,
|
|
64
|
+
logger: this.logger.push(LOG_ID.METRICS)
|
|
65
|
+
});
|
|
66
|
+
|
|
45
67
|
this.inboundServer = new InboundServer(
|
|
46
68
|
this.conf,
|
|
47
|
-
this.logger.push(
|
|
48
|
-
this.cache
|
|
69
|
+
this.logger.push(LOG_ID.INBOUND),
|
|
70
|
+
this.cache,
|
|
71
|
+
this.metricsClient
|
|
49
72
|
);
|
|
50
73
|
this.inboundServer.on('error', (...args) => {
|
|
51
74
|
this.logger.push({ args }).log('Unhandled error in Inbound Server');
|
|
@@ -54,8 +77,9 @@ class Server extends EventEmitter {
|
|
|
54
77
|
|
|
55
78
|
this.outboundServer = new OutboundServer(
|
|
56
79
|
this.conf,
|
|
57
|
-
this.logger.push(
|
|
58
|
-
this.cache
|
|
80
|
+
this.logger.push(LOG_ID.OUTBOUND),
|
|
81
|
+
this.cache,
|
|
82
|
+
this.metricsClient
|
|
59
83
|
);
|
|
60
84
|
this.outboundServer.on('error', (...args) => {
|
|
61
85
|
this.logger.push({ args }).log('Unhandled error in Outbound Server');
|
|
@@ -66,16 +90,140 @@ class Server extends EventEmitter {
|
|
|
66
90
|
clientKey: this.conf.oauthTestServer.clientKey,
|
|
67
91
|
clientSecret: this.conf.oauthTestServer.clientSecret,
|
|
68
92
|
port: this.conf.oauthTestServer.listenPort,
|
|
69
|
-
logger: this.logger.push(
|
|
93
|
+
logger: this.logger.push(LOG_ID.OAUTHTEST),
|
|
70
94
|
});
|
|
71
95
|
|
|
72
96
|
this.testServer = new TestServer({
|
|
73
|
-
port: this.conf.
|
|
74
|
-
logger: this.logger.push(
|
|
97
|
+
port: this.conf.test.port,
|
|
98
|
+
logger: this.logger.push(LOG_ID.TEST),
|
|
75
99
|
cache: this.cache,
|
|
76
100
|
});
|
|
77
101
|
}
|
|
78
102
|
|
|
103
|
+
async restart(newConf) {
|
|
104
|
+
// Figuring out what config is necessary in each server and component is a pretty big job
|
|
105
|
+
// that we'll have to save for later. For now, when the config changes, we'll restart
|
|
106
|
+
// more than we might have to.
|
|
107
|
+
// We'll do this by:
|
|
108
|
+
// 0. creating a new instance of the logger, if necessary
|
|
109
|
+
// 1. creating a new instance of the cache, if necessary
|
|
110
|
+
// 2. calling the async reconfigure method of each of the servers as necessary- this will
|
|
111
|
+
// return a synchronous function that we can call to swap over the server events and
|
|
112
|
+
// object properties to the new ones. It will:
|
|
113
|
+
// 1. remove the `request` listener for each of the HTTP servers
|
|
114
|
+
// 2. add the new appropriate `request` listener
|
|
115
|
+
// This results in a completely synchronous listener changeover to the new config and
|
|
116
|
+
// therefore hopefully avoids any concurrency issues arising from restarting different
|
|
117
|
+
// servers or components concurrently.
|
|
118
|
+
// TODO: in the sense of being able to reason about the code, it would make some sense to
|
|
119
|
+
// turn the config items or object passed to each server into an event emitter, or pass an
|
|
120
|
+
// additional event emitter to the server constructor for the server to listen to and act
|
|
121
|
+
// on changes. Before this, however, it's probably necessary to ensure each server gets
|
|
122
|
+
// _only_ the config it needs, not the entire config object.
|
|
123
|
+
// Further: it might be possible to use Object.observe for this functionality.
|
|
124
|
+
// TODO: what happens if this is run concurrently? I.e. if it is called twice in rapid
|
|
125
|
+
// succession. This question probably needs to be asked of the reconfigure message on every
|
|
126
|
+
// server.
|
|
127
|
+
// Note that it should be possible to reconfigure ports on a running server by reassigning
|
|
128
|
+
// servers, e.g.
|
|
129
|
+
// this.inboundServer._server = createHttpServer();
|
|
130
|
+
// this.inboundServer._server.listen(newPort);
|
|
131
|
+
// If there are conflicts, for example if the new configuration specifies the new inbound
|
|
132
|
+
// port to be the same value as the old outbound port, this will require either
|
|
133
|
+
// 1. some juggling of HTTP servers, e.g.
|
|
134
|
+
// const oldInboundServer = this.inboundServer._server;
|
|
135
|
+
// this.inboundServer._server = this.outboundServer._server;
|
|
136
|
+
// .. etc.
|
|
137
|
+
// 2. some juggling of sockets between servers, if possible
|
|
138
|
+
// 3. rearchitecting of the servers, perhaps splitting the .start() method on the servers
|
|
139
|
+
// to an .init() and .listen() methods, with the latter optionally taking an HTTP server
|
|
140
|
+
// as argument
|
|
141
|
+
// This _might_ introduce some confusion/complexity for existing websocket clients, but as
|
|
142
|
+
// the event handlers _should_ not be modified this shouldn't be a problem. A careful
|
|
143
|
+
// analysis of this will be necessary.
|
|
144
|
+
assert(newConf.inbound.port === this.conf.inbound.port
|
|
145
|
+
&& newConf.outbound.port === this.conf.outbound.port
|
|
146
|
+
&& newConf.test.port === this.conf.test.port
|
|
147
|
+
&& newConf.oauthTestServer.listenPort === this.conf.oauthTestServer.listenPort
|
|
148
|
+
&& newConf.control.mgmtAPIWsPort === this.conf.control.mgmtAPIWsPort,
|
|
149
|
+
'Cannot reconfigure ports on running server');
|
|
150
|
+
const doNothing = () => {};
|
|
151
|
+
const updateLogger = check.notDeepEqual(newConf.logIndent, this.conf.logIndent);
|
|
152
|
+
if (updateLogger) {
|
|
153
|
+
this.logger = new Logger.Logger({
|
|
154
|
+
context: {
|
|
155
|
+
// If we're running from a Mojaloop helm chart deployment, we'll have a SIM_NAME
|
|
156
|
+
simulator: process.env['SIM_NAME'],
|
|
157
|
+
hostname: hostname(),
|
|
158
|
+
},
|
|
159
|
+
stringify: Logger.buildStringify({ space: this.conf.logIndent }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
let oldCache;
|
|
163
|
+
const updateCache = (
|
|
164
|
+
updateLogger ||
|
|
165
|
+
check.notDeepEqual(this.conf.cacheConfig, newConf.cacheConfig) ||
|
|
166
|
+
check.notDeepEqual(this.conf.enableTestFeatures, newConf.enableTestFeatures)
|
|
167
|
+
);
|
|
168
|
+
if (updateCache) {
|
|
169
|
+
oldCache = this.cache;
|
|
170
|
+
this.cache = new Cache({
|
|
171
|
+
...newConf.cacheConfig,
|
|
172
|
+
logger: this.logger.push(LOG_ID.CACHE),
|
|
173
|
+
enableTestFeatures: newConf.enableTestFeatures,
|
|
174
|
+
});
|
|
175
|
+
await this.cache.connect();
|
|
176
|
+
}
|
|
177
|
+
const confChanged = !check.deepEqual(newConf, this.conf);
|
|
178
|
+
// TODO: find better naming than "restart", because that's not really what's happening.
|
|
179
|
+
const [restartInboundServer, restartOutboundServer, restartControlClient] = confChanged
|
|
180
|
+
? await Promise.all([
|
|
181
|
+
this.inboundServer.reconfigure(newConf, this.logger.push(LOG_ID.INBOUND), this.cache),
|
|
182
|
+
this.outboundServer.reconfigure(newConf, this.logger.push(LOG_ID.OUTBOUND), this.cache, this.metricsClient),
|
|
183
|
+
this.controlClient.reconfigure({
|
|
184
|
+
logger: this.logger.push(LOG_ID.CONTROL),
|
|
185
|
+
port: newConf.control.mgmtAPIWsPort,
|
|
186
|
+
appConfig: newConf
|
|
187
|
+
}),
|
|
188
|
+
])
|
|
189
|
+
: [doNothing, doNothing, doNothing];
|
|
190
|
+
const updateOAuthTestServer = (
|
|
191
|
+
updateLogger || check.notDeepEqual(newConf.oauthTestServer, this.conf.oauthTestServer)
|
|
192
|
+
);
|
|
193
|
+
const restartOAuthTestServer = updateOAuthTestServer
|
|
194
|
+
? await this.oauthTestServer.reconfigure({
|
|
195
|
+
clientKey: this.conf.oauthTestServer.clientKey,
|
|
196
|
+
clientSecret: this.conf.oauthTestServer.clientSecret,
|
|
197
|
+
port: this.conf.oauthTestServer.listenPort,
|
|
198
|
+
logger: this.logger.push(LOG_ID.OAUTHTEST),
|
|
199
|
+
})
|
|
200
|
+
: doNothing;
|
|
201
|
+
const updateTestServer = (
|
|
202
|
+
updateLogger || updateCache || check.notDeepEqual(newConf.test.port, this.conf.test.port)
|
|
203
|
+
);
|
|
204
|
+
const restartTestServer = updateTestServer
|
|
205
|
+
? await this.testServer.reconfigure({
|
|
206
|
+
port: newConf.test.port,
|
|
207
|
+
logger: this.logger.push(LOG_ID.TEST),
|
|
208
|
+
cache: this.cache,
|
|
209
|
+
})
|
|
210
|
+
: doNothing;
|
|
211
|
+
// You may not return an async restart function. Perform any required async activity in the
|
|
212
|
+
// reconfigure function and return a sync restart function. See the note at the top of this
|
|
213
|
+
// file.
|
|
214
|
+
[restartTestServer, restartOAuthTestServer, restartInboundServer, restartOutboundServer, restartControlClient]
|
|
215
|
+
.map(f => assert(Promise.resolve(f) !== f, 'Restart functions must be synchronous'));
|
|
216
|
+
restartTestServer();
|
|
217
|
+
restartOAuthTestServer();
|
|
218
|
+
restartInboundServer();
|
|
219
|
+
restartOutboundServer();
|
|
220
|
+
restartControlClient();
|
|
221
|
+
this.conf = newConf;
|
|
222
|
+
await Promise.all([
|
|
223
|
+
oldCache && oldCache.disconnect(),
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
|
|
79
227
|
async start() {
|
|
80
228
|
await this.cache.connect();
|
|
81
229
|
|
|
@@ -83,9 +231,25 @@ class Server extends EventEmitter {
|
|
|
83
231
|
const startOauthTestServer = this.conf.oauthTestServer.enabled
|
|
84
232
|
? this.oauthTestServer.start()
|
|
85
233
|
: null;
|
|
234
|
+
|
|
235
|
+
// We only start the control client if we're running within Mojaloop Payment Manager.
|
|
236
|
+
// The control server is the Payment Manager Management API Service.
|
|
237
|
+
// We only start the client to connect to and listen to the Management API service for
|
|
238
|
+
// management protocol messages e.g configuration changes, certificate updates etc.
|
|
239
|
+
if (this.conf.pm4mlEnabled) {
|
|
240
|
+
this.controlClient = await ControlAgent.Client.Create({
|
|
241
|
+
address: this.conf.control.mgmtAPIWsUrl,
|
|
242
|
+
port: this.conf.control.mgmtAPIWsPort,
|
|
243
|
+
logger: this.logger.push(LOG_ID.CONTROL),
|
|
244
|
+
appConfig: this.conf,
|
|
245
|
+
});
|
|
246
|
+
this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
|
|
247
|
+
}
|
|
248
|
+
|
|
86
249
|
await Promise.all([
|
|
87
250
|
this.inboundServer.start(),
|
|
88
251
|
this.outboundServer.start(),
|
|
252
|
+
this.metricsServer.start(),
|
|
89
253
|
startTestServer,
|
|
90
254
|
startOauthTestServer,
|
|
91
255
|
]);
|
|
@@ -97,10 +261,23 @@ class Server extends EventEmitter {
|
|
|
97
261
|
this.outboundServer.stop(),
|
|
98
262
|
this.oauthTestServer.stop(),
|
|
99
263
|
this.testServer.stop(),
|
|
264
|
+
this.controlClient.stop(),
|
|
265
|
+
this.metricsServer.stop(),
|
|
100
266
|
]);
|
|
101
267
|
}
|
|
102
268
|
}
|
|
103
269
|
|
|
270
|
+
/*
|
|
271
|
+
* Call the Connector Manager in Management API to get the updated config
|
|
272
|
+
*/
|
|
273
|
+
async function _GetUpdatedConfigFromMgmtAPI(conf, logger, client) {
|
|
274
|
+
logger.log(`Getting updated config from Management API at ${conf.control.mgmtAPIWsUrl}:${conf.control.mgmtAPIWsPort}...`);
|
|
275
|
+
const clientSendResponse = await client.send(ControlAgent.build.CONFIGURATION.READ());
|
|
276
|
+
logger.log('client send returned:: ', clientSendResponse);
|
|
277
|
+
const responseRead = await client.receive();
|
|
278
|
+
logger.log('client receive returned:: ', responseRead);
|
|
279
|
+
return responseRead.data;
|
|
280
|
+
}
|
|
104
281
|
|
|
105
282
|
if(require.main === module) {
|
|
106
283
|
(async () => {
|
|
@@ -114,6 +291,18 @@ if(require.main === module) {
|
|
|
114
291
|
},
|
|
115
292
|
stringify: Logger.buildStringify({ space: config.logIndent }),
|
|
116
293
|
});
|
|
294
|
+
if(config.pm4mlEnabled) {
|
|
295
|
+
const controlClient = await ControlAgent.Client.Create({
|
|
296
|
+
address: config.control.mgmtAPIWsUrl,
|
|
297
|
+
port: config.control.mgmtAPIWsPort,
|
|
298
|
+
logger: logger,
|
|
299
|
+
appConfig: config,
|
|
300
|
+
});
|
|
301
|
+
const updatedConfigFromMgmtAPI = await _GetUpdatedConfigFromMgmtAPI(config, logger, controlClient);
|
|
302
|
+
logger.log(`updatedConfigFromMgmtAPI: ${JSON.stringify(updatedConfigFromMgmtAPI)}`);
|
|
303
|
+
_.merge(config, updatedConfigFromMgmtAPI);
|
|
304
|
+
controlClient.terminate();
|
|
305
|
+
}
|
|
117
306
|
const svr = new Server(config, logger);
|
|
118
307
|
svr.on('error', (err) => {
|
|
119
308
|
logger.push({ err }).log('Unhandled server error');
|
|
@@ -139,6 +328,7 @@ if(require.main === module) {
|
|
|
139
328
|
// scheme adapter as a service
|
|
140
329
|
module.exports = {
|
|
141
330
|
Cache,
|
|
331
|
+
ControlAgent,
|
|
142
332
|
InboundServerMiddleware,
|
|
143
333
|
OutboundServerMiddleware,
|
|
144
334
|
Router,
|
package/src/lib/cache.js
CHANGED
|
@@ -21,8 +21,8 @@ const CONN_ST = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
* A shared cache abstraction over a REDIS distributed key/value store
|
|
25
|
+
*/
|
|
26
26
|
class Cache {
|
|
27
27
|
constructor(config) {
|
|
28
28
|
this._config = config;
|
|
@@ -53,12 +53,12 @@ class Cache {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
* Connects to a redis server and waits for ready events
|
|
57
|
+
* Note: We create two connections. One for get, set and publish commands
|
|
58
|
+
* and another for subscribe commands. We do this as we are not supposed
|
|
59
|
+
* to issue any non-pub/sub related commands on a connection used for sub
|
|
60
|
+
* See: https://redis.io/topics/pubsub
|
|
61
|
+
*/
|
|
62
62
|
async connect() {
|
|
63
63
|
switch(this._connectionState) {
|
|
64
64
|
case CONN_ST.CONNECTED:
|
|
@@ -74,7 +74,6 @@ class Cache {
|
|
|
74
74
|
await this._inProgressDisconnection;
|
|
75
75
|
break;
|
|
76
76
|
}
|
|
77
|
-
|
|
78
77
|
this._connectionState = CONN_ST.CONNECTING;
|
|
79
78
|
this._inProgressConnection = Promise.all([this._getClient(), this._getClient()]);
|
|
80
79
|
[this._client, this._subscriptionClient] = await this._inProgressConnection;
|
|
@@ -82,15 +81,19 @@ class Cache {
|
|
|
82
81
|
// hook up our sub message handler
|
|
83
82
|
this._subscriptionClient.on('message', this._onMessage.bind(this));
|
|
84
83
|
|
|
84
|
+
if (this._config.enableTestFeatures) {
|
|
85
|
+
this.setTestMode(true);
|
|
86
|
+
}
|
|
87
|
+
|
|
85
88
|
this._inProgressConnection = null;
|
|
86
89
|
this._connectionState = CONN_ST.CONNECTED;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
* Configure Redis to emit keyevent events. This corresponds to the application test mode, and
|
|
94
|
+
* enables us to listen for changes on callback_* and request_* keys.
|
|
95
|
+
* Docs: https://redis.io/topics/notifications
|
|
96
|
+
*/
|
|
94
97
|
async setTestMode(enable) {
|
|
95
98
|
// See for modes: https://redis.io/topics/notifications#configuration
|
|
96
99
|
// This mode, 'Es$' is:
|
|
@@ -100,7 +103,7 @@ class Cache {
|
|
|
100
103
|
const mode = enable ? 'Es$' : '';
|
|
101
104
|
this._logger
|
|
102
105
|
.push({ 'notify-keyspace-events': mode })
|
|
103
|
-
.log('
|
|
106
|
+
.log('Configuring Redis to emit keyevent events');
|
|
104
107
|
this._client.config('SET', 'notify-keyspace-events', mode);
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -133,12 +136,12 @@ class Cache {
|
|
|
133
136
|
|
|
134
137
|
|
|
135
138
|
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
* Subscribes to a channel
|
|
140
|
+
*
|
|
141
|
+
* @param channel {string} - The channel name to subscribe to
|
|
142
|
+
* @param callback {function} - Callback function to be executed when messages arrive on the specified channel
|
|
143
|
+
* @returns {Promise} - Promise that resolves with an integer callback Id to submit in unsubscribe request
|
|
144
|
+
*/
|
|
142
145
|
async subscribe(channel, callback) {
|
|
143
146
|
return new Promise((resolve, reject) => {
|
|
144
147
|
this._subscriptionClient.subscribe(channel, (err) => {
|
|
@@ -168,11 +171,11 @@ class Cache {
|
|
|
168
171
|
|
|
169
172
|
|
|
170
173
|
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
* Unsubscribes a callback from a channel
|
|
175
|
+
*
|
|
176
|
+
* @param channel {string} - name of the channel to unsubscribe from
|
|
177
|
+
* @param callbackId {integer} - id of the callback to remove
|
|
178
|
+
*/
|
|
176
179
|
async unsubscribe(channel, callbackId) {
|
|
177
180
|
return new Promise((resolve, reject) => {
|
|
178
181
|
if(this._callbacks[channel] && this._callbacks[channel][callbackId]) {
|
|
@@ -196,8 +199,8 @@ class Cache {
|
|
|
196
199
|
|
|
197
200
|
|
|
198
201
|
/**
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
* Handler for published messages
|
|
203
|
+
*/
|
|
201
204
|
async _onMessage(channel, msg) {
|
|
202
205
|
if(this._callbacks[channel]) {
|
|
203
206
|
// we have some callbacks to make
|
|
@@ -219,16 +222,16 @@ class Cache {
|
|
|
219
222
|
|
|
220
223
|
|
|
221
224
|
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
* Returns a new redis client
|
|
226
|
+
*
|
|
227
|
+
* @returns {object} - a connected REDIS client
|
|
228
|
+
* */
|
|
226
229
|
async _getClient() {
|
|
227
230
|
return new Promise((resolve, reject) => {
|
|
228
231
|
const client = redis.createClient(this._config);
|
|
229
232
|
|
|
230
233
|
client.on('error', (err) => {
|
|
231
|
-
this._logger.push({ err }).log('REDIS client
|
|
234
|
+
this._logger.push({ err }).log('Error from REDIS client getting subscriber');
|
|
232
235
|
return reject(err);
|
|
233
236
|
});
|
|
234
237
|
|
|
@@ -247,25 +250,25 @@ class Cache {
|
|
|
247
250
|
}
|
|
248
251
|
});
|
|
249
252
|
|
|
250
|
-
client.on('ready', () => {
|
|
251
|
-
this._logger.log(`REDIS client ready at: ${this._config.host}:${this._config.port}`);
|
|
252
|
-
return resolve(client);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
253
|
client.on('connect', () => {
|
|
256
254
|
this._logger.log(`REDIS client connected at: ${this._config.host}:${this._config.port}`);
|
|
257
255
|
});
|
|
256
|
+
|
|
257
|
+
client.on('ready', () => {
|
|
258
|
+
this._logger.log(`Connected to REDIS at: ${this._config.host}:${this._config.port}`);
|
|
259
|
+
return resolve(client);
|
|
260
|
+
});
|
|
258
261
|
});
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
|
|
262
265
|
/**
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
* Publishes the specified message to the specified channel
|
|
267
|
+
*
|
|
268
|
+
* @param channelName {string} - channel name to publish to
|
|
269
|
+
* @param value - any type that will be converted to a JSON string (unless it is already a string) and published as the message
|
|
270
|
+
* @returns {Promise} - Promise that will resolve with redis replies or reject with an error
|
|
271
|
+
*/
|
|
269
272
|
async publish(channelName, value) {
|
|
270
273
|
return new Promise((resolve, reject) => {
|
|
271
274
|
if(typeof(value) !== 'string') {
|
|
@@ -288,11 +291,11 @@ class Cache {
|
|
|
288
291
|
|
|
289
292
|
|
|
290
293
|
/**
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
* Sets a value in the cache
|
|
295
|
+
*
|
|
296
|
+
* @param key {string} - cache key
|
|
297
|
+
* @param value {stirng} - cache value
|
|
298
|
+
*/
|
|
296
299
|
async set(key, value) {
|
|
297
300
|
return new Promise((resolve, reject) => {
|
|
298
301
|
//if we are given an object, turn it into a string
|
|
@@ -313,10 +316,65 @@ class Cache {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
/**
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
319
|
+
* Add the specified value to the set stored at key
|
|
320
|
+
*
|
|
321
|
+
* @param key {string} - cache key
|
|
322
|
+
* @param value {string} - cache value
|
|
323
|
+
*/
|
|
324
|
+
async add(key, value) {
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
//if we are given an object, turn it into a string
|
|
327
|
+
if(typeof(value) !== 'string') {
|
|
328
|
+
value = JSON.stringify(value);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this._client.sadd(key, value, (err, replies) => {
|
|
332
|
+
if(err) {
|
|
333
|
+
this._logger.push({ key, value, err }).log(`Error setting cache key: ${key}`);
|
|
334
|
+
return reject(err);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this._logger.push({ key, value, replies }).log(`Add cache key: ${key}`);
|
|
338
|
+
return resolve(replies);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Returns all the members of the set value stored at key
|
|
345
|
+
*
|
|
346
|
+
* @param key {string} - cache key
|
|
347
|
+
*/
|
|
348
|
+
async members(key) {
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
this._client.smembers(key, (err, value) => {
|
|
351
|
+
if(err) {
|
|
352
|
+
this._logger.push({ key, err }).log(`Error getting cache key: ${key}`);
|
|
353
|
+
return reject(err);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this._logger.push({ key, value }).log(`Got cache key: ${key}`);
|
|
357
|
+
|
|
358
|
+
if(typeof(value) === 'string') {
|
|
359
|
+
try {
|
|
360
|
+
value = JSON.parse(value);
|
|
361
|
+
}
|
|
362
|
+
catch(err) {
|
|
363
|
+
this._logger.push({ err }).log('Error parsing JSON cache value');
|
|
364
|
+
return reject(err);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return resolve(value);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Gets a value from the cache
|
|
375
|
+
*
|
|
376
|
+
* @param key {string} - cache key
|
|
377
|
+
*/
|
|
320
378
|
async get(key) {
|
|
321
379
|
return new Promise((resolve, reject) => {
|
|
322
380
|
this._client.get(key, (err, value) => {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**************************************************************************
|
|
2
|
+
* (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
|
|
3
|
+
* *
|
|
4
|
+
* This file is made available under the terms of the license agreement *
|
|
5
|
+
* specified in the corresponding source code repository. *
|
|
6
|
+
* *
|
|
7
|
+
* ORIGINAL AUTHOR: *
|
|
8
|
+
* James Bush - james.bush@modusbox.com *
|
|
9
|
+
**************************************************************************/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const http = require('http');
|
|
14
|
+
const Koa = require('koa');
|
|
15
|
+
const koaBody = require('koa-body');
|
|
16
|
+
const PrometheusClient = require('prom-client');
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A utility class that abstracts the underlying metrics implementation (Prometheus)
|
|
21
|
+
* from the consumer. This may be premature if Prometheus is never swapped out...but
|
|
22
|
+
* who can tell what the Universe will bring us.
|
|
23
|
+
*
|
|
24
|
+
* This object exposes methods for getting different types of measurement construct
|
|
25
|
+
* in order for consuming code to record metrics. The constructs are quite tightly
|
|
26
|
+
* coupled to Prometheus view of metrics, although that is fairly abstract so the
|
|
27
|
+
* risk appears low that this will cause conflicts in future.
|
|
28
|
+
*
|
|
29
|
+
* The metrics client is intended to be used as a singleton in a process and keeps a
|
|
30
|
+
* 'per name' cache of metrics to avoid duplicates. Not sure if this is strictly
|
|
31
|
+
* necessary but I dont have time to dig into the prom-client code to see what
|
|
32
|
+
* happens if you create the same metric twice.
|
|
33
|
+
*/
|
|
34
|
+
class MetricsClient {
|
|
35
|
+
constructor() {
|
|
36
|
+
this._prometheusRegister = PrometheusClient.register;
|
|
37
|
+
this._metrics = {};
|
|
38
|
+
|
|
39
|
+
this._counterPrefix = 'cntr_';
|
|
40
|
+
this._histogramPrefix = 'hist_';
|
|
41
|
+
this._gaugePrefix = 'gage_';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
getHistogram(name, description, buckets) {
|
|
46
|
+
const metricName = `${this._histogramPrefix}${name}`;
|
|
47
|
+
|
|
48
|
+
let conf = {
|
|
49
|
+
name: name,
|
|
50
|
+
help: description,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if(buckets) {
|
|
54
|
+
conf.buckets = buckets;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if(!this._metrics[metricName]) {
|
|
58
|
+
this._metrics[metricName] = new PrometheusClient.Histogram(conf);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this._metrics[metricName];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
getCounter(name, description) {
|
|
66
|
+
const metricName = `${this._counterPrefix}${name}`;
|
|
67
|
+
|
|
68
|
+
if(!this._metrics[metricName]) {
|
|
69
|
+
this._metrics[metricName] = new PrometheusClient.Counter({
|
|
70
|
+
name: name,
|
|
71
|
+
help: description
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this._metrics[metricName];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
getGauge(name, description) {
|
|
80
|
+
const metricName = `${this._counterPrefix}${name}`;
|
|
81
|
+
|
|
82
|
+
if(!this._metrics[metricName]) {
|
|
83
|
+
this._metrics[metricName] = new PrometheusClient.Gauge({
|
|
84
|
+
name: name,
|
|
85
|
+
help: description
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this._metrics[metricName];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Exposes an HTTP endpoint for metrics to be scraped by some external daemon
|
|
96
|
+
*/
|
|
97
|
+
class MetricsServer {
|
|
98
|
+
/**
|
|
99
|
+
* @param {number} port metrics server listen port
|
|
100
|
+
* @param {Logger} logger Logger
|
|
101
|
+
* @param {Object} prometheusClient Prometheus client instance
|
|
102
|
+
*/
|
|
103
|
+
constructor({ port, logger }) {
|
|
104
|
+
this._port = port;
|
|
105
|
+
this._logger = logger;
|
|
106
|
+
this._prometheusClient = PrometheusClient;
|
|
107
|
+
this._prometheusRegister = PrometheusClient.register;
|
|
108
|
+
this._api = this.setupApi();
|
|
109
|
+
this._server = http.createServer(this._api.callback());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async start() {
|
|
113
|
+
if (this._server.listening) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this._prometheusClient.collectDefaultMetrics({
|
|
117
|
+
prefix: 'mojaloop_connector_default_'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await new Promise((resolve) => this._server.listen(this._port, resolve));
|
|
121
|
+
this._logger.push({ port: this._port }).log('Serving Metrics');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async stop() {
|
|
125
|
+
await new Promise(resolve => this._server.close(resolve));
|
|
126
|
+
this._logger.log('Metrics Server shut down complete');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setupApi() {
|
|
130
|
+
const result = new Koa();
|
|
131
|
+
|
|
132
|
+
result.use(koaBody());
|
|
133
|
+
result.use(async ctx => {
|
|
134
|
+
this._logger.log('Metrics request received');
|
|
135
|
+
|
|
136
|
+
ctx.response.set('Content-Type', this._prometheusRegister.contentType);
|
|
137
|
+
ctx.response.body = this._prometheusRegister.metrics();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
MetricsServer,
|
|
147
|
+
MetricsClient
|
|
148
|
+
};
|
|
@@ -66,6 +66,36 @@ class OutboundTransfersModel {
|
|
|
66
66
|
secret: config.ilpSecret,
|
|
67
67
|
logger: this._logger,
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
this.metrics = {
|
|
71
|
+
partyLookupRequests: config.metricsClient.getCounter(
|
|
72
|
+
'mojaloop_connector_outbound_party_lookup_request_count',
|
|
73
|
+
'Count of outbound party lookup requests sent'),
|
|
74
|
+
partyLookupResponses: config.metricsClient.getCounter(
|
|
75
|
+
'mojaloop_connector_outbound_party_lookup_response_count',
|
|
76
|
+
'Count of responses received to outbound party lookups'),
|
|
77
|
+
quoteRequests: config.metricsClient.getCounter(
|
|
78
|
+
'mojaloop_connector_outbound_quote_request_count',
|
|
79
|
+
'Count of outbound quote requests sent'),
|
|
80
|
+
quoteResponses: config.metricsClient.getCounter(
|
|
81
|
+
'mojaloop_connector_outbound_quote_response_count',
|
|
82
|
+
'Count of responses received to outbound quote requests'),
|
|
83
|
+
transferPrepares: config.metricsClient.getCounter(
|
|
84
|
+
'mojaloop_connector_outbound_transfer_prepare_count',
|
|
85
|
+
'Count of outbound transfer prepare requests sent'),
|
|
86
|
+
transferFulfils: config.metricsClient.getCounter(
|
|
87
|
+
'mojaloop_connector_outbound_transfer_fulfil_response_count',
|
|
88
|
+
'Count of responses received to outbound transfer prepares'),
|
|
89
|
+
partyLookupLatency: config.metricsClient.getHistogram(
|
|
90
|
+
'mojaloop_connector_outbound_party_lookup_latency',
|
|
91
|
+
'Time taken for a response to a party lookup request to be received'),
|
|
92
|
+
quoteRequestLatency: config.metricsClient.getHistogram(
|
|
93
|
+
'mojaloop_connector_outbound_quote_request_latency',
|
|
94
|
+
'Time taken for a response to a quote request to be received'),
|
|
95
|
+
transferLatency: config.metricsClient.getHistogram(
|
|
96
|
+
'mojaloop_connector_outbound_transfer_latency',
|
|
97
|
+
'Time taken for a response to a transfer prepare to be received')
|
|
98
|
+
};
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
|
|
@@ -185,9 +215,16 @@ class OutboundTransfersModel {
|
|
|
185
215
|
subId: this.data.to.idSubValue
|
|
186
216
|
});
|
|
187
217
|
|
|
218
|
+
let latencyTimerDone;
|
|
219
|
+
|
|
188
220
|
// hook up a subscriber to handle response messages
|
|
189
221
|
const subId = await this._cache.subscribe(payeeKey, (cn, msg, subId) => {
|
|
190
222
|
try {
|
|
223
|
+
if(latencyTimerDone) {
|
|
224
|
+
latencyTimerDone();
|
|
225
|
+
}
|
|
226
|
+
this.metrics.partyLookupResponses.inc();
|
|
227
|
+
|
|
191
228
|
let payee = JSON.parse(msg);
|
|
192
229
|
|
|
193
230
|
if(payee.errorInformation) {
|
|
@@ -279,8 +316,10 @@ class OutboundTransfersModel {
|
|
|
279
316
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
280
317
|
// a GET /parties request to the switch
|
|
281
318
|
try {
|
|
319
|
+
latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
|
|
282
320
|
const res = await this._requests.getParties(this.data.to.idType, this.data.to.idValue,
|
|
283
321
|
this.data.to.idSubValue, this.data.to.fspId);
|
|
322
|
+
this.metrics.partyLookupRequests.inc();
|
|
284
323
|
this._logger.push({ peer: res }).log('Party lookup sent to peer');
|
|
285
324
|
}
|
|
286
325
|
catch(err) {
|
|
@@ -312,10 +351,16 @@ class OutboundTransfersModel {
|
|
|
312
351
|
|
|
313
352
|
// listen for events on the quoteId
|
|
314
353
|
const quoteKey = `qt_${quote.quoteId}`;
|
|
354
|
+
let latencyTimerDone;
|
|
315
355
|
|
|
316
356
|
// hook up a subscriber to handle response messages
|
|
317
357
|
const subId = await this._cache.subscribe(quoteKey, (cn, msg, subId) => {
|
|
318
358
|
try {
|
|
359
|
+
if(latencyTimerDone) {
|
|
360
|
+
latencyTimerDone();
|
|
361
|
+
}
|
|
362
|
+
this.metrics.quoteResponses.inc();
|
|
363
|
+
|
|
319
364
|
let error;
|
|
320
365
|
let message = JSON.parse(msg);
|
|
321
366
|
|
|
@@ -380,7 +425,9 @@ class OutboundTransfersModel {
|
|
|
380
425
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
381
426
|
// a POST /quotes request to the switch
|
|
382
427
|
try {
|
|
428
|
+
latencyTimerDone = this.metrics.quoteRequestLatency.startTimer();
|
|
383
429
|
const res = await this._requests.postQuotes(quote, this.data.to.fspId);
|
|
430
|
+
this.metrics.quoteRequests.inc();
|
|
384
431
|
this._logger.push({ res }).log('Quote request sent to peer');
|
|
385
432
|
}
|
|
386
433
|
catch(err) {
|
|
@@ -459,12 +506,20 @@ class OutboundTransfersModel {
|
|
|
459
506
|
// listen for events on the transferId
|
|
460
507
|
const transferKey = `tf_${this.data.transferId}`;
|
|
461
508
|
|
|
509
|
+
let latencyTimerDone;
|
|
510
|
+
|
|
462
511
|
const subId = await this._cache.subscribe(transferKey, async (cn, msg, subId) => {
|
|
463
512
|
try {
|
|
464
513
|
let error;
|
|
465
514
|
let message = JSON.parse(msg);
|
|
466
515
|
|
|
516
|
+
if(latencyTimerDone) {
|
|
517
|
+
latencyTimerDone();
|
|
518
|
+
}
|
|
519
|
+
|
|
467
520
|
if (message.type === 'transferFulfil') {
|
|
521
|
+
this.metrics.transferFulfils.inc();
|
|
522
|
+
|
|
468
523
|
if (this._rejectExpiredTransferFulfils) {
|
|
469
524
|
const now = new Date().toISOString();
|
|
470
525
|
if (now > prepare.expiration) {
|
|
@@ -523,7 +578,9 @@ class OutboundTransfersModel {
|
|
|
523
578
|
// now we have a timeout handler and a cache subscriber hooked up we can fire off
|
|
524
579
|
// a POST /transfers request to the switch
|
|
525
580
|
try {
|
|
581
|
+
latencyTimerDone = this.metrics.transferLatency.startTimer();
|
|
526
582
|
const res = await this._requests.postTransfers(prepare, this.data.quoteResponseSource);
|
|
583
|
+
this.metrics.transferPrepares.inc();
|
|
527
584
|
this._logger.push({ res }).log('Transfer prepare sent to peer');
|
|
528
585
|
}
|
|
529
586
|
catch(err) {
|
package/test/__mocks__/redis.js
CHANGED
package/test/unit/api/utils.js
CHANGED
|
@@ -6,6 +6,7 @@ const Validate = require('~/lib/validate');
|
|
|
6
6
|
|
|
7
7
|
const InboundServer = require('~/InboundServer');
|
|
8
8
|
const OutboundServer = require('~/OutboundServer');
|
|
9
|
+
const { MetricsClient } = require('~/lib/metrics');
|
|
9
10
|
const { Logger } = require('@mojaloop/sdk-standard-components');
|
|
10
11
|
const Cache = require('~/lib/cache');
|
|
11
12
|
|
|
@@ -47,7 +48,9 @@ const createTestServers = async (config) => {
|
|
|
47
48
|
});
|
|
48
49
|
await cache.connect();
|
|
49
50
|
defConfig.requestProcessingTimeoutSeconds = 2;
|
|
50
|
-
const
|
|
51
|
+
const metricsClient = new MetricsClient();
|
|
52
|
+
metricsClient._prometheusRegister.clear();
|
|
53
|
+
const serverOutbound = new OutboundServer(defConfig, logger, cache, metricsClient);
|
|
51
54
|
await serverOutbound.start();
|
|
52
55
|
const reqOutbound = supertest(serverOutbound._server);
|
|
53
56
|
|
package/test/unit/index.test.js
CHANGED
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
'use strict';
|
|
12
12
|
|
|
13
|
-
const { Logger } = require('@mojaloop/sdk-standard-components');
|
|
14
|
-
const defaultConfig = require('./data/defaultConfig');
|
|
15
|
-
|
|
16
13
|
jest.mock('dotenv', () => ({
|
|
17
14
|
config: jest.fn()
|
|
18
15
|
}));
|
|
19
16
|
|
|
17
|
+
const promClient = require('prom-client');
|
|
18
|
+
const defaultConfig = require('./data/defaultConfig.json');
|
|
19
|
+
const { Logger } = require('@mojaloop/sdk-standard-components');
|
|
20
|
+
const { MetricsClient } = require('~/lib/metrics');
|
|
21
|
+
|
|
22
|
+
const TestControlServer = require('./ControlServer');
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
process.env.PEER_ENDPOINT = '172.17.0.3:4000';
|
|
21
26
|
process.env.BACKEND_ENDPOINT = '172.17.0.5:4000';
|
|
22
27
|
process.env.CACHE_HOST = '172.17.0.2';
|
|
@@ -26,6 +31,10 @@ process.env.MGMT_API_WS_URL = '0.0.0.0';
|
|
|
26
31
|
const index = require('~/index.js');
|
|
27
32
|
|
|
28
33
|
describe('index.js', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
promClient.register.clear();
|
|
36
|
+
});
|
|
37
|
+
|
|
29
38
|
test('WSO2 error events in OutboundServer propagate to top-level server', () => {
|
|
30
39
|
const logger = new Logger.Logger({ stringify: () => '' });
|
|
31
40
|
const svr = new index.Server(defaultConfig, logger);
|
|
@@ -53,3 +62,86 @@ describe('index.js', () => {
|
|
|
53
62
|
expect(typeof(index.Cache)).toBe('function');
|
|
54
63
|
});
|
|
55
64
|
});
|
|
65
|
+
|
|
66
|
+
describe('Server', () => {
|
|
67
|
+
let server, controlServer, conf, logger;
|
|
68
|
+
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
promClient.register.clear();
|
|
71
|
+
logger = new Logger.Logger({ stringify: () => '' });
|
|
72
|
+
conf = JSON.parse(JSON.stringify(defaultConfig));
|
|
73
|
+
conf.enableTestFeatures = true;
|
|
74
|
+
conf.pm4mlEnabled = true;
|
|
75
|
+
conf.control.mgmtAPIWsUrl = 'localhost';
|
|
76
|
+
conf.control.mgmtAPIWsPort = 4005;
|
|
77
|
+
conf.control.port = conf.control.mgmtAPIWsPort;
|
|
78
|
+
controlServer = new TestControlServer.Server({ logger, appConfig: conf });
|
|
79
|
+
server = new index.Server(conf, logger);
|
|
80
|
+
await server.start();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(async () => {
|
|
84
|
+
await controlServer.stop();
|
|
85
|
+
await server.stop();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('is reconfigured correctly by the control client', () => {
|
|
89
|
+
let newConf;
|
|
90
|
+
beforeEach(async () => {
|
|
91
|
+
// not every server restarts on every config change, we'll make sure they all restart
|
|
92
|
+
newConf = { ...conf, logIndent: conf.logIndent + 1, control: { ...conf.control, rubbish: 'data' }, test: { trash: 'data' } };
|
|
93
|
+
// Just in case, we'll assert the new configuration is different to the old one
|
|
94
|
+
expect(newConf).not.toEqual(conf);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('reconfigures and restarts constituent servers when triggered by control client', async () => {
|
|
98
|
+
const [restartInbound, restartOutbound, restartControl, restartOAuthTest, restartTest] =
|
|
99
|
+
Array.from({ length: 5 }).map(() => jest.fn());
|
|
100
|
+
server.inboundServer.reconfigure = jest.fn(() => restartInbound);
|
|
101
|
+
server.outboundServer.reconfigure = jest.fn(() => restartOutbound);
|
|
102
|
+
server.testServer.reconfigure = jest.fn(() => restartTest);
|
|
103
|
+
server.oauthTestServer.reconfigure = jest.fn(() => restartOAuthTest);
|
|
104
|
+
server.controlClient.reconfigure = jest.fn(() => restartControl);
|
|
105
|
+
|
|
106
|
+
await controlServer.broadcastConfigChange(newConf);
|
|
107
|
+
|
|
108
|
+
// We wait for the servers to get restarted
|
|
109
|
+
await new Promise((wait) => setTimeout(wait, 1000));
|
|
110
|
+
|
|
111
|
+
expect(server.inboundServer.reconfigure).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(server.inboundServer.reconfigure).toHaveBeenCalledWith(
|
|
113
|
+
newConf, expect.any(Logger.Logger), expect.any(index.Cache)
|
|
114
|
+
);
|
|
115
|
+
expect(server.outboundServer.reconfigure).toHaveBeenCalledTimes(1);
|
|
116
|
+
const metricsClient = new MetricsClient();
|
|
117
|
+
expect(server.outboundServer.reconfigure).toHaveBeenCalledWith(
|
|
118
|
+
newConf, expect.any(Logger.Logger), expect.any(index.Cache), metricsClient
|
|
119
|
+
);
|
|
120
|
+
expect(server.controlClient.reconfigure).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(server.controlClient.reconfigure).toHaveBeenCalledWith({
|
|
122
|
+
logger: expect.any(Logger.Logger),
|
|
123
|
+
port: newConf.control.port,
|
|
124
|
+
appConfig: newConf
|
|
125
|
+
});
|
|
126
|
+
expect(server.testServer.reconfigure).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(server.testServer.reconfigure).toHaveBeenCalledWith({
|
|
128
|
+
logger: expect.any(Logger.Logger),
|
|
129
|
+
cache: expect.any(index.Cache),
|
|
130
|
+
port: newConf.test.port
|
|
131
|
+
});
|
|
132
|
+
expect(server.oauthTestServer.reconfigure).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(server.oauthTestServer.reconfigure).toHaveBeenCalledWith({
|
|
134
|
+
logger: expect.any(Logger.Logger),
|
|
135
|
+
clientKey: newConf.oauthTestServer.clientKey,
|
|
136
|
+
clientSecret: newConf.oauthTestServer.clientSecret,
|
|
137
|
+
port: newConf.oauthTestServer.listenPort,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(restartInbound).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(restartOutbound).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(restartTest).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(restartOAuthTest).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(restartControl).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -15,6 +15,7 @@ jest.mock('@mojaloop/sdk-standard-components');
|
|
|
15
15
|
jest.mock('redis');
|
|
16
16
|
|
|
17
17
|
const Cache = require('~/lib/cache');
|
|
18
|
+
const { MetricsClient } = require('~/lib/metrics');
|
|
18
19
|
const Model = require('~/lib/model').OutboundTransfersModel;
|
|
19
20
|
const PartiesModel = require('~/lib/model').PartiesModel;
|
|
20
21
|
|
|
@@ -50,6 +51,7 @@ describe('outboundModel', () => {
|
|
|
50
51
|
let config;
|
|
51
52
|
let logger;
|
|
52
53
|
let cache;
|
|
54
|
+
let metricsClient;
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
*
|
|
@@ -91,6 +93,7 @@ describe('outboundModel', () => {
|
|
|
91
93
|
...config,
|
|
92
94
|
cache,
|
|
93
95
|
logger,
|
|
96
|
+
metricsClient,
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
await model.initialize(JSON.parse(JSON.stringify(transferRequest)));
|
|
@@ -113,6 +116,7 @@ describe('outboundModel', () => {
|
|
|
113
116
|
beforeAll(async () => {
|
|
114
117
|
logger = new Logger.Logger({ context: { app: 'outbound-model-unit-tests-cache' }, stringify: () => '' });
|
|
115
118
|
quoteResponse = JSON.parse(JSON.stringify(quoteResponseTemplate));
|
|
119
|
+
metricsClient = new MetricsClient();
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
beforeEach(async () => {
|
|
@@ -140,6 +144,7 @@ describe('outboundModel', () => {
|
|
|
140
144
|
const model = new Model({
|
|
141
145
|
cache,
|
|
142
146
|
logger,
|
|
147
|
+
metricsClient,
|
|
143
148
|
...config,
|
|
144
149
|
});
|
|
145
150
|
|
|
@@ -194,6 +199,7 @@ describe('outboundModel', () => {
|
|
|
194
199
|
const model = new Model({
|
|
195
200
|
cache,
|
|
196
201
|
logger,
|
|
202
|
+
metricsClient,
|
|
197
203
|
...config,
|
|
198
204
|
});
|
|
199
205
|
|
|
@@ -274,6 +280,7 @@ describe('outboundModel', () => {
|
|
|
274
280
|
const model = new Model({
|
|
275
281
|
cache,
|
|
276
282
|
logger,
|
|
283
|
+
metricsClient,
|
|
277
284
|
...config,
|
|
278
285
|
});
|
|
279
286
|
|
|
@@ -303,6 +310,7 @@ describe('outboundModel', () => {
|
|
|
303
310
|
const model = new Model({
|
|
304
311
|
cache,
|
|
305
312
|
logger,
|
|
313
|
+
metricsClient,
|
|
306
314
|
...config,
|
|
307
315
|
});
|
|
308
316
|
|
|
@@ -333,6 +341,7 @@ describe('outboundModel', () => {
|
|
|
333
341
|
const model = new Model({
|
|
334
342
|
cache,
|
|
335
343
|
logger,
|
|
344
|
+
metricsClient,
|
|
336
345
|
...config,
|
|
337
346
|
});
|
|
338
347
|
|
|
@@ -360,6 +369,7 @@ describe('outboundModel', () => {
|
|
|
360
369
|
const model = new Model({
|
|
361
370
|
cache,
|
|
362
371
|
logger,
|
|
372
|
+
metricsClient,
|
|
363
373
|
...config,
|
|
364
374
|
});
|
|
365
375
|
|
|
@@ -395,6 +405,7 @@ describe('outboundModel', () => {
|
|
|
395
405
|
let model = new Model({
|
|
396
406
|
cache,
|
|
397
407
|
logger,
|
|
408
|
+
metricsClient,
|
|
398
409
|
...config,
|
|
399
410
|
});
|
|
400
411
|
|
|
@@ -421,6 +432,7 @@ describe('outboundModel', () => {
|
|
|
421
432
|
model = new Model({
|
|
422
433
|
cache,
|
|
423
434
|
logger,
|
|
435
|
+
metricsClient,
|
|
424
436
|
...config,
|
|
425
437
|
});
|
|
426
438
|
|
|
@@ -451,6 +463,7 @@ describe('outboundModel', () => {
|
|
|
451
463
|
let model = new Model({
|
|
452
464
|
cache,
|
|
453
465
|
logger,
|
|
466
|
+
metricsClient,
|
|
454
467
|
...config,
|
|
455
468
|
});
|
|
456
469
|
|
|
@@ -477,6 +490,7 @@ describe('outboundModel', () => {
|
|
|
477
490
|
model = new Model({
|
|
478
491
|
cache,
|
|
479
492
|
logger,
|
|
493
|
+
metricsClient,
|
|
480
494
|
...config,
|
|
481
495
|
});
|
|
482
496
|
|
|
@@ -502,6 +516,7 @@ describe('outboundModel', () => {
|
|
|
502
516
|
model = new Model({
|
|
503
517
|
cache,
|
|
504
518
|
logger,
|
|
519
|
+
metricsClient,
|
|
505
520
|
...config,
|
|
506
521
|
});
|
|
507
522
|
|
|
@@ -557,6 +572,7 @@ describe('outboundModel', () => {
|
|
|
557
572
|
const model = new Model({
|
|
558
573
|
cache,
|
|
559
574
|
logger,
|
|
575
|
+
metricsClient,
|
|
560
576
|
...config,
|
|
561
577
|
});
|
|
562
578
|
|
|
@@ -608,6 +624,7 @@ describe('outboundModel', () => {
|
|
|
608
624
|
const model = new Model({
|
|
609
625
|
cache,
|
|
610
626
|
logger,
|
|
627
|
+
metricsClient,
|
|
611
628
|
...config,
|
|
612
629
|
});
|
|
613
630
|
|
|
@@ -701,6 +718,7 @@ describe('outboundModel', () => {
|
|
|
701
718
|
const model = new Model({
|
|
702
719
|
cache,
|
|
703
720
|
logger,
|
|
721
|
+
metricsClient,
|
|
704
722
|
...config,
|
|
705
723
|
});
|
|
706
724
|
|
|
@@ -763,6 +781,7 @@ describe('outboundModel', () => {
|
|
|
763
781
|
const model = new Model({
|
|
764
782
|
cache,
|
|
765
783
|
logger,
|
|
784
|
+
metricsClient,
|
|
766
785
|
...config,
|
|
767
786
|
});
|
|
768
787
|
|
|
@@ -823,6 +842,7 @@ describe('outboundModel', () => {
|
|
|
823
842
|
const model = new Model({
|
|
824
843
|
cache,
|
|
825
844
|
logger,
|
|
845
|
+
metricsClient,
|
|
826
846
|
...config,
|
|
827
847
|
});
|
|
828
848
|
|
|
@@ -854,6 +874,7 @@ describe('outboundModel', () => {
|
|
|
854
874
|
new Model({
|
|
855
875
|
cache,
|
|
856
876
|
logger,
|
|
877
|
+
metricsClient,
|
|
857
878
|
...config
|
|
858
879
|
});
|
|
859
880
|
|
|
@@ -866,4 +887,19 @@ describe('outboundModel', () => {
|
|
|
866
887
|
|
|
867
888
|
test('Outbound server should use HTTP if outbound mTLS disabled', () =>
|
|
868
889
|
testTlsServer(false));
|
|
890
|
+
|
|
891
|
+
test('Outbound transfers model should record metrics', async () => {
|
|
892
|
+
const metrics = metricsClient._prometheusRegister.metrics();
|
|
893
|
+
expect(metrics).toBeTruthy();
|
|
894
|
+
|
|
895
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_request_count'));
|
|
896
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_response_count'));
|
|
897
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_count'));
|
|
898
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_response_count'));
|
|
899
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_prepare_count'));
|
|
900
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_fulfil_response_count'));
|
|
901
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_quote_request_latency'));
|
|
902
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_transfer_latency'));
|
|
903
|
+
expect(metrics).toEqual(expect.stringContaining('mojaloop_connector_outbound_party_lookup_latency'));
|
|
904
|
+
});
|
|
869
905
|
});
|