@samuelaj1/eazisend-service-kit 1.0.6 → 1.0.8
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/dist/chunk-TS72ONWN.js +97 -0
- package/dist/index.cjs +297 -0
- package/dist/index.d.cts +81 -2
- package/dist/index.d.ts +81 -2
- package/dist/index.js +205 -0
- package/dist/middlewares/monitoring.middleware.cjs +121 -0
- package/dist/middlewares/monitoring.middleware.d.cts +15 -0
- package/dist/middlewares/monitoring.middleware.d.ts +15 -0
- package/dist/middlewares/monitoring.middleware.js +6 -0
- package/package.json +23 -10
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// src/middlewares/monitoring.middleware.ts
|
|
2
|
+
import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from "prom-client";
|
|
3
|
+
var applications = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
var methods = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE"]);
|
|
5
|
+
function routeLabel(request) {
|
|
6
|
+
const path = request.route?.path;
|
|
7
|
+
if (typeof path === "string") return path;
|
|
8
|
+
if (Array.isArray(path) && path.every((value) => typeof value === "string")) {
|
|
9
|
+
return path.join("|");
|
|
10
|
+
}
|
|
11
|
+
return path instanceof RegExp ? "__regexp__" : "__unmatched__";
|
|
12
|
+
}
|
|
13
|
+
function setupMetrics(app, options) {
|
|
14
|
+
if (typeof options?.serviceName !== "string" || !options.serviceName.trim()) {
|
|
15
|
+
throw new Error("Metrics serviceName must be a non-empty string");
|
|
16
|
+
}
|
|
17
|
+
const existing = applications.get(app);
|
|
18
|
+
if (existing) {
|
|
19
|
+
if (existing.serviceName !== options.serviceName) {
|
|
20
|
+
throw new Error("Metrics already initialized with a different serviceName");
|
|
21
|
+
}
|
|
22
|
+
return existing.registry;
|
|
23
|
+
}
|
|
24
|
+
const registry = new Registry();
|
|
25
|
+
registry.setDefaultLabels({ service: options.serviceName });
|
|
26
|
+
collectDefaultMetrics({ register: registry });
|
|
27
|
+
const labelNames = ["method", "route", "status_code"];
|
|
28
|
+
const requests = new Counter({
|
|
29
|
+
name: "http_requests_total",
|
|
30
|
+
help: "Completed HTTP requests",
|
|
31
|
+
labelNames,
|
|
32
|
+
registers: [registry]
|
|
33
|
+
});
|
|
34
|
+
const duration = new Histogram({
|
|
35
|
+
name: "http_request_duration_seconds",
|
|
36
|
+
help: "HTTP request duration in seconds",
|
|
37
|
+
labelNames,
|
|
38
|
+
registers: [registry],
|
|
39
|
+
buckets: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
40
|
+
});
|
|
41
|
+
const errors = new Counter({
|
|
42
|
+
name: "http_request_errors_total",
|
|
43
|
+
help: "Completed HTTP requests with 4xx or 5xx status",
|
|
44
|
+
labelNames,
|
|
45
|
+
registers: [registry]
|
|
46
|
+
});
|
|
47
|
+
const active = new Gauge({
|
|
48
|
+
name: "http_active_requests",
|
|
49
|
+
help: "HTTP requests currently in progress",
|
|
50
|
+
registers: [registry]
|
|
51
|
+
});
|
|
52
|
+
active.set(0);
|
|
53
|
+
const middleware = (request, response, next) => {
|
|
54
|
+
if (/^\/metrics\/?$/i.test(request.path)) return next();
|
|
55
|
+
const started = process.hrtime.bigint();
|
|
56
|
+
active.inc();
|
|
57
|
+
let completed = false;
|
|
58
|
+
const complete = () => {
|
|
59
|
+
if (completed) return;
|
|
60
|
+
completed = true;
|
|
61
|
+
response.removeListener("finish", complete);
|
|
62
|
+
response.removeListener("close", complete);
|
|
63
|
+
active.dec();
|
|
64
|
+
const labels = {
|
|
65
|
+
method: methods.has(request.method) ? request.method : "OTHER",
|
|
66
|
+
route: routeLabel(request),
|
|
67
|
+
// A disconnected response has no completed HTTP status.
|
|
68
|
+
status_code: response.writableFinished ? String(response.statusCode) : "aborted"
|
|
69
|
+
};
|
|
70
|
+
requests.inc(labels);
|
|
71
|
+
duration.observe(labels, Number(process.hrtime.bigint() - started) / 1e9);
|
|
72
|
+
if (response.writableFinished && response.statusCode >= 400 && response.statusCode < 600) {
|
|
73
|
+
errors.inc(labels);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
response.once("finish", complete);
|
|
77
|
+
response.once("close", complete);
|
|
78
|
+
next();
|
|
79
|
+
};
|
|
80
|
+
app.use(middleware);
|
|
81
|
+
app.get("/metrics", async (_request, response, next) => {
|
|
82
|
+
try {
|
|
83
|
+
const metrics = await registry.metrics();
|
|
84
|
+
response.setHeader("Content-Type", registry.contentType);
|
|
85
|
+
response.setHeader("Cache-Control", "no-store");
|
|
86
|
+
response.end(metrics);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
next(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
applications.set(app, { serviceName: options.serviceName, registry });
|
|
92
|
+
return registry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
setupMetrics
|
|
97
|
+
};
|
package/dist/index.cjs
CHANGED
|
@@ -33,10 +33,12 @@ __export(index_exports, {
|
|
|
33
33
|
LogLevelTypes: () => LogLevelTypes,
|
|
34
34
|
elasticSearchRepo: () => elasticSearchRepo,
|
|
35
35
|
loggerRepo: () => loggerRepo,
|
|
36
|
+
rabbitmqRepo: () => rabbitmqRepo,
|
|
36
37
|
registerErrorMiddleware: () => registerErrorMiddleware,
|
|
37
38
|
registerSecurityMiddleware: () => registerSecurityMiddleware,
|
|
38
39
|
registerStandardMiddleware: () => registerStandardMiddleware,
|
|
39
40
|
runServer: () => runServer,
|
|
41
|
+
setupMetrics: () => setupMetrics,
|
|
40
42
|
verifyGatewayRequest: () => verifyGatewayRequest
|
|
41
43
|
});
|
|
42
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -66,10 +68,13 @@ var Config = class {
|
|
|
66
68
|
API_GATEWAY_URL;
|
|
67
69
|
GATEWAY_TOKEN;
|
|
68
70
|
ELASTIC_SEARCH_NODE_URL;
|
|
71
|
+
RABBITMQ_URL;
|
|
69
72
|
PORT;
|
|
70
73
|
constructor() {
|
|
71
74
|
this.API_GATEWAY_URL = process.env.API_GATEWAY_URL || "";
|
|
72
75
|
this.GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "";
|
|
76
|
+
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL || "";
|
|
77
|
+
this.RABBITMQ_URL = process.env.RABBITMQ_URL || "";
|
|
73
78
|
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL?.trim() || "";
|
|
74
79
|
this.PORT = process.env.PORT || "";
|
|
75
80
|
}
|
|
@@ -197,6 +202,202 @@ var ElasticsearchRepo = class {
|
|
|
197
202
|
};
|
|
198
203
|
var elasticSearchRepo = new ElasticsearchRepo();
|
|
199
204
|
|
|
205
|
+
// src/repositories/rabbitmq.repo.ts
|
|
206
|
+
var import_amqplib = __toESM(require("amqplib"), 1);
|
|
207
|
+
var RabbitmqRepo = class {
|
|
208
|
+
queueChannel;
|
|
209
|
+
pendingConnection;
|
|
210
|
+
/**
|
|
211
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
async getQueueConnection() {
|
|
219
|
+
if (this.queueChannel) {
|
|
220
|
+
return this.queueChannel;
|
|
221
|
+
}
|
|
222
|
+
if (this.pendingConnection) {
|
|
223
|
+
return this.pendingConnection;
|
|
224
|
+
}
|
|
225
|
+
this.pendingConnection = this.createQueueConnection();
|
|
226
|
+
try {
|
|
227
|
+
return await this.pendingConnection;
|
|
228
|
+
} finally {
|
|
229
|
+
this.pendingConnection = void 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async createQueueConnection() {
|
|
233
|
+
let connection;
|
|
234
|
+
try {
|
|
235
|
+
connection = await import_amqplib.default.connect(libConfig.RABBITMQ_URL);
|
|
236
|
+
const channel = await connection.createChannel();
|
|
237
|
+
const clearChannel = () => {
|
|
238
|
+
if (this.queueChannel === channel) {
|
|
239
|
+
this.queueChannel = void 0;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
channel.on("error", (error) => {
|
|
243
|
+
clearChannel();
|
|
244
|
+
console.error("RabbitmqRepo channel error:", error);
|
|
245
|
+
});
|
|
246
|
+
channel.once("close", clearChannel);
|
|
247
|
+
connection.on("error", (error) => {
|
|
248
|
+
clearChannel();
|
|
249
|
+
console.error("RabbitmqRepo connection error:", error);
|
|
250
|
+
});
|
|
251
|
+
connection.once("close", clearChannel);
|
|
252
|
+
console.log("RabbitmqRepo connected to queue successfully...");
|
|
253
|
+
this.closeConnection(channel, connection);
|
|
254
|
+
this.queueChannel = channel;
|
|
255
|
+
return channel;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (connection) {
|
|
258
|
+
await connection.close().catch((closeError) => {
|
|
259
|
+
console.error("RabbitmqRepo error closing failed connection:", closeError);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
console.error("RabbitmqRepo error creating connection:", error);
|
|
263
|
+
return void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Closes the channel and connection when the application shuts down.
|
|
268
|
+
*/
|
|
269
|
+
closeConnection(channel, connection) {
|
|
270
|
+
let isClosing = false;
|
|
271
|
+
const close = async (signal) => {
|
|
272
|
+
if (isClosing) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
isClosing = true;
|
|
276
|
+
try {
|
|
277
|
+
try {
|
|
278
|
+
await channel.close();
|
|
279
|
+
} finally {
|
|
280
|
+
await connection.close();
|
|
281
|
+
}
|
|
282
|
+
console.log(`RabbitmqRepo queue connection closed after ${signal}`);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("RabbitmqRepo error closing connection:", error);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
process.once("SIGINT", close);
|
|
288
|
+
process.once("SIGTERM", close);
|
|
289
|
+
connection.once("close", () => {
|
|
290
|
+
process.removeListener("SIGINT", close);
|
|
291
|
+
process.removeListener("SIGTERM", close);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```ts
|
|
299
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
async establishQueueConnection() {
|
|
303
|
+
const channel = await this.getQueueConnection();
|
|
304
|
+
if (!channel) {
|
|
305
|
+
throw new Error("No channel found.");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async connectQueueToExchange(channel, exchange, queueName, routingKey, exchangeType) {
|
|
309
|
+
await channel.assertExchange(exchange, exchangeType);
|
|
310
|
+
const q = await channel.assertQueue(queueName, { durable: true, autoDelete: false });
|
|
311
|
+
await channel.bindQueue(q.queue, exchange, routingKey);
|
|
312
|
+
return q;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* ```ts
|
|
319
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
320
|
+
* exchange: 'notifications',
|
|
321
|
+
* routingKey: 'email',
|
|
322
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
323
|
+
* logMessage: 'Email notification published',
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
async publishMessageToQueue(args) {
|
|
328
|
+
const serializedMessage = JSON.stringify(args.message);
|
|
329
|
+
if (serializedMessage === void 0) {
|
|
330
|
+
throw new Error("Message must be JSON serializable.");
|
|
331
|
+
}
|
|
332
|
+
const channel = await this.getQueueConnection();
|
|
333
|
+
const { exchange, routingKey, logMessage, exchangeType } = args;
|
|
334
|
+
if (!channel) {
|
|
335
|
+
throw new Error("No channel found.");
|
|
336
|
+
}
|
|
337
|
+
await channel.assertExchange(exchange, exchangeType || "direct");
|
|
338
|
+
channel.publish(exchange, routingKey, Buffer.from(serializedMessage));
|
|
339
|
+
if (logMessage) {
|
|
340
|
+
console.log(logMessage);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
345
|
+
* Prefetch defaults to 10 when omitted.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
350
|
+
* exchange: 'notifications',
|
|
351
|
+
* queueName: 'email-queue',
|
|
352
|
+
* routingKey: 'email',
|
|
353
|
+
* prefetch: 5,
|
|
354
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
355
|
+
* // Process the email.
|
|
356
|
+
* },
|
|
357
|
+
* });
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
async consumeMessagesFromQueue(args) {
|
|
361
|
+
const { exchange, queueName, routingKey, callback } = args;
|
|
362
|
+
const prefetch = args.prefetch ?? 10;
|
|
363
|
+
if (!Number.isInteger(prefetch) || prefetch < 1 || prefetch > 65535) {
|
|
364
|
+
throw new Error("Prefetch must be an integer between 1 and 65535.");
|
|
365
|
+
}
|
|
366
|
+
const channel = args.channel ?? await this.getQueueConnection();
|
|
367
|
+
if (!channel) {
|
|
368
|
+
throw new Error("No channel found.");
|
|
369
|
+
}
|
|
370
|
+
const q = await this.connectQueueToExchange(channel, exchange, queueName, routingKey, args.exchangeType ?? "direct");
|
|
371
|
+
await channel.prefetch(prefetch, false);
|
|
372
|
+
await channel.consume(q.queue, async (message) => {
|
|
373
|
+
if (!message) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
let payload;
|
|
378
|
+
try {
|
|
379
|
+
payload = JSON.parse(message.content.toString());
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error("RabbitmqRepo invalid JSON message:", error);
|
|
382
|
+
channel.nack(message, false, false);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await callback(payload);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error("RabbitmqRepo message callback failed:", error);
|
|
389
|
+
channel.nack(message, false, true);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
channel.ack(message);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error("RabbitmqRepo error settling message:", error);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var rabbitmqRepo = new RabbitmqRepo();
|
|
400
|
+
|
|
200
401
|
// src/middlewares/gateway.middleware.ts
|
|
201
402
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
202
403
|
function verifyGatewayRequest(serviceId) {
|
|
@@ -228,14 +429,110 @@ function registerErrorMiddleware(app) {
|
|
|
228
429
|
res.status(import_http_status_codes.StatusCodes.INTERNAL_SERVER_ERROR).json({ error: err.message });
|
|
229
430
|
};
|
|
230
431
|
}
|
|
432
|
+
|
|
433
|
+
// src/middlewares/monitoring.middleware.ts
|
|
434
|
+
var import_prom_client = require("prom-client");
|
|
435
|
+
var applications = /* @__PURE__ */ new WeakMap();
|
|
436
|
+
var methods = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE"]);
|
|
437
|
+
function routeLabel(request) {
|
|
438
|
+
const path = request.route?.path;
|
|
439
|
+
if (typeof path === "string") return path;
|
|
440
|
+
if (Array.isArray(path) && path.every((value) => typeof value === "string")) {
|
|
441
|
+
return path.join("|");
|
|
442
|
+
}
|
|
443
|
+
return path instanceof RegExp ? "__regexp__" : "__unmatched__";
|
|
444
|
+
}
|
|
445
|
+
function setupMetrics(app, options) {
|
|
446
|
+
if (typeof options?.serviceName !== "string" || !options.serviceName.trim()) {
|
|
447
|
+
throw new Error("Metrics serviceName must be a non-empty string");
|
|
448
|
+
}
|
|
449
|
+
const existing = applications.get(app);
|
|
450
|
+
if (existing) {
|
|
451
|
+
if (existing.serviceName !== options.serviceName) {
|
|
452
|
+
throw new Error("Metrics already initialized with a different serviceName");
|
|
453
|
+
}
|
|
454
|
+
return existing.registry;
|
|
455
|
+
}
|
|
456
|
+
const registry = new import_prom_client.Registry();
|
|
457
|
+
registry.setDefaultLabels({ service: options.serviceName });
|
|
458
|
+
(0, import_prom_client.collectDefaultMetrics)({ register: registry });
|
|
459
|
+
const labelNames = ["method", "route", "status_code"];
|
|
460
|
+
const requests = new import_prom_client.Counter({
|
|
461
|
+
name: "http_requests_total",
|
|
462
|
+
help: "Completed HTTP requests",
|
|
463
|
+
labelNames,
|
|
464
|
+
registers: [registry]
|
|
465
|
+
});
|
|
466
|
+
const duration = new import_prom_client.Histogram({
|
|
467
|
+
name: "http_request_duration_seconds",
|
|
468
|
+
help: "HTTP request duration in seconds",
|
|
469
|
+
labelNames,
|
|
470
|
+
registers: [registry],
|
|
471
|
+
buckets: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
472
|
+
});
|
|
473
|
+
const errors = new import_prom_client.Counter({
|
|
474
|
+
name: "http_request_errors_total",
|
|
475
|
+
help: "Completed HTTP requests with 4xx or 5xx status",
|
|
476
|
+
labelNames,
|
|
477
|
+
registers: [registry]
|
|
478
|
+
});
|
|
479
|
+
const active = new import_prom_client.Gauge({
|
|
480
|
+
name: "http_active_requests",
|
|
481
|
+
help: "HTTP requests currently in progress",
|
|
482
|
+
registers: [registry]
|
|
483
|
+
});
|
|
484
|
+
active.set(0);
|
|
485
|
+
const middleware = (request, response, next) => {
|
|
486
|
+
if (/^\/metrics\/?$/i.test(request.path)) return next();
|
|
487
|
+
const started = process.hrtime.bigint();
|
|
488
|
+
active.inc();
|
|
489
|
+
let completed = false;
|
|
490
|
+
const complete = () => {
|
|
491
|
+
if (completed) return;
|
|
492
|
+
completed = true;
|
|
493
|
+
response.removeListener("finish", complete);
|
|
494
|
+
response.removeListener("close", complete);
|
|
495
|
+
active.dec();
|
|
496
|
+
const labels = {
|
|
497
|
+
method: methods.has(request.method) ? request.method : "OTHER",
|
|
498
|
+
route: routeLabel(request),
|
|
499
|
+
// A disconnected response has no completed HTTP status.
|
|
500
|
+
status_code: response.writableFinished ? String(response.statusCode) : "aborted"
|
|
501
|
+
};
|
|
502
|
+
requests.inc(labels);
|
|
503
|
+
duration.observe(labels, Number(process.hrtime.bigint() - started) / 1e9);
|
|
504
|
+
if (response.writableFinished && response.statusCode >= 400 && response.statusCode < 600) {
|
|
505
|
+
errors.inc(labels);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
response.once("finish", complete);
|
|
509
|
+
response.once("close", complete);
|
|
510
|
+
next();
|
|
511
|
+
};
|
|
512
|
+
app.use(middleware);
|
|
513
|
+
app.get("/metrics", async (_request, response, next) => {
|
|
514
|
+
try {
|
|
515
|
+
const metrics = await registry.metrics();
|
|
516
|
+
response.setHeader("Content-Type", registry.contentType);
|
|
517
|
+
response.setHeader("Cache-Control", "no-store");
|
|
518
|
+
response.end(metrics);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
next(error);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
applications.set(app, { serviceName: options.serviceName, registry });
|
|
524
|
+
return registry;
|
|
525
|
+
}
|
|
231
526
|
// Annotate the CommonJS export names for ESM import in node:
|
|
232
527
|
0 && (module.exports = {
|
|
233
528
|
LogLevelTypes,
|
|
234
529
|
elasticSearchRepo,
|
|
235
530
|
loggerRepo,
|
|
531
|
+
rabbitmqRepo,
|
|
236
532
|
registerErrorMiddleware,
|
|
237
533
|
registerSecurityMiddleware,
|
|
238
534
|
registerStandardMiddleware,
|
|
239
535
|
runServer,
|
|
536
|
+
setupMetrics,
|
|
240
537
|
verifyGatewayRequest
|
|
241
538
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Express, Request, Response, NextFunction, Application } from 'express';
|
|
2
2
|
import { LogEntry } from 'winston';
|
|
3
|
+
import { Channel } from 'amqplib';
|
|
4
|
+
export { MetricsOptions, setupMetrics } from './middlewares/monitoring.middleware.cjs';
|
|
5
|
+
import 'prom-client';
|
|
3
6
|
|
|
4
7
|
declare function registerStandardMiddleware(app: Express): void;
|
|
5
8
|
|
|
@@ -27,7 +30,7 @@ declare function runServer(app: Express): void;
|
|
|
27
30
|
|
|
28
31
|
/** Configured Elasticsearch cluster */
|
|
29
32
|
declare class ElasticsearchRepo {
|
|
30
|
-
private elasticSearchClient?;
|
|
33
|
+
private readonly elasticSearchClient?;
|
|
31
34
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
32
35
|
constructor();
|
|
33
36
|
/** Elasticsearch connection is established. */
|
|
@@ -37,8 +40,84 @@ declare class ElasticsearchRepo {
|
|
|
37
40
|
}
|
|
38
41
|
declare const elasticSearchRepo: ElasticsearchRepo;
|
|
39
42
|
|
|
43
|
+
declare class RabbitmqRepo {
|
|
44
|
+
private queueChannel;
|
|
45
|
+
private pendingConnection;
|
|
46
|
+
/**
|
|
47
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
getQueueConnection(): Promise<Channel | undefined>;
|
|
55
|
+
private createQueueConnection;
|
|
56
|
+
/**
|
|
57
|
+
* Closes the channel and connection when the application shuts down.
|
|
58
|
+
*/
|
|
59
|
+
private closeConnection;
|
|
60
|
+
/**
|
|
61
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
establishQueueConnection(): Promise<void>;
|
|
69
|
+
private connectQueueToExchange;
|
|
70
|
+
/**
|
|
71
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
76
|
+
* exchange: 'notifications',
|
|
77
|
+
* routingKey: 'email',
|
|
78
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
79
|
+
* logMessage: 'Email notification published',
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
publishMessageToQueue<T>(args: {
|
|
84
|
+
exchange: string;
|
|
85
|
+
routingKey: string;
|
|
86
|
+
message: T;
|
|
87
|
+
logMessage?: string;
|
|
88
|
+
exchangeType?: 'direct' | 'fanout';
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
92
|
+
* Prefetch defaults to 10 when omitted.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
97
|
+
* exchange: 'notifications',
|
|
98
|
+
* queueName: 'email-queue',
|
|
99
|
+
* routingKey: 'email',
|
|
100
|
+
* prefetch: 5,
|
|
101
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
102
|
+
* // Process the email.
|
|
103
|
+
* },
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
consumeMessagesFromQueue<T = unknown>(args: {
|
|
108
|
+
exchange: string;
|
|
109
|
+
queueName: string;
|
|
110
|
+
routingKey: string;
|
|
111
|
+
callback: (payload: T) => void | Promise<void>;
|
|
112
|
+
channel?: Channel;
|
|
113
|
+
exchangeType?: 'direct' | 'fanout';
|
|
114
|
+
prefetch?: number;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
declare const rabbitmqRepo: RabbitmqRepo;
|
|
118
|
+
|
|
40
119
|
declare function verifyGatewayRequest(serviceId: string): (req: Request, _res: Response, next: NextFunction) => void;
|
|
41
120
|
|
|
42
121
|
declare function registerErrorMiddleware(app: Application): (err: Error, _req: Request, res: Response, _next: NextFunction) => void;
|
|
43
122
|
|
|
44
|
-
export { LogLevelTypes, elasticSearchRepo, loggerRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
|
123
|
+
export { LogLevelTypes, elasticSearchRepo, loggerRepo, rabbitmqRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Express, Request, Response, NextFunction, Application } from 'express';
|
|
2
2
|
import { LogEntry } from 'winston';
|
|
3
|
+
import { Channel } from 'amqplib';
|
|
4
|
+
export { MetricsOptions, setupMetrics } from './middlewares/monitoring.middleware.js';
|
|
5
|
+
import 'prom-client';
|
|
3
6
|
|
|
4
7
|
declare function registerStandardMiddleware(app: Express): void;
|
|
5
8
|
|
|
@@ -27,7 +30,7 @@ declare function runServer(app: Express): void;
|
|
|
27
30
|
|
|
28
31
|
/** Configured Elasticsearch cluster */
|
|
29
32
|
declare class ElasticsearchRepo {
|
|
30
|
-
private elasticSearchClient?;
|
|
33
|
+
private readonly elasticSearchClient?;
|
|
31
34
|
/** Creates an Elasticsearch client using the configured cluster URL. */
|
|
32
35
|
constructor();
|
|
33
36
|
/** Elasticsearch connection is established. */
|
|
@@ -37,8 +40,84 @@ declare class ElasticsearchRepo {
|
|
|
37
40
|
}
|
|
38
41
|
declare const elasticSearchRepo: ElasticsearchRepo;
|
|
39
42
|
|
|
43
|
+
declare class RabbitmqRepo {
|
|
44
|
+
private queueChannel;
|
|
45
|
+
private pendingConnection;
|
|
46
|
+
/**
|
|
47
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
getQueueConnection(): Promise<Channel | undefined>;
|
|
55
|
+
private createQueueConnection;
|
|
56
|
+
/**
|
|
57
|
+
* Closes the channel and connection when the application shuts down.
|
|
58
|
+
*/
|
|
59
|
+
private closeConnection;
|
|
60
|
+
/**
|
|
61
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
establishQueueConnection(): Promise<void>;
|
|
69
|
+
private connectQueueToExchange;
|
|
70
|
+
/**
|
|
71
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
76
|
+
* exchange: 'notifications',
|
|
77
|
+
* routingKey: 'email',
|
|
78
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
79
|
+
* logMessage: 'Email notification published',
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
publishMessageToQueue<T>(args: {
|
|
84
|
+
exchange: string;
|
|
85
|
+
routingKey: string;
|
|
86
|
+
message: T;
|
|
87
|
+
logMessage?: string;
|
|
88
|
+
exchangeType?: 'direct' | 'fanout';
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
92
|
+
* Prefetch defaults to 10 when omitted.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
97
|
+
* exchange: 'notifications',
|
|
98
|
+
* queueName: 'email-queue',
|
|
99
|
+
* routingKey: 'email',
|
|
100
|
+
* prefetch: 5,
|
|
101
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
102
|
+
* // Process the email.
|
|
103
|
+
* },
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
consumeMessagesFromQueue<T = unknown>(args: {
|
|
108
|
+
exchange: string;
|
|
109
|
+
queueName: string;
|
|
110
|
+
routingKey: string;
|
|
111
|
+
callback: (payload: T) => void | Promise<void>;
|
|
112
|
+
channel?: Channel;
|
|
113
|
+
exchangeType?: 'direct' | 'fanout';
|
|
114
|
+
prefetch?: number;
|
|
115
|
+
}): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
declare const rabbitmqRepo: RabbitmqRepo;
|
|
118
|
+
|
|
40
119
|
declare function verifyGatewayRequest(serviceId: string): (req: Request, _res: Response, next: NextFunction) => void;
|
|
41
120
|
|
|
42
121
|
declare function registerErrorMiddleware(app: Application): (err: Error, _req: Request, res: Response, _next: NextFunction) => void;
|
|
43
122
|
|
|
44
|
-
export { LogLevelTypes, elasticSearchRepo, loggerRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
|
123
|
+
export { LogLevelTypes, elasticSearchRepo, loggerRepo, rabbitmqRepo, registerErrorMiddleware, registerSecurityMiddleware, registerStandardMiddleware, runServer, verifyGatewayRequest };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
setupMetrics
|
|
3
|
+
} from "./chunk-TS72ONWN.js";
|
|
4
|
+
|
|
1
5
|
// src/middlewares/standard.middleware.ts
|
|
2
6
|
import { json, urlencoded } from "express";
|
|
3
7
|
import compression from "compression";
|
|
@@ -19,10 +23,13 @@ var Config = class {
|
|
|
19
23
|
API_GATEWAY_URL;
|
|
20
24
|
GATEWAY_TOKEN;
|
|
21
25
|
ELASTIC_SEARCH_NODE_URL;
|
|
26
|
+
RABBITMQ_URL;
|
|
22
27
|
PORT;
|
|
23
28
|
constructor() {
|
|
24
29
|
this.API_GATEWAY_URL = process.env.API_GATEWAY_URL || "";
|
|
25
30
|
this.GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "";
|
|
31
|
+
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL || "";
|
|
32
|
+
this.RABBITMQ_URL = process.env.RABBITMQ_URL || "";
|
|
26
33
|
this.ELASTIC_SEARCH_NODE_URL = process.env.ELASTIC_SEARCH_NODE_URL?.trim() || "";
|
|
27
34
|
this.PORT = process.env.PORT || "";
|
|
28
35
|
}
|
|
@@ -150,6 +157,202 @@ var ElasticsearchRepo = class {
|
|
|
150
157
|
};
|
|
151
158
|
var elasticSearchRepo = new ElasticsearchRepo();
|
|
152
159
|
|
|
160
|
+
// src/repositories/rabbitmq.repo.ts
|
|
161
|
+
import amqp from "amqplib";
|
|
162
|
+
var RabbitmqRepo = class {
|
|
163
|
+
queueChannel;
|
|
164
|
+
pendingConnection;
|
|
165
|
+
/**
|
|
166
|
+
* Returns the shared channel, creating a connection when necessary.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```ts
|
|
170
|
+
* const channel = await rabbitmqRepo.getQueueConnection();
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
async getQueueConnection() {
|
|
174
|
+
if (this.queueChannel) {
|
|
175
|
+
return this.queueChannel;
|
|
176
|
+
}
|
|
177
|
+
if (this.pendingConnection) {
|
|
178
|
+
return this.pendingConnection;
|
|
179
|
+
}
|
|
180
|
+
this.pendingConnection = this.createQueueConnection();
|
|
181
|
+
try {
|
|
182
|
+
return await this.pendingConnection;
|
|
183
|
+
} finally {
|
|
184
|
+
this.pendingConnection = void 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async createQueueConnection() {
|
|
188
|
+
let connection;
|
|
189
|
+
try {
|
|
190
|
+
connection = await amqp.connect(libConfig.RABBITMQ_URL);
|
|
191
|
+
const channel = await connection.createChannel();
|
|
192
|
+
const clearChannel = () => {
|
|
193
|
+
if (this.queueChannel === channel) {
|
|
194
|
+
this.queueChannel = void 0;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
channel.on("error", (error) => {
|
|
198
|
+
clearChannel();
|
|
199
|
+
console.error("RabbitmqRepo channel error:", error);
|
|
200
|
+
});
|
|
201
|
+
channel.once("close", clearChannel);
|
|
202
|
+
connection.on("error", (error) => {
|
|
203
|
+
clearChannel();
|
|
204
|
+
console.error("RabbitmqRepo connection error:", error);
|
|
205
|
+
});
|
|
206
|
+
connection.once("close", clearChannel);
|
|
207
|
+
console.log("RabbitmqRepo connected to queue successfully...");
|
|
208
|
+
this.closeConnection(channel, connection);
|
|
209
|
+
this.queueChannel = channel;
|
|
210
|
+
return channel;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (connection) {
|
|
213
|
+
await connection.close().catch((closeError) => {
|
|
214
|
+
console.error("RabbitmqRepo error closing failed connection:", closeError);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
console.error("RabbitmqRepo error creating connection:", error);
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Closes the channel and connection when the application shuts down.
|
|
223
|
+
*/
|
|
224
|
+
closeConnection(channel, connection) {
|
|
225
|
+
let isClosing = false;
|
|
226
|
+
const close = async (signal) => {
|
|
227
|
+
if (isClosing) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
isClosing = true;
|
|
231
|
+
try {
|
|
232
|
+
try {
|
|
233
|
+
await channel.close();
|
|
234
|
+
} finally {
|
|
235
|
+
await connection.close();
|
|
236
|
+
}
|
|
237
|
+
console.log(`RabbitmqRepo queue connection closed after ${signal}`);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error("RabbitmqRepo error closing connection:", error);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
process.once("SIGINT", close);
|
|
243
|
+
process.once("SIGTERM", close);
|
|
244
|
+
connection.once("close", () => {
|
|
245
|
+
process.removeListener("SIGINT", close);
|
|
246
|
+
process.removeListener("SIGTERM", close);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Establishes the shared RabbitMQ connection during application startup.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* await rabbitmqRepo.establishQueueConnection();
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
async establishQueueConnection() {
|
|
258
|
+
const channel = await this.getQueueConnection();
|
|
259
|
+
if (!channel) {
|
|
260
|
+
throw new Error("No channel found.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async connectQueueToExchange(channel, exchange, queueName, routingKey, exchangeType) {
|
|
264
|
+
await channel.assertExchange(exchange, exchangeType);
|
|
265
|
+
const q = await channel.assertQueue(queueName, { durable: true, autoDelete: false });
|
|
266
|
+
await channel.bindQueue(q.queue, exchange, routingKey);
|
|
267
|
+
return q;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Publishes a typed payload as JSON using the supplied exchange and routing key.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* await rabbitmqRepo.publishMessageToQueue<EmailPayload>({
|
|
275
|
+
* exchange: 'notifications',
|
|
276
|
+
* routingKey: 'email',
|
|
277
|
+
* message: { receiverEmail, username, verifyLink, resetLink },
|
|
278
|
+
* logMessage: 'Email notification published',
|
|
279
|
+
* });
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
async publishMessageToQueue(args) {
|
|
283
|
+
const serializedMessage = JSON.stringify(args.message);
|
|
284
|
+
if (serializedMessage === void 0) {
|
|
285
|
+
throw new Error("Message must be JSON serializable.");
|
|
286
|
+
}
|
|
287
|
+
const channel = await this.getQueueConnection();
|
|
288
|
+
const { exchange, routingKey, logMessage, exchangeType } = args;
|
|
289
|
+
if (!channel) {
|
|
290
|
+
throw new Error("No channel found.");
|
|
291
|
+
}
|
|
292
|
+
await channel.assertExchange(exchange, exchangeType || "direct");
|
|
293
|
+
channel.publish(exchange, routingKey, Buffer.from(serializedMessage));
|
|
294
|
+
if (logMessage) {
|
|
295
|
+
console.log(logMessage);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Consumes JSON messages and acknowledges them after the callback succeeds.
|
|
300
|
+
* Prefetch defaults to 10 when omitted.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* await rabbitmqRepo.consumeMessagesFromQueue<EmailPayload>({
|
|
305
|
+
* exchange: 'notifications',
|
|
306
|
+
* queueName: 'email-queue',
|
|
307
|
+
* routingKey: 'email',
|
|
308
|
+
* prefetch: 5,
|
|
309
|
+
* callback: async ({ receiverEmail, username, verifyLink, resetLink }) => {
|
|
310
|
+
* // Process the email.
|
|
311
|
+
* },
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
async consumeMessagesFromQueue(args) {
|
|
316
|
+
const { exchange, queueName, routingKey, callback } = args;
|
|
317
|
+
const prefetch = args.prefetch ?? 10;
|
|
318
|
+
if (!Number.isInteger(prefetch) || prefetch < 1 || prefetch > 65535) {
|
|
319
|
+
throw new Error("Prefetch must be an integer between 1 and 65535.");
|
|
320
|
+
}
|
|
321
|
+
const channel = args.channel ?? await this.getQueueConnection();
|
|
322
|
+
if (!channel) {
|
|
323
|
+
throw new Error("No channel found.");
|
|
324
|
+
}
|
|
325
|
+
const q = await this.connectQueueToExchange(channel, exchange, queueName, routingKey, args.exchangeType ?? "direct");
|
|
326
|
+
await channel.prefetch(prefetch, false);
|
|
327
|
+
await channel.consume(q.queue, async (message) => {
|
|
328
|
+
if (!message) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
let payload;
|
|
333
|
+
try {
|
|
334
|
+
payload = JSON.parse(message.content.toString());
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error("RabbitmqRepo invalid JSON message:", error);
|
|
337
|
+
channel.nack(message, false, false);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await callback(payload);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error("RabbitmqRepo message callback failed:", error);
|
|
344
|
+
channel.nack(message, false, true);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
channel.ack(message);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error("RabbitmqRepo error settling message:", error);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var rabbitmqRepo = new RabbitmqRepo();
|
|
355
|
+
|
|
153
356
|
// src/middlewares/gateway.middleware.ts
|
|
154
357
|
import jwt from "jsonwebtoken";
|
|
155
358
|
function verifyGatewayRequest(serviceId) {
|
|
@@ -185,9 +388,11 @@ export {
|
|
|
185
388
|
LogLevelTypes,
|
|
186
389
|
elasticSearchRepo,
|
|
187
390
|
loggerRepo,
|
|
391
|
+
rabbitmqRepo,
|
|
188
392
|
registerErrorMiddleware,
|
|
189
393
|
registerSecurityMiddleware,
|
|
190
394
|
registerStandardMiddleware,
|
|
191
395
|
runServer,
|
|
396
|
+
setupMetrics,
|
|
192
397
|
verifyGatewayRequest
|
|
193
398
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middlewares/monitoring.middleware.ts
|
|
21
|
+
var monitoring_middleware_exports = {};
|
|
22
|
+
__export(monitoring_middleware_exports, {
|
|
23
|
+
setupMetrics: () => setupMetrics
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(monitoring_middleware_exports);
|
|
26
|
+
var import_prom_client = require("prom-client");
|
|
27
|
+
var applications = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
var methods = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE"]);
|
|
29
|
+
function routeLabel(request) {
|
|
30
|
+
const path = request.route?.path;
|
|
31
|
+
if (typeof path === "string") return path;
|
|
32
|
+
if (Array.isArray(path) && path.every((value) => typeof value === "string")) {
|
|
33
|
+
return path.join("|");
|
|
34
|
+
}
|
|
35
|
+
return path instanceof RegExp ? "__regexp__" : "__unmatched__";
|
|
36
|
+
}
|
|
37
|
+
function setupMetrics(app, options) {
|
|
38
|
+
if (typeof options?.serviceName !== "string" || !options.serviceName.trim()) {
|
|
39
|
+
throw new Error("Metrics serviceName must be a non-empty string");
|
|
40
|
+
}
|
|
41
|
+
const existing = applications.get(app);
|
|
42
|
+
if (existing) {
|
|
43
|
+
if (existing.serviceName !== options.serviceName) {
|
|
44
|
+
throw new Error("Metrics already initialized with a different serviceName");
|
|
45
|
+
}
|
|
46
|
+
return existing.registry;
|
|
47
|
+
}
|
|
48
|
+
const registry = new import_prom_client.Registry();
|
|
49
|
+
registry.setDefaultLabels({ service: options.serviceName });
|
|
50
|
+
(0, import_prom_client.collectDefaultMetrics)({ register: registry });
|
|
51
|
+
const labelNames = ["method", "route", "status_code"];
|
|
52
|
+
const requests = new import_prom_client.Counter({
|
|
53
|
+
name: "http_requests_total",
|
|
54
|
+
help: "Completed HTTP requests",
|
|
55
|
+
labelNames,
|
|
56
|
+
registers: [registry]
|
|
57
|
+
});
|
|
58
|
+
const duration = new import_prom_client.Histogram({
|
|
59
|
+
name: "http_request_duration_seconds",
|
|
60
|
+
help: "HTTP request duration in seconds",
|
|
61
|
+
labelNames,
|
|
62
|
+
registers: [registry],
|
|
63
|
+
buckets: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
64
|
+
});
|
|
65
|
+
const errors = new import_prom_client.Counter({
|
|
66
|
+
name: "http_request_errors_total",
|
|
67
|
+
help: "Completed HTTP requests with 4xx or 5xx status",
|
|
68
|
+
labelNames,
|
|
69
|
+
registers: [registry]
|
|
70
|
+
});
|
|
71
|
+
const active = new import_prom_client.Gauge({
|
|
72
|
+
name: "http_active_requests",
|
|
73
|
+
help: "HTTP requests currently in progress",
|
|
74
|
+
registers: [registry]
|
|
75
|
+
});
|
|
76
|
+
active.set(0);
|
|
77
|
+
const middleware = (request, response, next) => {
|
|
78
|
+
if (/^\/metrics\/?$/i.test(request.path)) return next();
|
|
79
|
+
const started = process.hrtime.bigint();
|
|
80
|
+
active.inc();
|
|
81
|
+
let completed = false;
|
|
82
|
+
const complete = () => {
|
|
83
|
+
if (completed) return;
|
|
84
|
+
completed = true;
|
|
85
|
+
response.removeListener("finish", complete);
|
|
86
|
+
response.removeListener("close", complete);
|
|
87
|
+
active.dec();
|
|
88
|
+
const labels = {
|
|
89
|
+
method: methods.has(request.method) ? request.method : "OTHER",
|
|
90
|
+
route: routeLabel(request),
|
|
91
|
+
// A disconnected response has no completed HTTP status.
|
|
92
|
+
status_code: response.writableFinished ? String(response.statusCode) : "aborted"
|
|
93
|
+
};
|
|
94
|
+
requests.inc(labels);
|
|
95
|
+
duration.observe(labels, Number(process.hrtime.bigint() - started) / 1e9);
|
|
96
|
+
if (response.writableFinished && response.statusCode >= 400 && response.statusCode < 600) {
|
|
97
|
+
errors.inc(labels);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
response.once("finish", complete);
|
|
101
|
+
response.once("close", complete);
|
|
102
|
+
next();
|
|
103
|
+
};
|
|
104
|
+
app.use(middleware);
|
|
105
|
+
app.get("/metrics", async (_request, response, next) => {
|
|
106
|
+
try {
|
|
107
|
+
const metrics = await registry.metrics();
|
|
108
|
+
response.setHeader("Content-Type", registry.contentType);
|
|
109
|
+
response.setHeader("Cache-Control", "no-store");
|
|
110
|
+
response.end(metrics);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
next(error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
applications.set(app, { serviceName: options.serviceName, registry });
|
|
116
|
+
return registry;
|
|
117
|
+
}
|
|
118
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
119
|
+
0 && (module.exports = {
|
|
120
|
+
setupMetrics
|
|
121
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
import { Registry } from 'prom-client';
|
|
3
|
+
|
|
4
|
+
interface MetricsOptions {
|
|
5
|
+
serviceName: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Register before authentication and application routes so GET /metrics bypasses auth.
|
|
9
|
+
* Restrict /metrics to internal access at the Kubernetes networking/Ingress level.
|
|
10
|
+
* Returns an app-local registry for custom metrics (use registers: [registry]).
|
|
11
|
+
* Router labels use leaf templates, omitting potentially dynamic mount prefixes.
|
|
12
|
+
*/
|
|
13
|
+
declare function setupMetrics(app: Express, options: MetricsOptions): Registry;
|
|
14
|
+
|
|
15
|
+
export { type MetricsOptions, setupMetrics };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
import { Registry } from 'prom-client';
|
|
3
|
+
|
|
4
|
+
interface MetricsOptions {
|
|
5
|
+
serviceName: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Register before authentication and application routes so GET /metrics bypasses auth.
|
|
9
|
+
* Restrict /metrics to internal access at the Kubernetes networking/Ingress level.
|
|
10
|
+
* Returns an app-local registry for custom metrics (use registers: [registry]).
|
|
11
|
+
* Router labels use leaf templates, omitting potentially dynamic mount prefixes.
|
|
12
|
+
*/
|
|
13
|
+
declare function setupMetrics(app: Express, options: MetricsOptions): Registry;
|
|
14
|
+
|
|
15
|
+
export { type MetricsOptions, setupMetrics };
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samuelaj1/eazisend-service-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "tsup"
|
|
6
6
|
},
|
|
7
7
|
"exports": {
|
|
8
|
+
"./monitoring": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/middlewares/monitoring.middleware.d.ts",
|
|
11
|
+
"default": "./dist/middlewares/monitoring.middleware.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/middlewares/monitoring.middleware.d.cts",
|
|
15
|
+
"default": "./dist/middlewares/monitoring.middleware.cjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
8
18
|
".": {
|
|
9
19
|
"import": {
|
|
10
20
|
"types": "./dist/index.d.ts",
|
|
@@ -29,25 +39,28 @@
|
|
|
29
39
|
"license": "MIT",
|
|
30
40
|
"type": "module",
|
|
31
41
|
"devDependencies": {
|
|
42
|
+
"@types/amqplib": "^0.10.8",
|
|
43
|
+
"@types/compression": "^1.8.1",
|
|
44
|
+
"@types/cors": "^2.8.19",
|
|
32
45
|
"@types/express": "^5.0.6",
|
|
46
|
+
"@types/hpp": "^0.2.7",
|
|
33
47
|
"@types/jsonwebtoken": "^9.0.10",
|
|
34
48
|
"tsup": "^8.5.1",
|
|
35
|
-
"typescript": "^6.0.3"
|
|
36
|
-
"@types/cors": "^2.8.19",
|
|
37
|
-
"@types/hpp": "^0.2.7",
|
|
38
|
-
"@types/compression": "^1.8.1"
|
|
49
|
+
"typescript": "^6.0.3"
|
|
39
50
|
},
|
|
40
51
|
"dependencies": {
|
|
41
52
|
"@elastic/elasticsearch": "^9.4.2",
|
|
53
|
+
"amqplib": "^2.2.0",
|
|
54
|
+
"compression": "^1.8.1",
|
|
55
|
+
"cors": "^2.8.6",
|
|
42
56
|
"dotenv": "^17.4.2",
|
|
43
57
|
"express": "^5.2.1",
|
|
58
|
+
"helmet": "^8.3.0",
|
|
59
|
+
"hpp": "^0.2.3",
|
|
44
60
|
"http-status-codes": "^2.3.0",
|
|
45
61
|
"jsonwebtoken": "^9.0.3",
|
|
62
|
+
"prom-client": "^15.1.3",
|
|
46
63
|
"winston": "^3.19.0",
|
|
47
|
-
"winston-elasticsearch": "^0.19.0"
|
|
48
|
-
"helmet": "^8.3.0",
|
|
49
|
-
"hpp": "^0.2.3",
|
|
50
|
-
"cors": "^2.8.6",
|
|
51
|
-
"compression": "^1.8.1"
|
|
64
|
+
"winston-elasticsearch": "^0.19.0"
|
|
52
65
|
}
|
|
53
66
|
}
|