@mojaloop/central-services-shared 18.30.8 → 18.32.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
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [18.32.0](https://github.com/mojaloop/central-services-shared/compare/v18.31.0...v18.32.0) (2025-09-06)
6
+
7
+
8
+ ### Features
9
+
10
+ * add sub service app-critical metrics on healthcheck ([#476](https://github.com/mojaloop/central-services-shared/issues/476)) ([6b57358](https://github.com/mojaloop/central-services-shared/commit/6b57358fa66388c605ba7dd3fb8111f86a6a52fc))
11
+
12
+ ## [18.31.0](https://github.com/mojaloop/central-services-shared/compare/v18.30.8...v18.31.0) (2025-09-05)
13
+
14
+
15
+ ### Features
16
+
17
+ * **csi-1713:** add general app-critical metric on healthcheck ([#475](https://github.com/mojaloop/central-services-shared/issues/475)) ([f3c2980](https://github.com/mojaloop/central-services-shared/commit/f3c2980b1f4167bcf0821c8b524651233799e218))
18
+
5
19
  ### [18.30.8](https://github.com/mojaloop/central-services-shared/compare/v18.30.7...v18.30.8) (2025-09-01)
6
20
 
7
21
 
package/audit-ci.jsonc CHANGED
@@ -5,6 +5,5 @@
5
5
  "moderate": true,
6
6
  "allowlist": [ // NOTE: Please add as much information as possible to any items added to the allowList
7
7
  // e.g. Currently no fixes available for the following
8
- "GHSA-968p-4wvh-cqc8" // https://github.com/advisories/GHSA-968p-4wvh-cqc8
9
8
  ]
10
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.30.8",
3
+ "version": "18.32.0",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -74,7 +74,7 @@
74
74
  "axios": "1.11.0",
75
75
  "clone": "2.1.2",
76
76
  "convict": "^6.2.4",
77
- "dotenv": "17.2.1",
77
+ "dotenv": "17.2.2",
78
78
  "env-var": "7.5.0",
79
79
  "event-stream": "4.0.1",
80
80
  "fast-safe-stringify": "2.1.1",
@@ -84,7 +84,7 @@
84
84
  "lodash": "4.17.21",
85
85
  "mustache": "4.2.0",
86
86
  "openapi-backend": "5.15.0",
87
- "raw-body": "3.0.0",
87
+ "raw-body": "3.0.1",
88
88
  "rc": "1.2.8",
89
89
  "redlock": "5.0.0-beta.2",
90
90
  "shins": "2.6.0",
@@ -96,10 +96,10 @@
96
96
  "devDependencies": {
97
97
  "@mojaloop/central-services-error-handling": "13.1.0",
98
98
  "@mojaloop/central-services-logger": "11.9.1",
99
- "@mojaloop/central-services-metrics": "12.6.0",
100
- "@mojaloop/event-sdk": "14.6.1",
99
+ "@mojaloop/central-services-metrics": "12.7.1",
100
+ "@mojaloop/event-sdk": "14.7.0",
101
101
  "@mojaloop/sdk-standard-components": "19.16.7",
102
- "@opentelemetry/auto-instrumentations-node": "^0.62.1",
102
+ "@opentelemetry/auto-instrumentations-node": "^0.62.2",
103
103
  "@types/hapi__joi": "17.1.15",
104
104
  "ajv": "^8.17.1",
105
105
  "ajv-formats": "^3.0.1",
@@ -32,6 +32,7 @@ const {
32
32
  statusEnum
33
33
  } = require('./HealthCheckEnums')
34
34
  const Logger = require('@mojaloop/central-services-logger')
35
+ const Metrics = require('@mojaloop/central-services-metrics')
35
36
 
36
37
  /**
37
38
  * @class HealthCheck
@@ -109,6 +110,9 @@ class HealthCheck {
109
110
  subServices = {
110
111
  services
111
112
  }
113
+
114
+ // Set per-subservice metrics
115
+ this.setSubServiceMetrics(services)
112
116
  } catch (err) {
113
117
  Logger.isErrorEnabled && Logger.error(`HealthCheck.getSubServiceHealth failed with error: ${err.message}`)
114
118
  isHealthy = false
@@ -118,6 +122,13 @@ class HealthCheck {
118
122
  status = statusEnum.DOWN
119
123
  }
120
124
 
125
+ try {
126
+ // Set general app-critical metrics
127
+ this.setGeneralMetrics(isHealthy)
128
+ } catch (err) {
129
+ Logger.isErrorEnabled && Logger.error(`Failed to set general app-critical metrics: ${err.message}`)
130
+ }
131
+
121
132
  return {
122
133
  status,
123
134
  uptime,
@@ -127,6 +138,35 @@ class HealthCheck {
127
138
  }
128
139
  }
129
140
 
141
+ setSubServiceMetrics (services) {
142
+ try {
143
+ services.forEach(service => {
144
+ // Counter for subservice critical events
145
+ if (service.status === statusEnum.DOWN) {
146
+ const subCounter = Metrics.getCounter(
147
+ 'app_critical_total',
148
+ 'Total times app entered critical health',
149
+ ['service']
150
+ )
151
+ subCounter.inc({ service: service?.name })
152
+ }
153
+ })
154
+ } catch (err) {
155
+ Logger.isErrorEnabled && Logger.error(`Failed to set subservice metrics: ${err.message}`)
156
+ }
157
+ }
158
+
159
+ setGeneralMetrics (isHealthy) {
160
+ try {
161
+ if (!isHealthy) {
162
+ const criticalCounter = Metrics.getCounter('app_critical_total', 'Total times app entered critical health', ['service'])
163
+ criticalCounter.inc({ service: 'general' })
164
+ }
165
+ } catch (err) {
166
+ Logger.isErrorEnabled && Logger.error(`Failed to set app-critical metrics: ${err.message}`)
167
+ }
168
+ }
169
+
130
170
  /**
131
171
  * @function evaluateServiceHealth
132
172
  *
@@ -31,6 +31,7 @@
31
31
  const Test = require('tapes')(require('tape'))
32
32
  const Sinon = require('sinon')
33
33
  const Joi = require('joi')
34
+ const Metrics = require('@mojaloop/central-services-metrics')
34
35
 
35
36
  const HealthCheck = require('../../../src/healthCheck').HealthCheck
36
37
 
@@ -172,5 +173,127 @@ Test('HealthCheck test', healthCheckTest => {
172
173
  evaluateServiceHealthTest.end()
173
174
  })
174
175
 
176
+ healthCheckTest.test('getHealth metrics', metricsTest => {
177
+ let metricsMock
178
+
179
+ metricsTest.beforeEach(t => {
180
+ metricsMock = {
181
+ getCounter: Sinon.stub().returns({ inc: Sinon.spy() })
182
+ }
183
+ // Mock Metrics
184
+ sandbox.stub(Metrics, 'getCounter').callsFake(metricsMock.getCounter)
185
+ t.end()
186
+ })
187
+
188
+ metricsTest.afterEach(t => {
189
+ sandbox.restore()
190
+ t.end()
191
+ })
192
+
193
+ metricsTest.test('increments counter when unhealthy', async test => {
194
+ // Arrange
195
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
196
+ async () => ({ status: 'DOWN', name: 'datastore' })
197
+ ])
198
+ // Act
199
+ await healthCheck.getHealth()
200
+ // Assert
201
+ test.ok(metricsMock.getCounter.calledWith('app_critical_total'), 'getCounter called')
202
+ // Subservice counter should be incremented
203
+ test.deepEqual(metricsMock.getCounter().inc.firstCall.args, [{ service: 'datastore' }], 'counter incremented for datastore service')
204
+ // General counter should be incremented
205
+ test.deepEqual(metricsMock.getCounter().inc.secondCall.args, [{ service: 'general' }], 'counter incremented for general service')
206
+ test.end()
207
+ })
208
+
209
+ metricsTest.test('increments counter for multiple sub-services with mixed health', async test => {
210
+ // Arrange
211
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
212
+ async () => ({ status: 'OK', name: 'datastore' }),
213
+ async () => ({ status: 'DOWN', name: 'broker' }),
214
+ async () => ({ status: 'OK', name: 'cache' })
215
+ ])
216
+ // Act
217
+ await healthCheck.getHealth()
218
+ // Assert
219
+ // Subservice counter incremented for DOWN service
220
+ test.deepEqual(metricsMock.getCounter().inc.firstCall.args, [{ service: 'broker' }], 'counter incremented for broker')
221
+ // General counter incremented
222
+ test.deepEqual(metricsMock.getCounter().inc.secondCall.args, [{ service: 'general' }], 'counter incremented for general service')
223
+ test.equal(metricsMock.getCounter().inc.callCount, 2, 'getCounter.inc called for each DOWN service and general')
224
+ test.end()
225
+ })
226
+
227
+ metricsTest.test('handles errors thrown in setGeneralMetrics gracefully', async test => {
228
+ // Arrange
229
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
230
+ async () => ({ status: 'OK', name: 'datastore' })
231
+ ])
232
+ // Patch setGeneralMetrics to throw
233
+ const origSetGeneralMetrics = healthCheck.setGeneralMetrics
234
+ healthCheck.setGeneralMetrics = () => { throw new Error('General metrics error') }
235
+ // Act & Assert
236
+ try {
237
+ await healthCheck.getHealth()
238
+ test.pass('No error thrown when setGeneralMetrics throws')
239
+ } catch (err) {
240
+ test.fail('Should not throw when setGeneralMetrics throws')
241
+ }
242
+ // Restore
243
+ healthCheck.setGeneralMetrics = origSetGeneralMetrics
244
+ test.end()
245
+ })
246
+
247
+ metricsTest.test('does not call getCounter if no subservice is DOWN', async test => {
248
+ // Arrange
249
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
250
+ async () => ({ status: 'OK', name: 'datastore' }),
251
+ async () => ({ status: 'OK', name: 'broker' })
252
+ ])
253
+ // Act
254
+ await healthCheck.getHealth()
255
+ // Assert
256
+ test.equal(metricsMock.getCounter().inc.callCount, 0, 'getCounter.inc not called when all services are healthy')
257
+ test.end()
258
+ })
259
+
260
+ metricsTest.test('calls getCounter only for DOWN subservices', async test => {
261
+ // Arrange
262
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
263
+ async () => ({ status: 'DOWN', name: 'datastore' }),
264
+ async () => ({ status: 'OK', name: 'broker' }),
265
+ async () => ({ status: 'DOWN', name: 'cache' })
266
+ ])
267
+ // Act
268
+ await healthCheck.getHealth()
269
+ // Assert
270
+ test.deepEqual(metricsMock.getCounter().inc.getCall(0).args, [{ service: 'datastore' }], 'counter incremented for datastore')
271
+ test.deepEqual(metricsMock.getCounter().inc.getCall(1).args, [{ service: 'cache' }], 'counter incremented for cache')
272
+ test.deepEqual(metricsMock.getCounter().inc.getCall(2).args, [{ service: 'general' }], 'counter incremented for general')
273
+ test.equal(metricsMock.getCounter().inc.callCount, 3, 'getCounter.inc called for each DOWN service and general')
274
+ test.end()
275
+ })
276
+
277
+ metricsTest.test('handles errors thrown in setSubServiceMetrics gracefully', async test => {
278
+ // Arrange
279
+ const healthCheck = new HealthCheck({ version: '1.0.0' }, [
280
+ async () => ({ status: 'DOWN', name: 'datastore' })
281
+ ])
282
+ // Stub Metrics.getCounter to throw when called
283
+ sandbox.restore() // Remove previous stubs
284
+ const getCounterStub = sandbox.stub(Metrics, 'getCounter').throws(new Error('Subservice metrics error'))
285
+ // Stub Logger.error to spy on error logging
286
+ const loggerErrorStub = sandbox.stub(require('@mojaloop/central-services-logger'), 'error')
287
+ // Act
288
+ await healthCheck.getHealth()
289
+ // Assert
290
+ test.ok(getCounterStub.called, 'getCounter called and throws')
291
+ test.ok(loggerErrorStub.called, 'Logger.error called when setSubServiceMetrics throws')
292
+ test.ok(loggerErrorStub.firstCall.args[0].includes('Failed to set subservice metrics'), 'Correct error message logged')
293
+ loggerErrorStub.restore()
294
+ test.end()
295
+ })
296
+ metricsTest.end()
297
+ })
175
298
  healthCheckTest.end()
176
299
  })