@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 +14 -0
- package/audit-ci.jsonc +0 -1
- package/package.json +6 -6
- package/src/healthCheck/HealthCheck.js +40 -0
- package/test/unit/healthCheck/HealthCheck.test.js +123 -0
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/central-services-shared",
|
|
3
|
-
"version": "18.
|
|
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.
|
|
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.
|
|
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.
|
|
100
|
-
"@mojaloop/event-sdk": "14.
|
|
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.
|
|
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
|
})
|