@avtechno/sfr 1.0.18 → 2.0.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.
Files changed (42) hide show
  1. package/README.md +890 -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
package/dist/index.mjs CHANGED
@@ -40,6 +40,8 @@ async function write_service_discovery(cfg, documents) {
40
40
  break;
41
41
  case "WS":
42
42
  {
43
+ documentation = documentation;
44
+ write_to_file(cfg, "ws/index.yaml", documentation);
43
45
  }
44
46
  break;
45
47
  case "MQ": {
@@ -60,7 +62,7 @@ async function write_to_file(cfg, dir, data) {
60
62
  else { //Indicates a nested file
61
63
  const file = paths.pop(); //Pops the path array to be used for dir creation
62
64
  await fs.mkdir(path.join(cwd, cfg.out, ...paths), { recursive: true });
63
- fs.writeFile(path.join(cwd, cfg.out, ...paths, `${file}.yml`), yaml.dump(data), { flag: "w+" });
65
+ await fs.writeFile(path.join(cwd, cfg.out, ...paths, `${file}.yml`), yaml.dump(data), { flag: "w+" });
64
66
  }
65
67
  }
66
68
  function inject(injections) {
@@ -72,6 +74,14 @@ function inject(injections) {
72
74
  ...SFRPipeline.injections.rest.controllers,
73
75
  ...injections.controllers
74
76
  };
77
+ SFRPipeline.injections.ws.handlers = {
78
+ ...SFRPipeline.injections.ws.handlers,
79
+ ...injections.handlers
80
+ };
81
+ SFRPipeline.injections.ws.controllers = {
82
+ ...SFRPipeline.injections.ws.controllers,
83
+ ...injections.controllers
84
+ };
75
85
  SFRPipeline.injections.mq.handlers = {
76
86
  ...SFRPipeline.injections.mq.handlers,
77
87
  ...injections.handlers
@@ -81,4 +91,12 @@ function inject(injections) {
81
91
  ...injections.controllers
82
92
  };
83
93
  }
84
- export { REST, WS, MQ, MQLib, BroadcastMQ, TargetedMQ, inject };
94
+ const set_auth_middleware = SFRPipeline.set_auth_middleware.bind(SFRPipeline);
95
+ const set_rate_limit_config = SFRPipeline.set_rate_limit_config.bind(SFRPipeline);
96
+ const set_observability_options = SFRPipeline.set_observability_options.bind(SFRPipeline);
97
+ // Observability exports
98
+ export { init_observability, shutdown_observability, is_observability_enabled } from "./observability/index.mjs";
99
+ 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";
100
+ export { get_sfr_metrics, get_meter, record_rest_request, record_ws_event, record_mq_message } from "./observability/metrics.mjs";
101
+ export { sfr_logger, create_child_logger, init_logger } from "./observability/logger.mjs";
102
+ export { REST, WS, MQ, MQLib, BroadcastMQ, TargetedMQ, inject, set_auth_middleware, set_rate_limit_config, set_observability_options };
package/dist/logger.mjs CHANGED
@@ -1,37 +1,9 @@
1
- import winston from 'winston';
2
- // Define log levels
3
- const levels = {
4
- error: 0,
5
- warn: 1,
6
- info: 2,
7
- http: 3,
8
- verbose: 4,
9
- debug: 5,
10
- silly: 6,
11
- };
12
- // Define colors for each level
13
- const colors = {
14
- error: 'red',
15
- warn: 'yellow',
16
- info: 'green',
17
- http: 'magenta',
18
- verbose: 'cyan',
19
- debug: 'blue',
20
- silly: 'gray',
21
- };
22
- // Add colors to Winston
23
- winston.addColors(colors);
24
- // Define the format for logs
25
- const format = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), winston.format.colorize({ all: true }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}${info.metadata ? ` ${JSON.stringify(info.metadata)}` : ''}`));
26
- // Create the Winston logger
27
- const logger = winston.createLogger({
28
- level: process.env.NODE_ENV === 'development' && Boolean(process.env.DEBUG_SFR) ? 'debug' : 'info',
29
- levels,
30
- format,
31
- transports: [
32
- // Console transport
33
- new winston.transports.Console(),
34
- ],
35
- });
36
- // Export the logger
37
- 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 } from "./observability/logger.mjs";
package/dist/mq.mjs CHANGED
@@ -5,6 +5,7 @@ import { connect } from "amqplib";
5
5
  export class MQLib {
6
6
  connection;
7
7
  channel;
8
+ is_closing = false;
8
9
  /**
9
10
  * Initializes the connection and channel to the message queue.
10
11
  *
@@ -37,6 +38,51 @@ export class MQLib {
37
38
  get_channel() {
38
39
  return this.channel;
39
40
  }
41
+ /**
42
+ * Gracefully disconnects from the message queue.
43
+ * Closes the channel first, then the connection.
44
+ *
45
+ * @param timeout - Optional timeout in ms to wait for in-flight messages (default: 5000)
46
+ * @example
47
+ * const mqLib = new MQLib();
48
+ * await mqLib.init("amqp://localhost");
49
+ * // ... do work ...
50
+ * await mqLib.disconnect();
51
+ */
52
+ async disconnect(timeout = 5000) {
53
+ if (this.is_closing)
54
+ return;
55
+ this.is_closing = true;
56
+ // Allow time for in-flight messages to complete
57
+ await new Promise(resolve => setTimeout(resolve, Math.min(timeout, 1000)));
58
+ try {
59
+ if (this.channel) {
60
+ await this.channel.close();
61
+ this.channel = undefined;
62
+ }
63
+ }
64
+ catch (err) {
65
+ // Channel may already be closed
66
+ }
67
+ try {
68
+ if (this.connection) {
69
+ await this.connection.close();
70
+ this.connection = undefined;
71
+ }
72
+ }
73
+ catch (err) {
74
+ // Connection may already be closed
75
+ }
76
+ this.is_closing = false;
77
+ }
78
+ /**
79
+ * Checks if the connection is currently active.
80
+ *
81
+ * @returns true if connected, false otherwise
82
+ */
83
+ is_connected() {
84
+ return !!this.connection && !!this.channel && !this.is_closing;
85
+ }
40
86
  }
41
87
  export class BaseMQ {
42
88
  channel;
@@ -0,0 +1,143 @@
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
+ import { NodeSDK } from "@opentelemetry/sdk-node";
8
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
9
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
10
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
11
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
12
+ import { Resource } from "@opentelemetry/resources";
13
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
14
+ import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
15
+ import { init_metrics } from "./metrics.mjs";
16
+ import { init_logger } from "./logger.mjs";
17
+ let sdk = null;
18
+ let is_initialized = false;
19
+ /**
20
+ * Extracts observability configuration from SFR's OASConfig.
21
+ *
22
+ * @param oas_cfg - The OASConfig passed to SFR
23
+ * @param options - Additional observability options
24
+ */
25
+ function extract_config_from_oas(oas_cfg, options = {}) {
26
+ const service_name = oas_cfg.meta?.service || oas_cfg.title || "sfr-service";
27
+ const service_version = oas_cfg.version || "1.0.0";
28
+ const environment = process.env.NODE_ENV || "development";
29
+ return {
30
+ service_name,
31
+ service_version,
32
+ environment,
33
+ domain: oas_cfg.meta?.domain,
34
+ service_type: oas_cfg.meta?.type,
35
+ language: oas_cfg.meta?.language,
36
+ port: oas_cfg.meta?.port,
37
+ ...options
38
+ };
39
+ }
40
+ /**
41
+ * Initializes the OpenTelemetry SDK with configuration derived from OASConfig.
42
+ * Must be called before SFR initialization for full instrumentation.
43
+ *
44
+ * @param oas_cfg - The OASConfig that will be passed to SFR
45
+ * @param options - Additional observability options
46
+ */
47
+ export function init_observability(oas_cfg, options = {}) {
48
+ if (is_initialized) {
49
+ console.warn("[SFR Observability] Already initialized, skipping...");
50
+ return;
51
+ }
52
+ if (options.enabled === false || process.env.SFR_TELEMETRY_ENABLED === "false") {
53
+ console.log("[SFR Observability] Telemetry disabled");
54
+ return;
55
+ }
56
+ const config = extract_config_from_oas(oas_cfg, options);
57
+ // Enable debug logging if requested
58
+ if (options.debug || process.env.OTEL_LOG_LEVEL === "debug") {
59
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
60
+ }
61
+ const otlp_endpoint = options.otlp_endpoint
62
+ || process.env.OTEL_EXPORTER_OTLP_ENDPOINT
63
+ || "http://localhost:4318";
64
+ // Build resource attributes from OASConfig
65
+ const resource_attributes = {
66
+ [ATTR_SERVICE_NAME]: config.service_name,
67
+ [ATTR_SERVICE_VERSION]: config.service_version,
68
+ "deployment.environment": config.environment,
69
+ "sfr.domain": config.domain || "unknown",
70
+ "sfr.service.type": config.service_type || "backend",
71
+ "sfr.language": config.language || "typescript",
72
+ ...options.resource_attributes
73
+ };
74
+ if (config.port) {
75
+ resource_attributes["sfr.port"] = String(config.port);
76
+ }
77
+ const resource = new Resource(resource_attributes);
78
+ // Configure exporters
79
+ const trace_exporter = new OTLPTraceExporter({
80
+ url: `${otlp_endpoint}/v1/traces`
81
+ });
82
+ // Build instrumentations
83
+ const instrumentations = options.auto_instrumentation !== false
84
+ ? [getNodeAutoInstrumentations({
85
+ "@opentelemetry/instrumentation-fs": { enabled: false }, // Too noisy
86
+ "@opentelemetry/instrumentation-express": { enabled: true },
87
+ "@opentelemetry/instrumentation-http": { enabled: true },
88
+ "@opentelemetry/instrumentation-amqplib": { enabled: true }
89
+ })]
90
+ : [];
91
+ const metric_reader = new PeriodicExportingMetricReader({
92
+ exporter: new OTLPMetricExporter({
93
+ url: `${otlp_endpoint}/v1/metrics`
94
+ }),
95
+ exportIntervalMillis: 60000 // Export every 60 seconds
96
+ });
97
+ sdk = new NodeSDK({
98
+ resource,
99
+ traceExporter: trace_exporter,
100
+ // @ts-expect-error - Type mismatch between SDK packages, but compatible at runtime
101
+ metricReader: metric_reader,
102
+ instrumentations
103
+ });
104
+ sdk.start();
105
+ is_initialized = true;
106
+ // Initialize SFR-specific metrics
107
+ init_metrics(config.service_name);
108
+ // Initialize logger with service context
109
+ init_logger({
110
+ service_name: config.service_name,
111
+ format: options.log_format
112
+ });
113
+ console.log(`[SFR Observability] Initialized for service: ${config.service_name} v${config.service_version}`);
114
+ }
115
+ /**
116
+ * Gracefully shuts down the OpenTelemetry SDK.
117
+ * Flushes all pending telemetry data before closing.
118
+ */
119
+ export async function shutdown_observability() {
120
+ if (!sdk)
121
+ return;
122
+ try {
123
+ await sdk.shutdown();
124
+ is_initialized = false;
125
+ console.log("[SFR Observability] Shutdown complete");
126
+ }
127
+ catch (error) {
128
+ console.error("[SFR Observability] Error during shutdown:", error);
129
+ }
130
+ }
131
+ /**
132
+ * Checks if observability has been initialized.
133
+ */
134
+ export function is_observability_enabled() {
135
+ return is_initialized;
136
+ }
137
+ // Re-export submodules
138
+ export { get_tracer, with_span, get_trace_context, inject_trace_context } from "./tracer.mjs";
139
+ export { get_sfr_metrics, init_metrics } from "./metrics.mjs";
140
+ export { sfr_logger, create_child_logger, init_logger } from "./logger.mjs";
141
+ export { sfr_rest_telemetry } from "./middleware/rest.mjs";
142
+ export { instrument_socket_io } from "./middleware/ws.mjs";
143
+ export { wrap_mq_handler, inject_mq_trace_context } from "./middleware/mq.mjs";
@@ -0,0 +1,128 @@
1
+ /**
2
+ * SFR Logger Module
3
+ *
4
+ * Enhanced Winston logger with automatic OpenTelemetry trace context injection.
5
+ */
6
+ import winston from "winston";
7
+ import { trace } from "@opentelemetry/api";
8
+ // Define log levels matching standard syslog levels
9
+ const levels = {
10
+ error: 0,
11
+ warn: 1,
12
+ info: 2,
13
+ http: 3,
14
+ verbose: 4,
15
+ debug: 5,
16
+ silly: 6
17
+ };
18
+ const colors = {
19
+ error: "red",
20
+ warn: "yellow",
21
+ info: "green",
22
+ http: "magenta",
23
+ verbose: "cyan",
24
+ debug: "blue",
25
+ silly: "gray"
26
+ };
27
+ winston.addColors(colors);
28
+ let logger_instance = null;
29
+ let service_name = "@avtechno/sfr";
30
+ /**
31
+ * Custom format that injects OpenTelemetry trace context into log entries.
32
+ */
33
+ const trace_context_format = winston.format((info) => {
34
+ const span = trace.getActiveSpan();
35
+ if (span) {
36
+ const ctx = span.spanContext();
37
+ info.trace_id = ctx.traceId;
38
+ info.span_id = ctx.spanId;
39
+ info.trace_flags = ctx.traceFlags;
40
+ }
41
+ return info;
42
+ });
43
+ /**
44
+ * Structured JSON format for production environments.
45
+ * Includes trace context, timestamp, and service metadata.
46
+ */
47
+ const json_format = winston.format.combine(trace_context_format(), winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.errors({ stack: true }), winston.format.json());
48
+ /**
49
+ * Human-readable format for development.
50
+ * Includes colorized output and truncated trace IDs.
51
+ */
52
+ const pretty_format = winston.format.combine(trace_context_format(), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }), winston.format.colorize({ all: true }), winston.format.printf((info) => {
53
+ const trace_suffix = info.trace_id
54
+ ? ` [${String(info.trace_id).slice(0, 8)}]`
55
+ : "";
56
+ let meta_str = "";
57
+ const meta_keys = Object.keys(info).filter((k) => !["level", "message", "timestamp", "trace_id", "span_id", "trace_flags", "service"].includes(k));
58
+ if (meta_keys.length > 0) {
59
+ const meta_obj = {};
60
+ meta_keys.forEach((k) => (meta_obj[k] = info[k]));
61
+ meta_str = ` ${JSON.stringify(meta_obj)}`;
62
+ }
63
+ return `${info.timestamp} ${info.level}${trace_suffix}: ${info.message}${meta_str}`;
64
+ }));
65
+ /**
66
+ * Initializes the SFR logger with the given configuration.
67
+ * Called automatically by init_observability.
68
+ */
69
+ export function init_logger(config = {}) {
70
+ const is_dev = process.env.NODE_ENV === "development";
71
+ const is_debug = Boolean(process.env.DEBUG_SFR);
72
+ const env_format = process.env.SFR_LOG_FORMAT;
73
+ service_name = config.service_name || service_name;
74
+ const format_to_use = config.format || env_format || (is_dev ? "pretty" : "json");
75
+ logger_instance = winston.createLogger({
76
+ level: config.level ?? (is_dev && is_debug ? "debug" : "info"),
77
+ levels,
78
+ format: format_to_use === "json" ? json_format : pretty_format,
79
+ defaultMeta: { service: service_name },
80
+ transports: config.transports ?? [new winston.transports.Console()]
81
+ });
82
+ return logger_instance;
83
+ }
84
+ /**
85
+ * Gets the SFR logger instance.
86
+ * Creates a default instance if not initialized.
87
+ */
88
+ export function get_logger() {
89
+ if (!logger_instance) {
90
+ init_logger();
91
+ }
92
+ return logger_instance;
93
+ }
94
+ /**
95
+ * Creates a child logger with additional context.
96
+ * Child loggers inherit parent configuration and add their own metadata.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const route_logger = create_child_logger({
101
+ * protocol: "REST",
102
+ * route: "/api/users"
103
+ * });
104
+ * route_logger.info("Processing request");
105
+ * ```
106
+ */
107
+ export function create_child_logger(meta) {
108
+ return get_logger().child(meta);
109
+ }
110
+ /**
111
+ * The default SFR logger instance.
112
+ * Automatically includes trace context when available.
113
+ */
114
+ export const sfr_logger = {
115
+ error: (message, meta) => get_logger().error(message, meta),
116
+ warn: (message, meta) => get_logger().warn(message, meta),
117
+ info: (message, meta) => get_logger().info(message, meta),
118
+ http: (message, meta) => get_logger().http(message, meta),
119
+ verbose: (message, meta) => get_logger().verbose(message, meta),
120
+ debug: (message, meta) => get_logger().debug(message, meta),
121
+ silly: (message, meta) => get_logger().silly(message, meta),
122
+ /**
123
+ * Creates a child logger with additional context.
124
+ */
125
+ child: (meta) => create_child_logger(meta)
126
+ };
127
+ // Legacy export for backward compatibility with existing logger.mts
128
+ export { sfr_logger as logger };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * SFR Metrics Module
3
+ *
4
+ * Provides pre-defined metrics for REST, WebSocket, and MQ protocols.
5
+ */
6
+ import { metrics } from "@opentelemetry/api";
7
+ const METER_NAME = "@avtechno/sfr";
8
+ const METER_VERSION = "1.0.0";
9
+ let sfr_metrics = null;
10
+ let meter = null;
11
+ /**
12
+ * Gets the SFR meter instance.
13
+ */
14
+ export function get_meter() {
15
+ if (!meter) {
16
+ meter = metrics.getMeter(METER_NAME, METER_VERSION);
17
+ }
18
+ return meter;
19
+ }
20
+ /**
21
+ * Initializes all SFR metrics.
22
+ * Called automatically by init_observability.
23
+ *
24
+ * @param service_name - Service name for metric labels
25
+ */
26
+ export function init_metrics(service_name) {
27
+ if (sfr_metrics)
28
+ return sfr_metrics;
29
+ const m = get_meter();
30
+ sfr_metrics = {
31
+ // REST Metrics
32
+ rest_requests_total: m.createCounter("sfr.rest.requests.total", {
33
+ description: "Total number of REST requests processed",
34
+ unit: "requests"
35
+ }),
36
+ rest_request_duration: m.createHistogram("sfr.rest.request.duration", {
37
+ description: "Duration of REST request processing",
38
+ unit: "ms",
39
+ advice: {
40
+ explicitBucketBoundaries: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]
41
+ }
42
+ }),
43
+ rest_errors_total: m.createCounter("sfr.rest.errors.total", {
44
+ description: "Total number of REST request errors",
45
+ unit: "errors"
46
+ }),
47
+ rest_request_size: m.createHistogram("sfr.rest.request.size", {
48
+ description: "Size of REST request bodies",
49
+ unit: "bytes"
50
+ }),
51
+ rest_response_size: m.createHistogram("sfr.rest.response.size", {
52
+ description: "Size of REST response bodies",
53
+ unit: "bytes"
54
+ }),
55
+ // WebSocket Metrics
56
+ ws_connections_active: m.createUpDownCounter("sfr.ws.connections.active", {
57
+ description: "Number of currently active WebSocket connections",
58
+ unit: "connections"
59
+ }),
60
+ ws_connections_total: m.createCounter("sfr.ws.connections.total", {
61
+ description: "Total number of WebSocket connections established",
62
+ unit: "connections"
63
+ }),
64
+ ws_events_total: m.createCounter("sfr.ws.events.total", {
65
+ description: "Total number of WebSocket events processed",
66
+ unit: "events"
67
+ }),
68
+ ws_event_duration: m.createHistogram("sfr.ws.event.duration", {
69
+ description: "Duration of WebSocket event processing",
70
+ unit: "ms",
71
+ advice: {
72
+ explicitBucketBoundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000]
73
+ }
74
+ }),
75
+ ws_errors_total: m.createCounter("sfr.ws.errors.total", {
76
+ description: "Total number of WebSocket errors",
77
+ unit: "errors"
78
+ }),
79
+ // MQ Metrics
80
+ mq_messages_received: m.createCounter("sfr.mq.messages.received", {
81
+ description: "Total number of MQ messages received",
82
+ unit: "messages"
83
+ }),
84
+ mq_messages_published: m.createCounter("sfr.mq.messages.published", {
85
+ description: "Total number of MQ messages published",
86
+ unit: "messages"
87
+ }),
88
+ mq_processing_duration: m.createHistogram("sfr.mq.processing.duration", {
89
+ description: "Duration of MQ message processing",
90
+ unit: "ms",
91
+ advice: {
92
+ explicitBucketBoundaries: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
93
+ }
94
+ }),
95
+ mq_errors_total: m.createCounter("sfr.mq.errors.total", {
96
+ description: "Total number of MQ processing errors",
97
+ unit: "errors"
98
+ }),
99
+ mq_messages_rejected: m.createCounter("sfr.mq.messages.rejected", {
100
+ description: "Total number of MQ messages rejected",
101
+ unit: "messages"
102
+ }),
103
+ mq_messages_acked: m.createCounter("sfr.mq.messages.acked", {
104
+ description: "Total number of MQ messages acknowledged",
105
+ unit: "messages"
106
+ })
107
+ };
108
+ return sfr_metrics;
109
+ }
110
+ /**
111
+ * Gets the SFR metrics instance.
112
+ * Initializes with default values if not already initialized.
113
+ */
114
+ export function get_sfr_metrics() {
115
+ return sfr_metrics;
116
+ }
117
+ /**
118
+ * Convenience function to record a REST request.
119
+ */
120
+ export function record_rest_request(attributes) {
121
+ if (!sfr_metrics)
122
+ return;
123
+ const labels = {
124
+ method: attributes.method,
125
+ route: attributes.route,
126
+ status_code: String(attributes.status_code)
127
+ };
128
+ sfr_metrics.rest_requests_total.add(1, labels);
129
+ sfr_metrics.rest_request_duration.record(attributes.duration_ms, labels);
130
+ if (attributes.status_code >= 400) {
131
+ sfr_metrics.rest_errors_total.add(1, labels);
132
+ }
133
+ if (attributes.request_size !== undefined) {
134
+ sfr_metrics.rest_request_size.record(attributes.request_size, { method: attributes.method });
135
+ }
136
+ if (attributes.response_size !== undefined) {
137
+ sfr_metrics.rest_response_size.record(attributes.response_size, { method: attributes.method });
138
+ }
139
+ }
140
+ /**
141
+ * Convenience function to record a WebSocket event.
142
+ */
143
+ export function record_ws_event(attributes) {
144
+ if (!sfr_metrics)
145
+ return;
146
+ const labels = {
147
+ namespace: attributes.namespace,
148
+ event: attributes.event
149
+ };
150
+ sfr_metrics.ws_events_total.add(1, labels);
151
+ sfr_metrics.ws_event_duration.record(attributes.duration_ms, labels);
152
+ if (attributes.error) {
153
+ sfr_metrics.ws_errors_total.add(1, labels);
154
+ }
155
+ }
156
+ /**
157
+ * Convenience function to record an MQ message.
158
+ */
159
+ export function record_mq_message(attributes) {
160
+ if (!sfr_metrics)
161
+ return;
162
+ const labels = {
163
+ queue: attributes.queue,
164
+ pattern: attributes.pattern
165
+ };
166
+ sfr_metrics.mq_messages_received.add(1, labels);
167
+ sfr_metrics.mq_processing_duration.record(attributes.duration_ms, labels);
168
+ if (attributes.error) {
169
+ sfr_metrics.mq_errors_total.add(1, labels);
170
+ }
171
+ if (attributes.rejected) {
172
+ sfr_metrics.mq_messages_rejected.add(1, labels);
173
+ }
174
+ if (attributes.acked) {
175
+ sfr_metrics.mq_messages_acked.add(1, labels);
176
+ }
177
+ }