@samanbayaka/core 0.0.1
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/README.md +812 -0
- package/commit-hash.mjs +1 -0
- package/helper/mol-built-in/AjvValidator.mjs +90 -0
- package/helper/mol-built-in/CustomLogger.mjs +83 -0
- package/helper/mol-built-in/HybridCacher.mjs +155 -0
- package/helper/utility/access-token-validator.mjs +249 -0
- package/helper/utility/aux-broker-params-validator.mjs +126 -0
- package/helper/utility/check-syntax.mjs +91 -0
- package/helper/utility/config-handler.mjs +161 -0
- package/helper/utility/error-handler.mjs +456 -0
- package/helper/utility/file-handler.mjs +121 -0
- package/helper/utility/global-configs-validator.mjs +47 -0
- package/helper/utility/openapi-to-mol-params.mjs +84 -0
- package/helper/utility/sign-jwt.mjs +35 -0
- package/helper/utility/telemetry.mjs +129 -0
- package/index.mjs +427 -0
- package/package.json +78 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fileURLToPath, pathToFileURL } from "url"
|
|
2
|
+
import * as path from "path"
|
|
3
|
+
import * as fs from "fs"
|
|
4
|
+
import os from "os"
|
|
5
|
+
import { createRequire } from 'module'
|
|
6
|
+
import chokidar from "chokidar"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Project absolute path
|
|
16
|
+
*/
|
|
17
|
+
export const ABSOLUTE_PATH = path.join(__filename.split('/samanbayaka/')[0], "samanbayaka")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Demo services directory
|
|
22
|
+
* @type {path}
|
|
23
|
+
*/
|
|
24
|
+
export const DEMO_SERVICES_DIR = path.join(ABSOLUTE_PATH, 'services', 'demo')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Feature services absolute path
|
|
29
|
+
*/
|
|
30
|
+
const SERVICES_DIR = process.cwd()
|
|
31
|
+
// /node_modules/.test(__dirname)
|
|
32
|
+
// ? __dirname.split('node_modules')[0]
|
|
33
|
+
// : __dirname.replace("/helper/utility","")
|
|
34
|
+
|
|
35
|
+
console.log("=================================", process.cwd())
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Assets path
|
|
39
|
+
*/
|
|
40
|
+
export const absoluteAssetPath = (dirName) => {
|
|
41
|
+
return path
|
|
42
|
+
.join(
|
|
43
|
+
ABSOLUTE_PATH,
|
|
44
|
+
dirName,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read package.json file and return object
|
|
51
|
+
* @param {strig} filePath
|
|
52
|
+
* @return {object} package object
|
|
53
|
+
*/
|
|
54
|
+
const readPkg = (filePath) => {
|
|
55
|
+
return JSON.parse(fs
|
|
56
|
+
.readFileSync(
|
|
57
|
+
path
|
|
58
|
+
.join(filePath, "package.json"),
|
|
59
|
+
"utf8"
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Read package.json of services and convert to object
|
|
67
|
+
* @type {object}
|
|
68
|
+
*/
|
|
69
|
+
const pkgService = readPkg(SERVICES_DIR)
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Getting service name
|
|
73
|
+
*/
|
|
74
|
+
export const serviceDtls = {
|
|
75
|
+
name: pkgService.name,
|
|
76
|
+
version: pkgService.version,
|
|
77
|
+
id: [pkgService.name, os.hostname(), process.pid].join("-"),
|
|
78
|
+
osPid: [os.hostname(), process.pid].join("-")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read package.json of samanbayaka and convert to object
|
|
84
|
+
* @type {object}
|
|
85
|
+
*/
|
|
86
|
+
const pkgMain = readPkg(ABSOLUTE_PATH)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create and object of name and version
|
|
91
|
+
* @type {Object}
|
|
92
|
+
*/
|
|
93
|
+
export const pkgInfo = {
|
|
94
|
+
name: pkgMain.name.split('-')[0]
|
|
95
|
+
.split(" ")
|
|
96
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
97
|
+
.join(" "),
|
|
98
|
+
fullName: pkgMain.name,
|
|
99
|
+
version: pkgMain.version
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* File chage watcher in the service directory
|
|
105
|
+
*/
|
|
106
|
+
export const createFileWatcher = () => {
|
|
107
|
+
return chokidar.watch(SERVICES_DIR, {
|
|
108
|
+
ignoreInitial: true,
|
|
109
|
+
awaitWriteFinish: {
|
|
110
|
+
stabilityThreshold: 2000, // wait 2000ms after last change
|
|
111
|
+
pollInterval: 50
|
|
112
|
+
},
|
|
113
|
+
ignored: [
|
|
114
|
+
/(^|[\/\\])\../,
|
|
115
|
+
/(^|[\/\\])node_modules([\/\\]|$)/,
|
|
116
|
+
/~$/,
|
|
117
|
+
/\.swp$/,
|
|
118
|
+
/\.tmp$/
|
|
119
|
+
]
|
|
120
|
+
})
|
|
121
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as errHdl from "./error-handler.mjs"
|
|
2
|
+
|
|
3
|
+
export const validateGlobalConfigs = async(prefix) => {
|
|
4
|
+
try{
|
|
5
|
+
const nodeVer = errHdl.checkMinNoveVer()
|
|
6
|
+
const configHdl = await import("./config-handler.mjs")
|
|
7
|
+
const { serviceDtls, createFileWatcher } = await import("#hUti/file-handler.mjs")
|
|
8
|
+
const globalConfigs = await configHdl.getConfigAll(prefix)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const isServiceRemoved = (((await configHdl.getConfigs( `/config/sbk/members/${serviceDtls.name}` )) || "false") == "false")
|
|
12
|
+
if ( serviceDtls.name != "@samanbayaka/core" && isServiceRemoved ) {
|
|
13
|
+
throw new Error (`The service "${serviceDtls.name}" is disallowed by CONFIGADMIN.`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
nodeVer,
|
|
18
|
+
configCred: configHdl.etcdCred,
|
|
19
|
+
maxL1TTL: errHdl.validateL1TTL(globalConfigs?.MAX_L1_TTL),
|
|
20
|
+
maxL2TTL: errHdl.validateL2TTL(globalConfigs?.MAX_L2_TTL),
|
|
21
|
+
logLevel: errHdl.validateLogLevel(globalConfigs?.LOG_LEVEL),
|
|
22
|
+
telemetry: errHdl.validateTelemetry(globalConfigs?.TELEMETRY),
|
|
23
|
+
configRevision: errHdl.validateConfRevision(globalConfigs?.REVISION),
|
|
24
|
+
serviceDtls,
|
|
25
|
+
createFileWatcher,
|
|
26
|
+
hostEntry: {
|
|
27
|
+
nats: await errHdl.validateHostEntry('nats'),
|
|
28
|
+
redis: await errHdl.validateHostEntry('redis'),
|
|
29
|
+
etcd: await errHdl.validateHostEntry('etcd'),
|
|
30
|
+
redpanda: await errHdl.validateHostEntry('redpanda', true),
|
|
31
|
+
openobserve: await errHdl.validateHostEntry('openobserve', true),
|
|
32
|
+
},
|
|
33
|
+
namespaceTxt: errHdl.validateNamespace(globalConfigs.APP_ENV),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
catch(err){
|
|
37
|
+
// console.error("\x1b[31m%s\x1b[0m", ` ❖❖ SBKERR ❖❖ \n ${errHdl.extractFileLines(err.stack)}`)
|
|
38
|
+
console.error("\x1b[31m%s\x1b[0m", ` ❖❖ SBKERR ❖❖ ${err.message}`)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate the Samanbayaka global configurations
|
|
46
|
+
*/
|
|
47
|
+
export const SBK_GLOBAL_CONFIGS = await validateGlobalConfigs("/config/sbk/global/envs/")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Moleculer params definitions from the OpenAPI schema to enable request parameter validation using AJV
|
|
3
|
+
* @param {object} schema
|
|
4
|
+
*/
|
|
5
|
+
export const openApiToMoleculerParams = (schema) => {
|
|
6
|
+
if( schema?.openapi?.parameters || schema?.openapi?.requestBody ){
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Moleculer params schema for request validation
|
|
10
|
+
* @type {Object}
|
|
11
|
+
*/
|
|
12
|
+
schema.params = {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
params: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
},
|
|
19
|
+
required: [],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
},
|
|
22
|
+
query: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
|
|
26
|
+
},
|
|
27
|
+
required: [],
|
|
28
|
+
},
|
|
29
|
+
body: {}
|
|
30
|
+
},
|
|
31
|
+
required:[],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enforce the presence of a request body in the request based on OpenAPI schema definition
|
|
37
|
+
*/
|
|
38
|
+
if( schema?.openapi?.requestBody?.required ){
|
|
39
|
+
schema.params.required.push("body")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate request body params from the OpenAPI schema definition.
|
|
44
|
+
*/
|
|
45
|
+
if( schema?.openapi?.requestBody && schema?.openapi?.requestBody?.content ){
|
|
46
|
+
schema.params.properties.body.anyOf = Object.entries(schema?.openapi?.requestBody?.content)
|
|
47
|
+
.map(el=>el[1].schema)
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate request path & query params from the OpenAPI schema definition.
|
|
53
|
+
*/
|
|
54
|
+
if( schema?.openapi?.parameters ) {
|
|
55
|
+
schema.params.properties.params.properties = schema?.openapi?.parameters
|
|
56
|
+
.filter(el=> el.in == "path")
|
|
57
|
+
.reduce((obj, item) => {
|
|
58
|
+
obj[item.name] = item.schema
|
|
59
|
+
return obj
|
|
60
|
+
}, {})
|
|
61
|
+
schema.params.properties.params.required = Object.keys(schema.params.properties.params.properties)
|
|
62
|
+
|
|
63
|
+
if( schema.params.properties.params.required.length > 0 ){
|
|
64
|
+
schema.params.required.push("params")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
schema.params.properties.query.properties = schema?.openapi?.parameters
|
|
68
|
+
.filter(el=> el.in == "query").reduce((obj, item) => {
|
|
69
|
+
obj[item.name] = item.schema
|
|
70
|
+
return obj
|
|
71
|
+
}, {})
|
|
72
|
+
|
|
73
|
+
if( Object.keys(schema.params.properties.query.properties).length > 0 ){
|
|
74
|
+
schema.params.required.push("query")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
schema.params.properties.query.required = schema?.openapi?.parameters
|
|
78
|
+
.filter(el=> el.in == "query")
|
|
79
|
+
.filter(el => el.required)
|
|
80
|
+
.map(el => el.name )
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createPublicKey, randomUUID } from "node:crypto"
|
|
2
|
+
import { v5 as uuidv5 } from "uuid"
|
|
3
|
+
import jwt from "jsonwebtoken"
|
|
4
|
+
|
|
5
|
+
import * as configHdl from '#hUti/config-handler.mjs'
|
|
6
|
+
|
|
7
|
+
const privateKey = await configHdl.getConfigs(`/config/sbk/sys/certs/SERVER_KEY`)
|
|
8
|
+
const cert = await configHdl.getConfigs(`/config/sbk/sys/certs/SERVER_CRT`)
|
|
9
|
+
const publicKey = createPublicKey(cert).export({
|
|
10
|
+
type: "spki",
|
|
11
|
+
format: "pem",
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const MY_NAMESPACE = randomUUID()
|
|
15
|
+
const sub = uuidv5("samanbayaka", MY_NAMESPACE)
|
|
16
|
+
const typ = "Bearer"
|
|
17
|
+
|
|
18
|
+
export const signJWT = (data, expiresIn = "30s") => {
|
|
19
|
+
return jwt.sign(
|
|
20
|
+
{
|
|
21
|
+
sub,
|
|
22
|
+
typ,
|
|
23
|
+
data,
|
|
24
|
+
},
|
|
25
|
+
privateKey,
|
|
26
|
+
{
|
|
27
|
+
algorithm: 'RS256',
|
|
28
|
+
expiresIn
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const verifyJWT = (token) => {
|
|
34
|
+
return jwt.verify(token, publicKey)
|
|
35
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node"
|
|
2
|
+
import { resourceFromAttributes } from "@opentelemetry/resources"
|
|
3
|
+
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions"
|
|
4
|
+
|
|
5
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
|
|
6
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"
|
|
7
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"
|
|
8
|
+
import { context, trace, metrics, SpanStatusCode } from "@opentelemetry/api"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const resource = resourceFromAttributes({
|
|
12
|
+
[SemanticResourceAttributes.SERVICE_NAME]: "moleculer-sbk"
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// import { ConsoleSpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
|
|
17
|
+
// export const sdk = new NodeSDK({
|
|
18
|
+
// spanProcessors: [
|
|
19
|
+
// new SimpleSpanProcessor(new ConsoleSpanExporter())
|
|
20
|
+
// ]
|
|
21
|
+
|
|
22
|
+
// })
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export const createOpenTelemetryExporters = (config) => {
|
|
26
|
+
const sdk = new NodeSDK({
|
|
27
|
+
resource,
|
|
28
|
+
|
|
29
|
+
traceExporter: new OTLPTraceExporter({
|
|
30
|
+
url: `http://${config.host}:${config.port}/api/default/v1/traces`,
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: config.token
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
metricReaders: [new PeriodicExportingMetricReader({
|
|
37
|
+
exporter: new OTLPMetricExporter({
|
|
38
|
+
url: `http://${config.host}:${config.port}/api/default/v1/metrics`,
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: config.token
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
exportIntervalMillis: 5000,
|
|
44
|
+
exportTimeoutMillis: 5000
|
|
45
|
+
})],
|
|
46
|
+
|
|
47
|
+
autoGeneratePropagators: true, // Recommended
|
|
48
|
+
|
|
49
|
+
})
|
|
50
|
+
sdk.start()
|
|
51
|
+
return sdk
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const tracer = trace.getTracer("moleculer")
|
|
56
|
+
|
|
57
|
+
export const OpenTelemetryMW = (isEnabled) => {
|
|
58
|
+
const meter = metrics.getMeter("moleculer")
|
|
59
|
+
|
|
60
|
+
const requestCounter = meter.createCounter("moleculer_requests_total", {
|
|
61
|
+
description: "Total number of requests"
|
|
62
|
+
})
|
|
63
|
+
const errorCounter = meter.createCounter("moleculer_errors_total", {
|
|
64
|
+
description: "Total number of errors"
|
|
65
|
+
})
|
|
66
|
+
const durationHistogram = meter.createHistogram("moleculer_request_duration_ms", {
|
|
67
|
+
description: "Request duration in ms"
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
name: "otel",
|
|
72
|
+
|
|
73
|
+
localAction(next, action) {
|
|
74
|
+
if ( !isEnabled ) return next
|
|
75
|
+
return async (ctx) => {
|
|
76
|
+
const startTime = Date.now()
|
|
77
|
+
|
|
78
|
+
const span = tracer.startSpan(`action:${action.name}`, {
|
|
79
|
+
attributes: {
|
|
80
|
+
"moleculer.service": action.service.name,
|
|
81
|
+
"moleculer.action": action.name,
|
|
82
|
+
"moleculer.nodeID": ctx.nodeID
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return context.with(trace.setSpan(context.active(), span), async () => {
|
|
87
|
+
try {
|
|
88
|
+
const result = await next(ctx)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Metrics
|
|
92
|
+
*/
|
|
93
|
+
requestCounter.add(1, {
|
|
94
|
+
action: action.name,
|
|
95
|
+
service: action.service.name
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
durationHistogram.record(Date.now() - startTime, {
|
|
99
|
+
action: action.name
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
} catch (err) {
|
|
106
|
+
/**
|
|
107
|
+
* Metrics
|
|
108
|
+
*/
|
|
109
|
+
errorCounter.add(1, {
|
|
110
|
+
action: action.name,
|
|
111
|
+
service: action.service.name
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
span.recordException(err)
|
|
115
|
+
span.setStatus({
|
|
116
|
+
code: SpanStatusCode.ERROR,
|
|
117
|
+
message: err.message
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
throw err
|
|
121
|
+
|
|
122
|
+
} finally {
|
|
123
|
+
span.end()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|