@avtechno/sfr 1.0.18 → 2.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.
Files changed (42) hide show
  1. package/README.md +893 -45
  2. package/dist/index.mjs +20 -2
  3. package/dist/logger.mjs +9 -37
  4. package/dist/mq.mjs +46 -0
  5. package/dist/observability/index.mjs +143 -0
  6. package/dist/observability/logger.mjs +128 -0
  7. package/dist/observability/metrics.mjs +177 -0
  8. package/dist/observability/middleware/mq.mjs +156 -0
  9. package/dist/observability/middleware/rest.mjs +120 -0
  10. package/dist/observability/middleware/ws.mjs +135 -0
  11. package/dist/observability/tracer.mjs +163 -0
  12. package/dist/sfr-pipeline.mjs +412 -12
  13. package/dist/templates.mjs +8 -1
  14. package/dist/types/index.d.mts +8 -1
  15. package/dist/types/logger.d.mts +9 -3
  16. package/dist/types/mq.d.mts +19 -0
  17. package/dist/types/observability/index.d.mts +45 -0
  18. package/dist/types/observability/logger.d.mts +54 -0
  19. package/dist/types/observability/metrics.d.mts +74 -0
  20. package/dist/types/observability/middleware/mq.d.mts +46 -0
  21. package/dist/types/observability/middleware/rest.d.mts +33 -0
  22. package/dist/types/observability/middleware/ws.d.mts +35 -0
  23. package/dist/types/observability/tracer.d.mts +90 -0
  24. package/dist/types/sfr-pipeline.d.mts +42 -1
  25. package/dist/types/templates.d.mts +1 -6
  26. package/package.json +29 -4
  27. package/src/index.mts +66 -3
  28. package/src/logger.mts +16 -51
  29. package/src/mq.mts +49 -0
  30. package/src/observability/index.mts +184 -0
  31. package/src/observability/logger.mts +169 -0
  32. package/src/observability/metrics.mts +266 -0
  33. package/src/observability/middleware/mq.mts +187 -0
  34. package/src/observability/middleware/rest.mts +143 -0
  35. package/src/observability/middleware/ws.mts +162 -0
  36. package/src/observability/tracer.mts +205 -0
  37. package/src/sfr-pipeline.mts +468 -18
  38. package/src/templates.mts +14 -5
  39. package/src/types/index.d.ts +240 -16
  40. package/dist/example.mjs +0 -33
  41. package/dist/types/example.d.mts +0 -11
  42. package/src/example.mts +0 -35
@@ -3,7 +3,13 @@ import fs from "fs/promises";
3
3
  import { template } from "./util.mjs";
4
4
  import { BroadcastMQ, TargetedMQ } from "./mq.mjs";
5
5
  import j2s from "joi-to-swagger";
6
- import { logger } from "./logger.mjs";
6
+ import rateLimit from "express-rate-limit";
7
+ // Observability imports
8
+ import { init_observability, is_observability_enabled } from "./observability/index.mjs";
9
+ import { sfr_logger as logger } from "./observability/logger.mjs";
10
+ import { sfr_rest_telemetry } from "./observability/middleware/rest.mjs";
11
+ import { instrument_socket_io, wrap_ws_handler } from "./observability/middleware/ws.mjs";
12
+ import { wrap_mq_handler, record_mq_reject } from "./observability/middleware/mq.mjs";
7
13
  const CWD = process.cwd();
8
14
  const PATTERNS = ["Point-to-Point", "Request-Reply", "Fanout", "Direct", "Topic"];
9
15
  const TARGETED_PATTERN = ["Point-to-Point", "Request-Reply"];
@@ -25,11 +31,44 @@ export class SFRPipeline {
25
31
  handlers: {},
26
32
  controllers: {}
27
33
  },
34
+ ws: {
35
+ handlers: {},
36
+ controllers: {}
37
+ },
28
38
  mq: {
29
39
  handlers: {},
30
40
  controllers: {}
31
41
  },
32
42
  };
43
+ /* Authentication middleware configuration */
44
+ static auth_config = {};
45
+ /* Rate limit configuration */
46
+ static rate_limit_config = {};
47
+ /* Observability configuration */
48
+ static observability_options = {};
49
+ /**
50
+ * Sets the authentication middleware for protected routes.
51
+ * @param middleware - The authentication middleware function
52
+ */
53
+ static set_auth_middleware(middleware) {
54
+ SFRPipeline.auth_config.middleware = middleware;
55
+ }
56
+ /**
57
+ * Configures global rate limiting options for all routes.
58
+ * Can be overridden per-route using cfg.rate_limit in SFR files.
59
+ * @param config - Rate limit configuration options
60
+ */
61
+ static set_rate_limit_config(config) {
62
+ SFRPipeline.rate_limit_config = config;
63
+ }
64
+ /**
65
+ * Configures observability options for tracing, metrics, and logging.
66
+ * Must be called before SFR initialization to take effect.
67
+ * @param options - Observability configuration options
68
+ */
69
+ static set_observability_options(options) {
70
+ SFRPipeline.observability_options = options;
71
+ }
33
72
  constructor(cfg, oas_cfg, comms) {
34
73
  this.cfg = cfg;
35
74
  this.oas_cfg = oas_cfg;
@@ -37,6 +76,18 @@ export class SFRPipeline {
37
76
  }
38
77
  async init(base_url) {
39
78
  this.base_url = base_url;
79
+ // Initialize observability with service config from oas_cfg
80
+ if (SFRPipeline.observability_options.enabled !== false) {
81
+ init_observability(this.oas_cfg, SFRPipeline.observability_options);
82
+ }
83
+ // Add REST telemetry middleware if enabled
84
+ if (this.comms["REST"] && is_observability_enabled()) {
85
+ this.comms["REST"].use(sfr_rest_telemetry());
86
+ }
87
+ // Instrument Socket.IO if enabled
88
+ if (this.comms["WS"] && is_observability_enabled()) {
89
+ instrument_socket_io(this.comms["WS"]);
90
+ }
40
91
  if (this.comms["MQ"]) {
41
92
  //Create channels for each type of Communication Pattern
42
93
  const channels = await Promise.all(PATTERNS.map(async (v) => {
@@ -52,6 +103,17 @@ export class SFRPipeline {
52
103
  ...SFRPipeline.injections.mq.handlers,
53
104
  mq: this.pattern_channels
54
105
  };
106
+ SFRPipeline.injections.ws.handlers = {
107
+ ...SFRPipeline.injections.ws.handlers,
108
+ mq: this.pattern_channels
109
+ };
110
+ }
111
+ // Inject Socket.IO server into WS handlers
112
+ if (this.comms["WS"]) {
113
+ SFRPipeline.injections.ws.handlers = {
114
+ ...SFRPipeline.injections.ws.handlers,
115
+ io: this.comms["WS"]
116
+ };
55
117
  }
56
118
  //Declarations for each protocol in key/value pairs.
57
119
  let protocols = await this.file_parsing();
@@ -110,6 +172,82 @@ export class SFRPipeline {
110
172
  }
111
173
  });
112
174
  }
175
+ /**
176
+ * Generates OpenAPI multipart/form-data schema for Multer validators.
177
+ * Since Multer middleware functions can't be easily inspected at runtime,
178
+ * this generates a generic file upload schema.
179
+ */
180
+ generate_multer_openapi_schema(validator) {
181
+ // Multer middleware is a function, so we generate a generic multipart schema
182
+ // In the future, this could be enhanced to parse Multer configuration
183
+ return {
184
+ content: {
185
+ "multipart/form-data": {
186
+ schema: {
187
+ type: "object",
188
+ properties: {
189
+ file: {
190
+ type: "string",
191
+ format: "binary",
192
+ description: "File to upload"
193
+ }
194
+ }
195
+ },
196
+ encoding: {
197
+ file: {
198
+ contentType: "*/*"
199
+ }
200
+ }
201
+ }
202
+ }
203
+ };
204
+ }
205
+ /**
206
+ * Gets rate limit configuration for a route, merging global defaults with route-specific config.
207
+ */
208
+ get_rate_limit_config_for_route(cfg) {
209
+ if (cfg?.rate_limit === false) {
210
+ return null; // Explicitly disabled
211
+ }
212
+ const global_default = SFRPipeline.rate_limit_config.default;
213
+ const route_config = cfg?.rate_limit;
214
+ if (!global_default && !route_config) {
215
+ return null; // No rate limiting configured
216
+ }
217
+ return {
218
+ max: 100,
219
+ windowMs: 60000,
220
+ ...global_default,
221
+ ...route_config
222
+ };
223
+ }
224
+ /**
225
+ * Creates a rate limiter middleware based on configuration.
226
+ * Merges route-specific config with global defaults.
227
+ */
228
+ create_rate_limiter(route_config) {
229
+ // If explicitly disabled, return null
230
+ if (route_config === false) {
231
+ return null;
232
+ }
233
+ // Merge route config with global default
234
+ const global_default = SFRPipeline.rate_limit_config.default;
235
+ const config = {
236
+ max: 100,
237
+ windowMs: 60000,
238
+ message: "Too many requests, please try again later.",
239
+ statusCode: 429,
240
+ standardHeaders: true,
241
+ legacyHeaders: false,
242
+ ...global_default,
243
+ ...route_config
244
+ };
245
+ // Only create rate limiter if max is greater than 0
246
+ if (config.max && config.max > 0) {
247
+ return rateLimit(config);
248
+ }
249
+ return null;
250
+ }
113
251
  /* Fn Binding */
114
252
  async bind_rest_fns(declaration) {
115
253
  const comms = this.comms["REST"];
@@ -137,14 +275,26 @@ export class SFRPipeline {
137
275
  const dir = `${base_dir}/${namespace}/${name}`;
138
276
  const validator = validators[name];
139
277
  const validator_type = typeof validator !== "function" ? "joi" : "multer";
278
+ // Apply rate limiting middleware if configured
279
+ const rate_limiter = this.create_rate_limiter(cfg.rate_limit);
280
+ if (rate_limiter) {
281
+ comms[method.toLowerCase()](dir, rate_limiter);
282
+ }
140
283
  /* bind validators */
141
284
  switch (validator_type) {
142
285
  case "joi":
143
286
  {
144
- comms[method.toLowerCase()](dir, (req, res, next) => {
287
+ comms[method.toLowerCase()](dir, async (req, res, next) => {
145
288
  let error = true;
146
- if (is_public) {
147
- //if(!req.session.user)return res.status(401).json({error : "Session not found."});
289
+ // Authentication check for protected routes
290
+ if (!is_public && SFRPipeline.auth_config.middleware) {
291
+ const auth_result = await SFRPipeline.auth_config.middleware(req, res);
292
+ if (auth_result === false) {
293
+ return res.status(401).json({ error: "Unauthorized" });
294
+ }
295
+ if (typeof auth_result === "object" && auth_result.error) {
296
+ return res.status(auth_result.status || 401).json({ error: auth_result.error });
297
+ }
148
298
  }
149
299
  const validator_keys = Object.keys(validator);
150
300
  if (validator_keys.length) {
@@ -177,6 +327,96 @@ export class SFRPipeline {
177
327
  });
178
328
  }
179
329
  async bind_ws_fns(declaration) {
330
+ const io = this.comms["WS"];
331
+ if (!io)
332
+ throw new Error(`FN Binding failed: \nWS protocol lacks its associated SFRProtocols`);
333
+ Object.entries(declaration).forEach(([namespace, module]) => {
334
+ const { cfg, validators, handlers } = module.content;
335
+ // Determine the Socket.IO namespace to use
336
+ // Priority: cfg.namespace -> cfg.base_dir -> "/"
337
+ let ws_namespace = "/";
338
+ if (cfg.namespace)
339
+ ws_namespace = cfg.namespace.startsWith("/") ? cfg.namespace : `/${cfg.namespace}`;
340
+ else if (cfg.base_dir)
341
+ ws_namespace = cfg.base_dir.startsWith("/") ? cfg.base_dir : `/${cfg.base_dir}`;
342
+ logger.info(`[SFR WS ${module.name}] namespace: ${ws_namespace}`);
343
+ // Get or create the namespace
344
+ const nsp = io.of(ws_namespace);
345
+ // Handle connections to this namespace
346
+ nsp.on("connection", (socket) => {
347
+ logger.info(`[SFR WS] Client connected to ${ws_namespace}: ${socket.id}`);
348
+ // Register event handlers for this connection
349
+ Object.entries(handlers).forEach(([event, handler]) => {
350
+ // Skip if no matching validator
351
+ const validator = validators[event];
352
+ if (!validator)
353
+ return;
354
+ socket.on(event, async (data, ack) => {
355
+ // Validate incoming data if validator is a Joi schema (not Multer)
356
+ const validator_type = typeof validator !== "function" ? "joi" : "custom";
357
+ if (validator_type === "joi") {
358
+ const validator_keys = Object.keys(validator);
359
+ if (validator_keys.length) {
360
+ const error = template(validator, data || {});
361
+ if (error) {
362
+ // Send validation error back via acknowledgement or emit
363
+ if (ack) {
364
+ ack({ error });
365
+ }
366
+ else {
367
+ socket.emit(`${event}:error`, { error });
368
+ }
369
+ return;
370
+ }
371
+ }
372
+ }
373
+ // Build context for handler
374
+ const ctx = {
375
+ io,
376
+ socket,
377
+ data: data || {},
378
+ ack
379
+ };
380
+ // Create the handler execution function
381
+ const execute_handler = async () => {
382
+ await handler.fn.call({
383
+ ...module.content.controllers,
384
+ ...SFRPipeline.injections.ws.handlers,
385
+ io,
386
+ socket
387
+ }, ctx);
388
+ };
389
+ try {
390
+ // Wrap with telemetry if observability is enabled
391
+ if (is_observability_enabled()) {
392
+ const instrumented = wrap_ws_handler(ws_namespace, event, execute_handler);
393
+ await instrumented();
394
+ }
395
+ else {
396
+ await execute_handler();
397
+ }
398
+ }
399
+ catch (err) {
400
+ logger.error(`[SFR WS] Error in handler ${event}`, {
401
+ error: err instanceof Error ? err.message : String(err),
402
+ event,
403
+ namespace: ws_namespace
404
+ });
405
+ if (ack) {
406
+ ack({ error: err instanceof Error ? err.message : "Unknown error" });
407
+ }
408
+ else {
409
+ socket.emit(`${event}:error`, { error: err instanceof Error ? err.message : "Unknown error" });
410
+ }
411
+ }
412
+ });
413
+ this.mount_data.ws++;
414
+ });
415
+ socket.on("disconnect", (reason) => {
416
+ logger.info(`[SFR WS] Client disconnected from ${ws_namespace}: ${socket.id} (${reason})`);
417
+ });
418
+ });
419
+ });
180
420
  }
181
421
  async bind_mq_fns(declaration) {
182
422
  const comms = this.comms["MQ"];
@@ -202,11 +442,16 @@ export class SFRPipeline {
202
442
  const dir = `${base_dir}${namespace}/${name}`;
203
443
  const validator = validators[name];
204
444
  //const validator_type = typeof validator !== "function" ? "joi" : "multer"; // TODO: Dynamically update parameter content based on validator type.
205
- let validator_fn = function (msg) {
445
+ // Create the base handler with validation
446
+ const base_handler = function (msg) {
206
447
  const validator_keys = Object.keys(validator);
207
448
  if (validator_keys.length) {
208
449
  const error = template(validator, msg.content);
209
450
  if (error) {
451
+ // Record rejection metrics
452
+ if (is_observability_enabled()) {
453
+ record_mq_reject(dir, pattern, false);
454
+ }
210
455
  if (mq instanceof TargetedMQ) {
211
456
  if (error) {
212
457
  switch (pattern) {
@@ -224,6 +469,10 @@ export class SFRPipeline {
224
469
  }
225
470
  handler.fn(msg);
226
471
  };
472
+ // Wrap with telemetry if observability is enabled
473
+ const validator_fn = is_observability_enabled()
474
+ ? wrap_mq_handler(dir, pattern, base_handler)
475
+ : base_handler;
227
476
  /* bind validators */
228
477
  if (mq instanceof TargetedMQ) {
229
478
  mq.consume(dir, {
@@ -248,8 +497,8 @@ export class SFRPipeline {
248
497
  const documents = Object.entries(protocols).map(([protocol, declaration]) => {
249
498
  switch (protocol) {
250
499
  case "REST": return [protocol, this.generate_open_api_document(declaration)];
251
- case "WS": return [protocol, this.generate_async_api_document(declaration)];
252
- case "MQ": return [protocol, this.generate_async_api_document(declaration)];
500
+ case "WS": return [protocol, this.generate_ws_async_api_document(declaration)];
501
+ case "MQ": return [protocol, this.generate_mq_async_api_document(declaration)];
253
502
  default: throw new Error(`Failed to generate SFR Spec: ${protocol} protocol is unknown.`);
254
503
  }
255
504
  });
@@ -290,29 +539,97 @@ export class SFRPipeline {
290
539
  if (body.tags && Array.isArray(body.tags))
291
540
  default_tags.push(...body.tags);
292
541
  const operation_id = `${base_dir}${namespace}/${endpoint}`;
542
+ const is_public = Boolean(cfg.public);
543
+ const rate_limit_config = this.get_rate_limit_config_for_route(cfg);
293
544
  const document = {
294
545
  operationId: `${method.toUpperCase()}:${operation_id}`,
295
546
  summary: body.summary || "",
296
547
  description: body.description || "",
297
548
  tags: default_tags,
298
- public: Boolean(cfg.public),
549
+ public: is_public,
299
550
  responses: {
300
551
  "200": {
301
552
  description: "Successful operation",
302
553
  content: { "application/json": { schema: { type: "object" } } }
303
554
  },
304
555
  "400": {
305
- description: "An error has occured",
306
- content: { "application/json": { schema: { type: "object" } } }
556
+ description: "Bad request - validation error",
557
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
307
558
  }
308
559
  }
309
560
  };
561
+ // Add authentication-related responses for protected routes
562
+ if (!is_public) {
563
+ document.responses["401"] = {
564
+ description: "Unauthorized - authentication required",
565
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
566
+ };
567
+ document.responses["403"] = {
568
+ description: "Forbidden - insufficient permissions",
569
+ content: { "application/json": { schema: { type: "object", properties: { error: { type: "string" } } } } }
570
+ };
571
+ }
572
+ // Add rate limiting response and headers if rate limiting is configured
573
+ if (rate_limit_config && rate_limit_config.max && rate_limit_config.max > 0) {
574
+ document.responses["429"] = {
575
+ description: "Too many requests - rate limit exceeded",
576
+ content: {
577
+ "application/json": {
578
+ schema: {
579
+ type: "object",
580
+ properties: {
581
+ error: { type: "string" },
582
+ message: { type: "string" }
583
+ }
584
+ }
585
+ }
586
+ },
587
+ headers: {}
588
+ };
589
+ // Add rate limit headers if standardHeaders is enabled
590
+ if (rate_limit_config.standardHeaders !== false) {
591
+ document.responses["429"].headers["RateLimit-Limit"] = {
592
+ description: "The maximum number of requests allowed in the current window",
593
+ schema: { type: "integer", example: rate_limit_config.max }
594
+ };
595
+ document.responses["429"].headers["RateLimit-Remaining"] = {
596
+ description: "The number of requests remaining in the current window",
597
+ schema: { type: "integer" }
598
+ };
599
+ document.responses["429"].headers["RateLimit-Reset"] = {
600
+ description: "The time at which the current rate limit window resets (Unix timestamp)",
601
+ schema: { type: "integer" }
602
+ };
603
+ }
604
+ // Add legacy headers if enabled
605
+ if (rate_limit_config.legacyHeaders) {
606
+ document.responses["429"].headers["X-RateLimit-Limit"] = {
607
+ description: "The maximum number of requests allowed in the current window (legacy)",
608
+ schema: { type: "integer", example: rate_limit_config.max }
609
+ };
610
+ document.responses["429"].headers["X-RateLimit-Remaining"] = {
611
+ description: "The number of requests remaining in the current window (legacy)",
612
+ schema: { type: "integer" }
613
+ };
614
+ document.responses["429"].headers["X-RateLimit-Reset"] = {
615
+ description: "The time at which the current rate limit window resets (legacy)",
616
+ schema: { type: "integer" }
617
+ };
618
+ }
619
+ // Add rate limit metadata to operation description
620
+ if (rate_limit_config.windowMs) {
621
+ const window_seconds = Math.floor(rate_limit_config.windowMs / 1000);
622
+ const rate_limit_note = `\n\n**Rate Limit:** ${rate_limit_config.max} requests per ${window_seconds} second${window_seconds !== 1 ? 's' : ''}`;
623
+ document.description = (document.description || "") + rate_limit_note;
624
+ }
625
+ }
310
626
  //Insert either a parameter or a requestBody according to the method type
311
627
  if (validator_type === "joi") {
312
628
  document[method === "GET" ? "parameters" : "requestBody"] = j2s(validator).swagger;
313
629
  }
314
- //Haven't found a library for converting multer validators to swagger doc
630
+ // Convert Multer validators to multipart/form-data OpenAPI schema
315
631
  if (validator_type === "multer") {
632
+ document.requestBody = this.generate_multer_openapi_schema(validator);
316
633
  }
317
634
  //Create path if it does not exist.
318
635
  if (!spec.paths[operation_id])
@@ -327,8 +644,91 @@ export class SFRPipeline {
327
644
  spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
328
645
  return spec;
329
646
  }
647
+ // Method to generate an AsyncAPI document from a WSNamespaceDeclaration
648
+ generate_ws_async_api_document(declaration) {
649
+ const spec = {
650
+ asyncapi: '3.0.0',
651
+ info: Object.assign({}, this.oas_cfg),
652
+ servers: {
653
+ websocket: {
654
+ host: `localhost:${this.oas_cfg.meta?.port || 3000}`,
655
+ protocol: 'ws',
656
+ description: 'WebSocket server'
657
+ }
658
+ },
659
+ channels: {},
660
+ operations: {},
661
+ components: {
662
+ messages: {},
663
+ },
664
+ };
665
+ // Conform to AsyncAPI standards by appending "x-property" on each field under "meta"
666
+ if (spec.info.meta) {
667
+ spec.info.meta = Object.fromEntries(Object.entries(spec.info.meta).map(([k, v]) => [`x-${k}`, v]));
668
+ }
669
+ Object.entries(declaration).forEach(([namespace, module]) => {
670
+ const { cfg, validators, handlers } = module.content;
671
+ // Determine namespace path
672
+ let ws_namespace = "/";
673
+ if (cfg.namespace)
674
+ ws_namespace = cfg.namespace.startsWith("/") ? cfg.namespace : `/${cfg.namespace}`;
675
+ else if (cfg.base_dir)
676
+ ws_namespace = cfg.base_dir.startsWith("/") ? cfg.base_dir : `/${cfg.base_dir}`;
677
+ // Loop over all event handlers
678
+ Object.entries(handlers).forEach(([event, handler]) => {
679
+ const validator = validators[event];
680
+ if (!validator)
681
+ return;
682
+ const validator_type = typeof validator !== "function" ? "joi" : "custom";
683
+ const channel_id = `${ws_namespace === "/" ? "" : ws_namespace}/${event}`.replace(/^\/+/, "").replace(/\//g, "-") || event;
684
+ // Default tags for service classification
685
+ const default_tags = [
686
+ { name: `sfr-namespace:${namespace}` },
687
+ { name: `sfr-service:${this.oas_cfg.title || "unspecified"}` }
688
+ ];
689
+ if (handler.tags && Array.isArray(handler.tags)) {
690
+ default_tags.push(...handler.tags.map(t => ({ name: t })));
691
+ }
692
+ const channel_document = {
693
+ address: ws_namespace === "/" ? `/${event}` : `${ws_namespace}/${event}`,
694
+ messages: {}
695
+ };
696
+ const operation_document = {
697
+ action: "receive",
698
+ summary: handler.summary || `Handle ${event} event`,
699
+ description: handler.description || "",
700
+ channel: { $ref: `#/channels/${channel_id}` },
701
+ tags: default_tags
702
+ };
703
+ // Generate message schema from Joi validator
704
+ if (validator_type === "joi") {
705
+ const message_id = `${channel_id}-message`;
706
+ channel_document.messages[message_id] = {
707
+ $ref: `#/components/messages/${message_id}`
708
+ };
709
+ spec.components.messages[message_id] = {
710
+ name: message_id,
711
+ contentType: "application/json",
712
+ payload: j2s(validator).swagger
713
+ };
714
+ }
715
+ spec.channels[channel_id] = channel_document;
716
+ spec.operations[`${channel_id}-receive`] = operation_document;
717
+ // Add send operation for events that can emit responses
718
+ const send_operation = {
719
+ action: "send",
720
+ summary: `Send ${event} response`,
721
+ description: `Response for ${event} event`,
722
+ channel: { $ref: `#/channels/${channel_id}` },
723
+ tags: default_tags
724
+ };
725
+ spec.operations[`${channel_id}-send`] = send_operation;
726
+ });
727
+ });
728
+ return spec;
729
+ }
330
730
  // Method to generate an AsyncAPI document from an MQNamespaceDeclaration
331
- generate_async_api_document(declaration) {
731
+ generate_mq_async_api_document(declaration) {
332
732
  // This will hold the final AsyncAPI Document.
333
733
  const spec = {
334
734
  asyncapi: '3.0.0',
@@ -23,7 +23,14 @@ export function WS(struct) {
23
23
  const controllers = struct.controllers || {};
24
24
  const handlers = struct.handlers || {};
25
25
  const cfg = struct.cfg || {};
26
- //Dependency injection happens here...npm
26
+ // Bind controller injections to each controller
27
+ Object.entries(controllers).map(([k, v]) => controllers[k] = v.bind({ ...SFRPipeline.injections.ws.controllers }));
28
+ // Bind handler injections and SFR controllers into each handler
29
+ Object.entries(handlers).forEach(([event, handler]) => {
30
+ /* @ts-ignore */
31
+ handlers[event] = { ...handler, fn: handler.fn.bind({ ...controllers, ...SFRPipeline.injections.ws.handlers }) };
32
+ });
33
+ /* @ts-ignore */
27
34
  return { validators, controllers, handlers, cfg };
28
35
  }
29
36
  export function MQ(struct) {
@@ -4,4 +4,11 @@ import { MQLib, BroadcastMQ, TargetedMQ } from "./mq.mjs";
4
4
  declare const _default: (cfg: ParserCFG, oas_cfg: OASConfig, connectors: SFRProtocols, base_url?: string) => Promise<ServiceManifest>;
5
5
  export default _default;
6
6
  declare function inject(injections: InjectedFacilities): void;
7
- export { REST, WS, MQ, MQLib, BroadcastMQ, TargetedMQ, inject };
7
+ declare const set_auth_middleware: any;
8
+ declare const set_rate_limit_config: any;
9
+ declare const set_observability_options: any;
10
+ export { init_observability, shutdown_observability, is_observability_enabled, type ObservabilityOptions } from "./observability/index.mjs";
11
+ export { get_tracer, with_span, with_span_sync, get_trace_context, inject_trace_context, extract_trace_context, add_span_attributes, add_span_event, set_span_error, SpanKind } from "./observability/tracer.mjs";
12
+ export { get_sfr_metrics, get_meter, record_rest_request, record_ws_event, record_mq_message, type SFRMetrics } from "./observability/metrics.mjs";
13
+ export { sfr_logger, create_child_logger, init_logger, type LoggerConfig } from "./observability/logger.mjs";
14
+ export { REST, WS, MQ, MQLib, BroadcastMQ, TargetedMQ, inject, set_auth_middleware, set_rate_limit_config, set_observability_options };
@@ -1,3 +1,9 @@
1
- import winston from 'winston';
2
- declare const logger: winston.Logger;
3
- export { logger };
1
+ /**
2
+ * SFR Logger Module (Backward Compatibility)
3
+ *
4
+ * This module re-exports the new observability logger for backward compatibility.
5
+ * New code should import from "./observability/logger.mjs" directly.
6
+ *
7
+ * @deprecated Use imports from "./observability/logger.mjs" instead
8
+ */
9
+ export { sfr_logger as logger, sfr_logger, create_child_logger, init_logger, get_logger, type LoggerConfig } from "./observability/logger.mjs";
@@ -5,6 +5,7 @@ import { ChannelModel, Options, Channel, ConsumeMessage } from "amqplib";
5
5
  export declare class MQLib {
6
6
  private connection?;
7
7
  private channel?;
8
+ private is_closing;
8
9
  /**
9
10
  * Initializes the connection and channel to the message queue.
10
11
  *
@@ -26,6 +27,24 @@ export declare class MQLib {
26
27
  * @returns The channel object to the message queue.
27
28
  */
28
29
  get_channel(): Channel;
30
+ /**
31
+ * Gracefully disconnects from the message queue.
32
+ * Closes the channel first, then the connection.
33
+ *
34
+ * @param timeout - Optional timeout in ms to wait for in-flight messages (default: 5000)
35
+ * @example
36
+ * const mqLib = new MQLib();
37
+ * await mqLib.init("amqp://localhost");
38
+ * // ... do work ...
39
+ * await mqLib.disconnect();
40
+ */
41
+ disconnect(timeout?: number): Promise<void>;
42
+ /**
43
+ * Checks if the connection is currently active.
44
+ *
45
+ * @returns true if connected, false otherwise
46
+ */
47
+ is_connected(): boolean;
29
48
  }
30
49
  export declare class BaseMQ {
31
50
  channel: Channel;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SFR Observability Module
3
+ *
4
+ * Provides OpenTelemetry-compatible tracing, metrics, and logging.
5
+ * Configuration is automatically extracted from SFR's OASConfig.
6
+ */
7
+ export interface ObservabilityOptions {
8
+ /** Enable/disable observability (default: true) */
9
+ enabled?: boolean;
10
+ /** OTLP endpoint URL (default: http://localhost:4318) */
11
+ otlp_endpoint?: string;
12
+ /** Enable auto-instrumentation for common libraries (default: true) */
13
+ auto_instrumentation?: boolean;
14
+ /** Sampling ratio 0.0 - 1.0 (default: 1.0) */
15
+ sampling_ratio?: number;
16
+ /** Log format: 'json' for production, 'pretty' for development */
17
+ log_format?: "json" | "pretty";
18
+ /** Additional resource attributes */
19
+ resource_attributes?: Record<string, string>;
20
+ /** Enable debug logging for OTel SDK */
21
+ debug?: boolean;
22
+ }
23
+ /**
24
+ * Initializes the OpenTelemetry SDK with configuration derived from OASConfig.
25
+ * Must be called before SFR initialization for full instrumentation.
26
+ *
27
+ * @param oas_cfg - The OASConfig that will be passed to SFR
28
+ * @param options - Additional observability options
29
+ */
30
+ export declare function init_observability(oas_cfg: OASConfig, options?: ObservabilityOptions): void;
31
+ /**
32
+ * Gracefully shuts down the OpenTelemetry SDK.
33
+ * Flushes all pending telemetry data before closing.
34
+ */
35
+ export declare function shutdown_observability(): Promise<void>;
36
+ /**
37
+ * Checks if observability has been initialized.
38
+ */
39
+ export declare function is_observability_enabled(): boolean;
40
+ export { get_tracer, with_span, get_trace_context, inject_trace_context } from "./tracer.mjs";
41
+ export { get_sfr_metrics, init_metrics, type SFRMetrics } from "./metrics.mjs";
42
+ export { sfr_logger, create_child_logger, init_logger } from "./logger.mjs";
43
+ export { sfr_rest_telemetry } from "./middleware/rest.mjs";
44
+ export { instrument_socket_io } from "./middleware/ws.mjs";
45
+ export { wrap_mq_handler, inject_mq_trace_context } from "./middleware/mq.mjs";