@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 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
 
@@ -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.2.3",
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({ component: 'cache' }),
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({ app: 'mojaloop-sdk-inbound-api' }),
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({ app: 'mojaloop-sdk-outbound-api' }),
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({ app: 'mojaloop-sdk-oauth-test-server' }),
93
+ logger: this.logger.push(LOG_ID.OAUTHTEST),
70
94
  });
71
95
 
72
96
  this.testServer = new TestServer({
73
- port: this.conf.testServerPort,
74
- logger: this.logger.push({ app: 'mojaloop-sdk-test-api' }),
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
- * A shared cache abstraction over a REDIS distributed key/value store
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
- * 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
- */
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
- * Configure Redis to emit keyevent events. This corresponds to the application test mode, and
91
- * enables us to listen for changes on callback_* and request_* keys.
92
- * Docs: https://redis.io/topics/notifications
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('REDIS client Configured to emit keyspace-events');
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
- * Subscribes to a channel
137
- *
138
- * @param channel {string} - The channel name to subscribe to
139
- * @param callback {function} - Callback function to be executed when messages arrive on the specified channel
140
- * @returns {Promise} - Promise that resolves with an integer callback Id to submit in unsubscribe request
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
- * Unsubscribes a callback from a channel
172
- *
173
- * @param channel {string} - name of the channel to unsubscribe from
174
- * @param callbackId {integer} - id of the callback to remove
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
- * Handler for published messages
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
- * Returns a new redis client
223
- *
224
- * @returns {object} - a connected REDIS client
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 Error');
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
- * Publishes the specified message to the specified channel
264
- *
265
- * @param channelName {string} - channel name to publish to
266
- * @param value - any type that will be converted to a JSON string (unless it is already a string) and published as the message
267
- * @returns {Promise} - Promise that will resolve with redis replies or reject with an error
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
- * Sets a value in the cache
292
- *
293
- * @param key {string} - cache key
294
- * @param value {stirng} - cache value
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
- * Gets a value from the cache
317
- *
318
- * @param key {string} - cache key
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) {
@@ -26,6 +26,10 @@ class RedisClient extends redisMock.RedisClient {
26
26
  }
27
27
  }
28
28
 
29
+ config(...args) {
30
+ this._executeCallback(...args);
31
+ }
32
+
29
33
  subscribe(...args) {
30
34
  super.subscribe(...args);
31
35
  this._executeCallback(...args);
@@ -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 serverOutbound = new OutboundServer(defConfig, logger, cache);
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
 
@@ -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
  });