@mojaloop/central-services-shared 18.35.2 → 18.36.0-snapshot.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/package.json +7 -4
- package/src/config.js +7 -0
- package/src/index.d.ts +23 -4
- package/src/util/otelDto.js +53 -0
- package/src/util/request.js +81 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/central-services-shared",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.36.0-snapshot.0",
|
|
4
4
|
"description": "Shared code for mojaloop central services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "ModusBox",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"test:endpoints": "npx tape 'test/unit/util/endpoints.test.js'",
|
|
49
49
|
"test:mysql": "npx tape 'test/unit/mysql/**/*.test.js'",
|
|
50
50
|
"test:participants": "npx tape 'test/unit/util/participants.test.js'",
|
|
51
|
+
"test:request": "npx tape 'test/unit/util/request.test.js'",
|
|
51
52
|
"test:trans": "npx tape 'test/unit/util/headers/transformer.test.js'",
|
|
52
53
|
"test:unit": "npx tape 'test/unit/**/*.test.js' | tap-spec",
|
|
53
54
|
"test:xunit": "npx tape 'test/unit/**/**.test.js' | tap-xunit > ./test/results/xunit.xml",
|
|
@@ -71,12 +72,13 @@
|
|
|
71
72
|
"@hapi/joi-date": "2.0.1",
|
|
72
73
|
"@mojaloop/inter-scheme-proxy-cache-lib": "2.9.0",
|
|
73
74
|
"@opentelemetry/api": "1.9.0",
|
|
75
|
+
"@opentelemetry/semantic-conventions": "1.39.0",
|
|
74
76
|
"async-exit-hook": "2.0.1",
|
|
75
77
|
"async-retry": "1.3.3",
|
|
76
78
|
"axios": "1.13.4",
|
|
77
79
|
"clone": "2.1.2",
|
|
78
80
|
"convict": "^6.2.4",
|
|
79
|
-
"dotenv": "17.2.
|
|
81
|
+
"dotenv": "17.2.4",
|
|
80
82
|
"env-var": "7.5.0",
|
|
81
83
|
"event-stream": "4.0.1",
|
|
82
84
|
"fast-safe-stringify": "2.1.1",
|
|
@@ -100,7 +102,7 @@
|
|
|
100
102
|
"@mojaloop/central-services-logger": "11.10.3",
|
|
101
103
|
"@mojaloop/central-services-metrics": "12.8.3",
|
|
102
104
|
"@mojaloop/event-sdk": "14.8.2",
|
|
103
|
-
"@mojaloop/sdk-standard-components": "19.18.
|
|
105
|
+
"@mojaloop/sdk-standard-components": "19.18.7",
|
|
104
106
|
"@opentelemetry/auto-instrumentations-node": "^0.69.0",
|
|
105
107
|
"@types/hapi__joi": "17.1.15",
|
|
106
108
|
"ajv": "^8.17.1",
|
|
@@ -152,7 +154,8 @@
|
|
|
152
154
|
"validator": "13.15.22",
|
|
153
155
|
"lodash": "4.17.23",
|
|
154
156
|
"lodash-es": "4.17.23",
|
|
155
|
-
"undici": "7.18.2"
|
|
157
|
+
"undici": "7.18.2",
|
|
158
|
+
"fast-xml-parser": "5.3.4"
|
|
156
159
|
},
|
|
157
160
|
"peerDependencies": {
|
|
158
161
|
"@mojaloop/central-services-error-handling": "13.x.x",
|
package/src/config.js
CHANGED
|
@@ -9,6 +9,13 @@ const config = convict({
|
|
|
9
9
|
env: 'SHARED_CACHE_LOG_LEVEL'
|
|
10
10
|
},
|
|
11
11
|
|
|
12
|
+
httpLogLevel: {
|
|
13
|
+
doc: 'Log level for HTTP wrapper.',
|
|
14
|
+
format: logLevelValues,
|
|
15
|
+
default: logLevelsMap.warn,
|
|
16
|
+
env: 'LOG_LEVEL_HTTP'
|
|
17
|
+
},
|
|
18
|
+
|
|
12
19
|
defaultTtlSec: {
|
|
13
20
|
doc: 'Default cache TTL.',
|
|
14
21
|
format: Number,
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Utils as HapiUtil, Server } from '@hapi/hapi'
|
|
2
2
|
import { ILogger } from '@mojaloop/central-services-logger/src/contextLogger'
|
|
3
3
|
import { Knex } from 'knex';
|
|
4
|
+
import { AxiosRequestConfig, AxiosResponse, ResponseType as AxiosResponseType } from 'axios'
|
|
4
5
|
import IORedis from 'ioredis';
|
|
5
6
|
|
|
6
7
|
declare namespace CentralServicesShared {
|
|
@@ -656,9 +657,27 @@ declare namespace CentralServicesShared {
|
|
|
656
657
|
accept: string
|
|
657
658
|
}
|
|
658
659
|
|
|
659
|
-
type RequestParams = {
|
|
660
|
-
|
|
661
|
-
|
|
660
|
+
type RequestParams = {
|
|
661
|
+
url: string,
|
|
662
|
+
headers: HapiUtil.Dictionary<string>,
|
|
663
|
+
source: string,
|
|
664
|
+
destination?: string,
|
|
665
|
+
hubNameRegex: RegExp,
|
|
666
|
+
method?: RestMethodsEnum,
|
|
667
|
+
payload?: any,
|
|
668
|
+
params?: AxiosRequestConfig['params'],
|
|
669
|
+
responseType?: AxiosResponseType,
|
|
670
|
+
span?: any,
|
|
671
|
+
jwsSigner?: any,
|
|
672
|
+
protocolVersions?: ProtocolVersionsType,
|
|
673
|
+
apiType?: ApiTypeValues,
|
|
674
|
+
axiosRequestOptionsOverride?: Partial<AxiosRequestConfig>,
|
|
675
|
+
logger?: ILogger,
|
|
676
|
+
peerService?: string
|
|
677
|
+
}
|
|
678
|
+
export interface Request {
|
|
679
|
+
sendRequest(params: RequestParams): Promise<AxiosResponse>
|
|
680
|
+
sendBaseRequest(params?: AxiosRequestConfig & { logger?: ILogger, peerService?: string }): Promise<AxiosResponse>
|
|
662
681
|
}
|
|
663
682
|
|
|
664
683
|
interface Kafka {
|
|
@@ -788,7 +807,7 @@ declare namespace CentralServicesShared {
|
|
|
788
807
|
RedisCache: RedisCache;
|
|
789
808
|
}
|
|
790
809
|
|
|
791
|
-
type RedisInstanceConfig =
|
|
810
|
+
type RedisInstanceConfig =
|
|
792
811
|
| {
|
|
793
812
|
type: 'redis';
|
|
794
813
|
host: string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2020-2025 Mojaloop Foundation
|
|
5
|
+
The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
10
|
+
|
|
11
|
+
Contributors
|
|
12
|
+
--------------
|
|
13
|
+
This is the official list of the Mojaloop project contributors for this file.
|
|
14
|
+
Names of the original copyright holders (individuals or organizations)
|
|
15
|
+
should be listed with a '*' in the first column. People who have
|
|
16
|
+
contributed from an organization can be listed under the organization
|
|
17
|
+
that actually holds the copyright for their contributions (see the
|
|
18
|
+
Mojaloop Foundation for an example). Those individuals should have
|
|
19
|
+
their names indented and be marked with a '-'. Email address can be added
|
|
20
|
+
optionally within square brackets <email>.
|
|
21
|
+
|
|
22
|
+
* Mojaloop Foundation
|
|
23
|
+
* Eugen Klymniuk <eugen.klymniuk@infitx.com>
|
|
24
|
+
|
|
25
|
+
--------------
|
|
26
|
+
******/
|
|
27
|
+
/* istanbul ignore file */
|
|
28
|
+
|
|
29
|
+
const otel = require('@opentelemetry/semantic-conventions')
|
|
30
|
+
|
|
31
|
+
const ATTR_SERVICE_PEER_NAME = 'service.peer.name' // using string literal because ATTR_SERVICE_PEER_NAME is only available in @opentelemetry/semantic-conventions/incubating as of now
|
|
32
|
+
|
|
33
|
+
const outgoingRequestDto = ({
|
|
34
|
+
method, url, durationSec, statusCode, errorType, peerService
|
|
35
|
+
}) => ({
|
|
36
|
+
attributes: {
|
|
37
|
+
[otel.ATTR_HTTP_REQUEST_METHOD]: method,
|
|
38
|
+
[otel.ATTR_URL_FULL]: url,
|
|
39
|
+
[otel.METRIC_HTTP_CLIENT_REQUEST_DURATION]: durationSec, // 'duration.ms' is a custom attribute
|
|
40
|
+
...(statusCode && { [otel.ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode }),
|
|
41
|
+
...(errorType && { [otel.ATTR_ERROR_TYPE]: errorType }),
|
|
42
|
+
...(peerService && { [ATTR_SERVICE_PEER_NAME]: peerService })
|
|
43
|
+
// peerService - logical service name, must be explicitly provided by caller (not derived from URL hostname)
|
|
44
|
+
// think if we should extract it for internal http://... calls from url hostname
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const incomingRequestDto = () => ({ attributes: {} }) // todo: add impl.
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
outgoingRequestDto,
|
|
52
|
+
incomingRequestDto
|
|
53
|
+
}
|
package/src/util/request.js
CHANGED
|
@@ -30,29 +30,31 @@
|
|
|
30
30
|
'use strict'
|
|
31
31
|
|
|
32
32
|
const http = require('node:http')
|
|
33
|
-
const
|
|
33
|
+
const axios = require('axios')
|
|
34
34
|
const stringify = require('fast-safe-stringify')
|
|
35
35
|
const EventSdk = require('@mojaloop/event-sdk')
|
|
36
36
|
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
37
37
|
const Metrics = require('@mojaloop/central-services-metrics')
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const { logger } = require('../logger')
|
|
38
|
+
|
|
39
|
+
const { logger: globalLogger } = require('../logger')
|
|
41
40
|
const { API_TYPES } = require('../constants')
|
|
42
41
|
const config = require('../config')
|
|
42
|
+
const enums = require('../enums')
|
|
43
|
+
const Headers = require('./headers/transformer')
|
|
44
|
+
const { outgoingRequestDto } = require('./otelDto')
|
|
43
45
|
|
|
44
46
|
const MISSING_FUNCTION_PARAMETERS = 'Missing parameters for function'
|
|
45
47
|
|
|
46
48
|
// Delete the default headers that the `axios` module inserts as they can brake our conventions.
|
|
47
49
|
// By default it would insert `"Accept":"application/json, text/plain, */*"`.
|
|
48
|
-
delete
|
|
50
|
+
delete axios.defaults.headers.common.Accept
|
|
49
51
|
|
|
50
52
|
const keepAlive = (process.env.HTTP_AGENT_KEEP_ALIVE ?? 'true') === 'true'
|
|
51
|
-
|
|
53
|
+
globalLogger.verbose('http keepAlive:', { keepAlive })
|
|
52
54
|
|
|
53
55
|
// Enable keepalive for http
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
axios.defaults.httpAgent = new http.Agent({ keepAlive })
|
|
57
|
+
axios.defaults.httpAgent.toJSON = () => ({})
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* @function sendRequest
|
|
@@ -77,11 +79,12 @@ request.defaults.httpAgent.toJSON = () => ({})
|
|
|
77
79
|
* @param {SendRequestProtocolVersions | undefined} protocolVersions the config for Protocol versions to be used
|
|
78
80
|
* @param {'fspiop' | 'iso20022'} apiType the API type of the request being sent
|
|
79
81
|
* @param {object} axiosRequestOptionsOverride axios request options to override https://axios-http.com/docs/req_config
|
|
82
|
+
* @param {ILogger} [logger] ContextLogger instance with specific context
|
|
83
|
+
* @param {string} [peerService] Logical service name to call (for OTel)
|
|
80
84
|
* @param {regex} hubNameRegex hubName Regex
|
|
81
85
|
*
|
|
82
86
|
*@return {Promise<any>} The response for the request being sent or error object with response included
|
|
83
87
|
*/
|
|
84
|
-
|
|
85
88
|
const sendRequest = async ({
|
|
86
89
|
url,
|
|
87
90
|
headers,
|
|
@@ -96,6 +99,8 @@ const sendRequest = async ({
|
|
|
96
99
|
protocolVersions = undefined,
|
|
97
100
|
apiType = API_TYPES.fspiop,
|
|
98
101
|
axiosRequestOptionsOverride = {},
|
|
102
|
+
logger = createHttpLogger(),
|
|
103
|
+
peerService = '',
|
|
99
104
|
hubNameRegex
|
|
100
105
|
}) => {
|
|
101
106
|
const histTimerEnd = Metrics.getHistogram(
|
|
@@ -113,6 +118,9 @@ const sendRequest = async ({
|
|
|
113
118
|
// think, if we can just avoid checking "destination"
|
|
114
119
|
throw ErrorHandler.Factory.createInternalServerFSPIOPError(MISSING_FUNCTION_PARAMETERS)
|
|
115
120
|
}
|
|
121
|
+
|
|
122
|
+
const log = logger.child({ component: 'httpRequest' })
|
|
123
|
+
|
|
116
124
|
try {
|
|
117
125
|
const transformedHeaders = Headers.transformHeaders(headers, {
|
|
118
126
|
httpMethod: method,
|
|
@@ -126,9 +134,10 @@ const sendRequest = async ({
|
|
|
126
134
|
url,
|
|
127
135
|
method,
|
|
128
136
|
headers: transformedHeaders,
|
|
129
|
-
data: payload,
|
|
137
|
+
data: payload,
|
|
130
138
|
params,
|
|
131
139
|
responseType,
|
|
140
|
+
peerService,
|
|
132
141
|
timeout: config.get('httpRequestTimeoutMs'),
|
|
133
142
|
...axiosRequestOptionsOverride
|
|
134
143
|
}
|
|
@@ -149,14 +158,18 @@ const sendRequest = async ({
|
|
|
149
158
|
}
|
|
150
159
|
span.audit({ ...rest, payload }, EventSdk.AuditEventAction.egress)
|
|
151
160
|
}
|
|
152
|
-
|
|
153
|
-
const response = await
|
|
161
|
+
|
|
162
|
+
const response = await sendBaseRequest({
|
|
163
|
+
...requestOptions,
|
|
164
|
+
logger: log,
|
|
165
|
+
peerService
|
|
166
|
+
})
|
|
154
167
|
|
|
155
168
|
!!sendRequestSpan && await sendRequestSpan.finish()
|
|
156
169
|
histTimerEnd({ success: true, source, destination, method })
|
|
157
170
|
return response
|
|
158
171
|
} catch (error) {
|
|
159
|
-
|
|
172
|
+
log.error('error in request.sendRequest:', {
|
|
160
173
|
code: error.code,
|
|
161
174
|
message: error.message,
|
|
162
175
|
stack: error.stack,
|
|
@@ -247,6 +260,60 @@ const sendRequest = async ({
|
|
|
247
260
|
}
|
|
248
261
|
}
|
|
249
262
|
|
|
263
|
+
// todo: think better name
|
|
264
|
+
// it's for http calls without params validation and transformHeaders
|
|
265
|
+
const sendBaseRequest = async ({
|
|
266
|
+
logger = createHttpLogger(),
|
|
267
|
+
peerService = '',
|
|
268
|
+
...reqOptions
|
|
269
|
+
} = {}) => {
|
|
270
|
+
const log = logger.child({ component: 'sendBaseRequest' })
|
|
271
|
+
const { method, url } = reqOptions
|
|
272
|
+
const methodUrl = `${method?.toUpperCase()} ${url}`
|
|
273
|
+
const startTime = Date.now()
|
|
274
|
+
let statusCode
|
|
275
|
+
let errorType
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
log.debug(`[-->] options for ${methodUrl}: `, { reqOptions })
|
|
279
|
+
|
|
280
|
+
const response = await axios(reqOptions)
|
|
281
|
+
|
|
282
|
+
statusCode = response?.status
|
|
283
|
+
log.verbose(`[<--] details of ${methodUrl}: `, {
|
|
284
|
+
data: response?.data,
|
|
285
|
+
headers: response?.headers, // todo: extract only needed headers
|
|
286
|
+
statusCode,
|
|
287
|
+
reqOptions
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return response
|
|
291
|
+
} catch (error) {
|
|
292
|
+
statusCode = error.response?.status
|
|
293
|
+
errorType = error.code
|
|
294
|
+
throw error // todo: think, if we need to rethrow our custom error here
|
|
295
|
+
} finally {
|
|
296
|
+
const severity = typeof statusCode === 'number'
|
|
297
|
+
? (statusCode >= 200 && statusCode < 300 ? 'info' : 'warn')
|
|
298
|
+
: 'error'
|
|
299
|
+
log[severity](`[<--] ${methodUrl} [${statusCode || errorType}]: `, outgoingRequestDto({
|
|
300
|
+
method,
|
|
301
|
+
url,
|
|
302
|
+
statusCode,
|
|
303
|
+
durationSec: (Date.now() - startTime) / 1000,
|
|
304
|
+
errorType,
|
|
305
|
+
peerService
|
|
306
|
+
}))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const createHttpLogger = () => {
|
|
311
|
+
const logger = globalLogger.child()
|
|
312
|
+
logger.setLevel(config.get('httpLogLevel'))
|
|
313
|
+
return logger
|
|
314
|
+
}
|
|
315
|
+
|
|
250
316
|
module.exports = {
|
|
251
|
-
sendRequest
|
|
317
|
+
sendRequest,
|
|
318
|
+
sendBaseRequest
|
|
252
319
|
}
|