@avtechno/sfr 1.0.17 → 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 +241 -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/src/mq.mts CHANGED
@@ -6,6 +6,7 @@ import { ChannelModel, Options, connect, Channel, ConsumeMessage } from "amqplib
6
6
  export class MQLib {
7
7
  private connection?: ChannelModel;
8
8
  private channel?: Channel;
9
+ private is_closing = false;
9
10
 
10
11
  /**
11
12
  * Initializes the connection and channel to the message queue.
@@ -41,6 +42,54 @@ export class MQLib {
41
42
  get_channel() {
42
43
  return this.channel;
43
44
  }
45
+
46
+ /**
47
+ * Gracefully disconnects from the message queue.
48
+ * Closes the channel first, then the connection.
49
+ *
50
+ * @param timeout - Optional timeout in ms to wait for in-flight messages (default: 5000)
51
+ * @example
52
+ * const mqLib = new MQLib();
53
+ * await mqLib.init("amqp://localhost");
54
+ * // ... do work ...
55
+ * await mqLib.disconnect();
56
+ */
57
+ async disconnect(timeout = 5000): Promise<void> {
58
+ if (this.is_closing) return;
59
+ this.is_closing = true;
60
+
61
+ // Allow time for in-flight messages to complete
62
+ await new Promise(resolve => setTimeout(resolve, Math.min(timeout, 1000)));
63
+
64
+ try {
65
+ if (this.channel) {
66
+ await this.channel.close();
67
+ this.channel = undefined;
68
+ }
69
+ } catch (err) {
70
+ // Channel may already be closed
71
+ }
72
+
73
+ try {
74
+ if (this.connection) {
75
+ await this.connection.close();
76
+ this.connection = undefined;
77
+ }
78
+ } catch (err) {
79
+ // Connection may already be closed
80
+ }
81
+
82
+ this.is_closing = false;
83
+ }
84
+
85
+ /**
86
+ * Checks if the connection is currently active.
87
+ *
88
+ * @returns true if connected, false otherwise
89
+ */
90
+ is_connected(): boolean {
91
+ return !!this.connection && !!this.channel && !this.is_closing;
92
+ }
44
93
  }
45
94
 
46
95
  export class BaseMQ{
@@ -0,0 +1,184 @@
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
+
8
+ import { NodeSDK } from "@opentelemetry/sdk-node";
9
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
10
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
11
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
12
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
13
+ import { Resource } from "@opentelemetry/resources";
14
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
15
+ import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
16
+
17
+ import { init_metrics } from "./metrics.mjs";
18
+ import { init_logger } from "./logger.mjs";
19
+
20
+ export interface ObservabilityOptions {
21
+ /** Enable/disable observability (default: true) */
22
+ enabled?: boolean;
23
+ /** OTLP endpoint URL (default: http://localhost:4318) */
24
+ otlp_endpoint?: string;
25
+ /** Enable auto-instrumentation for common libraries (default: true) */
26
+ auto_instrumentation?: boolean;
27
+ /** Sampling ratio 0.0 - 1.0 (default: 1.0) */
28
+ sampling_ratio?: number;
29
+ /** Log format: 'json' for production, 'pretty' for development */
30
+ log_format?: "json" | "pretty";
31
+ /** Additional resource attributes */
32
+ resource_attributes?: Record<string, string>;
33
+ /** Enable debug logging for OTel SDK */
34
+ debug?: boolean;
35
+ }
36
+
37
+ let sdk: NodeSDK | null = null;
38
+ let is_initialized = false;
39
+
40
+ /**
41
+ * Extracts observability configuration from SFR's OASConfig.
42
+ *
43
+ * @param oas_cfg - The OASConfig passed to SFR
44
+ * @param options - Additional observability options
45
+ */
46
+ function extract_config_from_oas(oas_cfg: OASConfig, options: ObservabilityOptions = {}) {
47
+ const service_name = oas_cfg.meta?.service || oas_cfg.title || "sfr-service";
48
+ const service_version = oas_cfg.version || "1.0.0";
49
+ const environment = process.env.NODE_ENV || "development";
50
+
51
+ return {
52
+ service_name,
53
+ service_version,
54
+ environment,
55
+ domain: oas_cfg.meta?.domain,
56
+ service_type: oas_cfg.meta?.type,
57
+ language: oas_cfg.meta?.language,
58
+ port: oas_cfg.meta?.port,
59
+ ...options
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Initializes the OpenTelemetry SDK with configuration derived from OASConfig.
65
+ * Must be called before SFR initialization for full instrumentation.
66
+ *
67
+ * @param oas_cfg - The OASConfig that will be passed to SFR
68
+ * @param options - Additional observability options
69
+ */
70
+ export function init_observability(oas_cfg: OASConfig, options: ObservabilityOptions = {}): void {
71
+ if (is_initialized) {
72
+ console.warn("[SFR Observability] Already initialized, skipping...");
73
+ return;
74
+ }
75
+
76
+ if (options.enabled === false || process.env.SFR_TELEMETRY_ENABLED === "false") {
77
+ console.log("[SFR Observability] Telemetry disabled");
78
+ return;
79
+ }
80
+
81
+ const config = extract_config_from_oas(oas_cfg, options);
82
+
83
+ // Enable debug logging if requested
84
+ if (options.debug || process.env.OTEL_LOG_LEVEL === "debug") {
85
+ diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
86
+ }
87
+
88
+ const otlp_endpoint = options.otlp_endpoint
89
+ || process.env.OTEL_EXPORTER_OTLP_ENDPOINT
90
+ || "http://localhost:4318";
91
+
92
+ // Build resource attributes from OASConfig
93
+ const resource_attributes: Record<string, string> = {
94
+ [ATTR_SERVICE_NAME]: config.service_name,
95
+ [ATTR_SERVICE_VERSION]: config.service_version,
96
+ "deployment.environment": config.environment,
97
+ "sfr.domain": config.domain || "unknown",
98
+ "sfr.service.type": config.service_type || "backend",
99
+ "sfr.language": config.language || "typescript",
100
+ ...options.resource_attributes
101
+ };
102
+
103
+ if (config.port) {
104
+ resource_attributes["sfr.port"] = String(config.port);
105
+ }
106
+
107
+ const resource = new Resource(resource_attributes);
108
+
109
+ // Configure exporters
110
+ const trace_exporter = new OTLPTraceExporter({
111
+ url: `${otlp_endpoint}/v1/traces`
112
+ });
113
+
114
+ // Build instrumentations
115
+ const instrumentations = options.auto_instrumentation !== false
116
+ ? [getNodeAutoInstrumentations({
117
+ "@opentelemetry/instrumentation-fs": { enabled: false }, // Too noisy
118
+ "@opentelemetry/instrumentation-express": { enabled: true },
119
+ "@opentelemetry/instrumentation-http": { enabled: true },
120
+ "@opentelemetry/instrumentation-amqplib": { enabled: true }
121
+ })]
122
+ : [];
123
+
124
+ const metric_reader = new PeriodicExportingMetricReader({
125
+ exporter: new OTLPMetricExporter({
126
+ url: `${otlp_endpoint}/v1/metrics`
127
+ }),
128
+ exportIntervalMillis: 60000 // Export every 60 seconds
129
+ });
130
+
131
+ sdk = new NodeSDK({
132
+ resource,
133
+ traceExporter: trace_exporter,
134
+ // @ts-expect-error - Type mismatch between SDK packages, but compatible at runtime
135
+ metricReader: metric_reader,
136
+ instrumentations
137
+ });
138
+
139
+ sdk.start();
140
+ is_initialized = true;
141
+
142
+ // Initialize SFR-specific metrics
143
+ init_metrics(config.service_name);
144
+
145
+ // Initialize logger with service context
146
+ init_logger({
147
+ service_name: config.service_name,
148
+ format: options.log_format
149
+ });
150
+
151
+ console.log(`[SFR Observability] Initialized for service: ${config.service_name} v${config.service_version}`);
152
+ }
153
+
154
+ /**
155
+ * Gracefully shuts down the OpenTelemetry SDK.
156
+ * Flushes all pending telemetry data before closing.
157
+ */
158
+ export async function shutdown_observability(): Promise<void> {
159
+ if (!sdk) return;
160
+
161
+ try {
162
+ await sdk.shutdown();
163
+ is_initialized = false;
164
+ console.log("[SFR Observability] Shutdown complete");
165
+ } catch (error) {
166
+ console.error("[SFR Observability] Error during shutdown:", error);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Checks if observability has been initialized.
172
+ */
173
+ export function is_observability_enabled(): boolean {
174
+ return is_initialized;
175
+ }
176
+
177
+ // Re-export submodules
178
+ export { get_tracer, with_span, get_trace_context, inject_trace_context } from "./tracer.mjs";
179
+ export { get_sfr_metrics, init_metrics, type SFRMetrics } from "./metrics.mjs";
180
+ export { sfr_logger, create_child_logger, init_logger } from "./logger.mjs";
181
+ export { sfr_rest_telemetry } from "./middleware/rest.mjs";
182
+ export { instrument_socket_io } from "./middleware/ws.mjs";
183
+ export { wrap_mq_handler, inject_mq_trace_context } from "./middleware/mq.mjs";
184
+
@@ -0,0 +1,169 @@
1
+ /**
2
+ * SFR Logger Module
3
+ *
4
+ * Enhanced Winston logger with automatic OpenTelemetry trace context injection.
5
+ */
6
+
7
+ import winston from "winston";
8
+ import { trace } from "@opentelemetry/api";
9
+
10
+ // Define log levels matching standard syslog levels
11
+ const levels = {
12
+ error: 0,
13
+ warn: 1,
14
+ info: 2,
15
+ http: 3,
16
+ verbose: 4,
17
+ debug: 5,
18
+ silly: 6
19
+ };
20
+
21
+ const colors = {
22
+ error: "red",
23
+ warn: "yellow",
24
+ info: "green",
25
+ http: "magenta",
26
+ verbose: "cyan",
27
+ debug: "blue",
28
+ silly: "gray"
29
+ };
30
+
31
+ winston.addColors(colors);
32
+
33
+ export interface LoggerConfig {
34
+ service_name?: string;
35
+ level?: string;
36
+ format?: "json" | "pretty";
37
+ transports?: winston.transport[];
38
+ }
39
+
40
+ let logger_instance: winston.Logger | null = null;
41
+ let service_name = "@avtechno/sfr";
42
+
43
+ /**
44
+ * Custom format that injects OpenTelemetry trace context into log entries.
45
+ */
46
+ const trace_context_format = winston.format((info) => {
47
+ const span = trace.getActiveSpan();
48
+ if (span) {
49
+ const ctx = span.spanContext();
50
+ info.trace_id = ctx.traceId;
51
+ info.span_id = ctx.spanId;
52
+ info.trace_flags = ctx.traceFlags;
53
+ }
54
+ return info;
55
+ });
56
+
57
+ /**
58
+ * Structured JSON format for production environments.
59
+ * Includes trace context, timestamp, and service metadata.
60
+ */
61
+ const json_format = winston.format.combine(
62
+ trace_context_format(),
63
+ winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
64
+ winston.format.errors({ stack: true }),
65
+ winston.format.json()
66
+ );
67
+
68
+ /**
69
+ * Human-readable format for development.
70
+ * Includes colorized output and truncated trace IDs.
71
+ */
72
+ const pretty_format = winston.format.combine(
73
+ trace_context_format(),
74
+ winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
75
+ winston.format.colorize({ all: true }),
76
+ winston.format.printf((info) => {
77
+ const trace_suffix = info.trace_id
78
+ ? ` [${String(info.trace_id).slice(0, 8)}]`
79
+ : "";
80
+
81
+ let meta_str = "";
82
+ const meta_keys = Object.keys(info).filter(
83
+ (k) => !["level", "message", "timestamp", "trace_id", "span_id", "trace_flags", "service"].includes(k)
84
+ );
85
+
86
+ if (meta_keys.length > 0) {
87
+ const meta_obj: Record<string, any> = {};
88
+ meta_keys.forEach((k) => (meta_obj[k] = info[k]));
89
+ meta_str = ` ${JSON.stringify(meta_obj)}`;
90
+ }
91
+
92
+ return `${info.timestamp} ${info.level}${trace_suffix}: ${info.message}${meta_str}`;
93
+ })
94
+ );
95
+
96
+ /**
97
+ * Initializes the SFR logger with the given configuration.
98
+ * Called automatically by init_observability.
99
+ */
100
+ export function init_logger(config: LoggerConfig = {}): winston.Logger {
101
+ const is_dev = process.env.NODE_ENV === "development";
102
+ const is_debug = Boolean(process.env.DEBUG_SFR);
103
+ const env_format = process.env.SFR_LOG_FORMAT as "json" | "pretty" | undefined;
104
+
105
+ service_name = config.service_name || service_name;
106
+
107
+ const format_to_use = config.format || env_format || (is_dev ? "pretty" : "json");
108
+
109
+ logger_instance = winston.createLogger({
110
+ level: config.level ?? (is_dev && is_debug ? "debug" : "info"),
111
+ levels,
112
+ format: format_to_use === "json" ? json_format : pretty_format,
113
+ defaultMeta: { service: service_name },
114
+ transports: config.transports ?? [new winston.transports.Console()]
115
+ });
116
+
117
+ return logger_instance;
118
+ }
119
+
120
+ /**
121
+ * Gets the SFR logger instance.
122
+ * Creates a default instance if not initialized.
123
+ */
124
+ export function get_logger(): winston.Logger {
125
+ if (!logger_instance) {
126
+ init_logger();
127
+ }
128
+ return logger_instance!;
129
+ }
130
+
131
+ /**
132
+ * Creates a child logger with additional context.
133
+ * Child loggers inherit parent configuration and add their own metadata.
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const route_logger = create_child_logger({
138
+ * protocol: "REST",
139
+ * route: "/api/users"
140
+ * });
141
+ * route_logger.info("Processing request");
142
+ * ```
143
+ */
144
+ export function create_child_logger(meta: Record<string, any>): winston.Logger {
145
+ return get_logger().child(meta);
146
+ }
147
+
148
+ /**
149
+ * The default SFR logger instance.
150
+ * Automatically includes trace context when available.
151
+ */
152
+ export const sfr_logger = {
153
+ error: (message: string, meta?: Record<string, any>) => get_logger().error(message, meta),
154
+ warn: (message: string, meta?: Record<string, any>) => get_logger().warn(message, meta),
155
+ info: (message: string, meta?: Record<string, any>) => get_logger().info(message, meta),
156
+ http: (message: string, meta?: Record<string, any>) => get_logger().http(message, meta),
157
+ verbose: (message: string, meta?: Record<string, any>) => get_logger().verbose(message, meta),
158
+ debug: (message: string, meta?: Record<string, any>) => get_logger().debug(message, meta),
159
+ silly: (message: string, meta?: Record<string, any>) => get_logger().silly(message, meta),
160
+
161
+ /**
162
+ * Creates a child logger with additional context.
163
+ */
164
+ child: (meta: Record<string, any>) => create_child_logger(meta)
165
+ };
166
+
167
+ // Legacy export for backward compatibility with existing logger.mts
168
+ export { sfr_logger as logger };
169
+
@@ -0,0 +1,266 @@
1
+ /**
2
+ * SFR Metrics Module
3
+ *
4
+ * Provides pre-defined metrics for REST, WebSocket, and MQ protocols.
5
+ */
6
+
7
+ import {
8
+ metrics,
9
+ type Counter,
10
+ type Histogram,
11
+ type UpDownCounter,
12
+ type Meter
13
+ } from "@opentelemetry/api";
14
+
15
+ const METER_NAME = "@avtechno/sfr";
16
+ const METER_VERSION = "1.0.0";
17
+
18
+ /**
19
+ * Pre-defined SFR metrics for all protocols.
20
+ */
21
+ export interface SFRMetrics {
22
+ // REST Metrics
23
+ rest_requests_total: Counter;
24
+ rest_request_duration: Histogram;
25
+ rest_errors_total: Counter;
26
+ rest_request_size: Histogram;
27
+ rest_response_size: Histogram;
28
+
29
+ // WebSocket Metrics
30
+ ws_connections_active: UpDownCounter;
31
+ ws_connections_total: Counter;
32
+ ws_events_total: Counter;
33
+ ws_event_duration: Histogram;
34
+ ws_errors_total: Counter;
35
+
36
+ // MQ Metrics
37
+ mq_messages_received: Counter;
38
+ mq_messages_published: Counter;
39
+ mq_processing_duration: Histogram;
40
+ mq_errors_total: Counter;
41
+ mq_messages_rejected: Counter;
42
+ mq_messages_acked: Counter;
43
+ }
44
+
45
+ let sfr_metrics: SFRMetrics | null = null;
46
+ let meter: Meter | null = null;
47
+
48
+ /**
49
+ * Gets the SFR meter instance.
50
+ */
51
+ export function get_meter(): Meter {
52
+ if (!meter) {
53
+ meter = metrics.getMeter(METER_NAME, METER_VERSION);
54
+ }
55
+ return meter;
56
+ }
57
+
58
+ /**
59
+ * Initializes all SFR metrics.
60
+ * Called automatically by init_observability.
61
+ *
62
+ * @param service_name - Service name for metric labels
63
+ */
64
+ export function init_metrics(service_name: string): SFRMetrics {
65
+ if (sfr_metrics) return sfr_metrics;
66
+
67
+ const m = get_meter();
68
+
69
+ sfr_metrics = {
70
+ // REST Metrics
71
+ rest_requests_total: m.createCounter("sfr.rest.requests.total", {
72
+ description: "Total number of REST requests processed",
73
+ unit: "requests"
74
+ }),
75
+
76
+ rest_request_duration: m.createHistogram("sfr.rest.request.duration", {
77
+ description: "Duration of REST request processing",
78
+ unit: "ms",
79
+ advice: {
80
+ explicitBucketBoundaries: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]
81
+ }
82
+ }),
83
+
84
+ rest_errors_total: m.createCounter("sfr.rest.errors.total", {
85
+ description: "Total number of REST request errors",
86
+ unit: "errors"
87
+ }),
88
+
89
+ rest_request_size: m.createHistogram("sfr.rest.request.size", {
90
+ description: "Size of REST request bodies",
91
+ unit: "bytes"
92
+ }),
93
+
94
+ rest_response_size: m.createHistogram("sfr.rest.response.size", {
95
+ description: "Size of REST response bodies",
96
+ unit: "bytes"
97
+ }),
98
+
99
+ // WebSocket Metrics
100
+ ws_connections_active: m.createUpDownCounter("sfr.ws.connections.active", {
101
+ description: "Number of currently active WebSocket connections",
102
+ unit: "connections"
103
+ }),
104
+
105
+ ws_connections_total: m.createCounter("sfr.ws.connections.total", {
106
+ description: "Total number of WebSocket connections established",
107
+ unit: "connections"
108
+ }),
109
+
110
+ ws_events_total: m.createCounter("sfr.ws.events.total", {
111
+ description: "Total number of WebSocket events processed",
112
+ unit: "events"
113
+ }),
114
+
115
+ ws_event_duration: m.createHistogram("sfr.ws.event.duration", {
116
+ description: "Duration of WebSocket event processing",
117
+ unit: "ms",
118
+ advice: {
119
+ explicitBucketBoundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000]
120
+ }
121
+ }),
122
+
123
+ ws_errors_total: m.createCounter("sfr.ws.errors.total", {
124
+ description: "Total number of WebSocket errors",
125
+ unit: "errors"
126
+ }),
127
+
128
+ // MQ Metrics
129
+ mq_messages_received: m.createCounter("sfr.mq.messages.received", {
130
+ description: "Total number of MQ messages received",
131
+ unit: "messages"
132
+ }),
133
+
134
+ mq_messages_published: m.createCounter("sfr.mq.messages.published", {
135
+ description: "Total number of MQ messages published",
136
+ unit: "messages"
137
+ }),
138
+
139
+ mq_processing_duration: m.createHistogram("sfr.mq.processing.duration", {
140
+ description: "Duration of MQ message processing",
141
+ unit: "ms",
142
+ advice: {
143
+ explicitBucketBoundaries: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]
144
+ }
145
+ }),
146
+
147
+ mq_errors_total: m.createCounter("sfr.mq.errors.total", {
148
+ description: "Total number of MQ processing errors",
149
+ unit: "errors"
150
+ }),
151
+
152
+ mq_messages_rejected: m.createCounter("sfr.mq.messages.rejected", {
153
+ description: "Total number of MQ messages rejected",
154
+ unit: "messages"
155
+ }),
156
+
157
+ mq_messages_acked: m.createCounter("sfr.mq.messages.acked", {
158
+ description: "Total number of MQ messages acknowledged",
159
+ unit: "messages"
160
+ })
161
+ };
162
+
163
+ return sfr_metrics;
164
+ }
165
+
166
+ /**
167
+ * Gets the SFR metrics instance.
168
+ * Initializes with default values if not already initialized.
169
+ */
170
+ export function get_sfr_metrics(): SFRMetrics | null {
171
+ return sfr_metrics;
172
+ }
173
+
174
+ /**
175
+ * Convenience function to record a REST request.
176
+ */
177
+ export function record_rest_request(attributes: {
178
+ method: string;
179
+ route: string;
180
+ status_code: number;
181
+ duration_ms: number;
182
+ request_size?: number;
183
+ response_size?: number;
184
+ }): void {
185
+ if (!sfr_metrics) return;
186
+
187
+ const labels = {
188
+ method: attributes.method,
189
+ route: attributes.route,
190
+ status_code: String(attributes.status_code)
191
+ };
192
+
193
+ sfr_metrics.rest_requests_total.add(1, labels);
194
+ sfr_metrics.rest_request_duration.record(attributes.duration_ms, labels);
195
+
196
+ if (attributes.status_code >= 400) {
197
+ sfr_metrics.rest_errors_total.add(1, labels);
198
+ }
199
+
200
+ if (attributes.request_size !== undefined) {
201
+ sfr_metrics.rest_request_size.record(attributes.request_size, { method: attributes.method });
202
+ }
203
+
204
+ if (attributes.response_size !== undefined) {
205
+ sfr_metrics.rest_response_size.record(attributes.response_size, { method: attributes.method });
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Convenience function to record a WebSocket event.
211
+ */
212
+ export function record_ws_event(attributes: {
213
+ namespace: string;
214
+ event: string;
215
+ duration_ms: number;
216
+ error?: boolean;
217
+ }): void {
218
+ if (!sfr_metrics) return;
219
+
220
+ const labels = {
221
+ namespace: attributes.namespace,
222
+ event: attributes.event
223
+ };
224
+
225
+ sfr_metrics.ws_events_total.add(1, labels);
226
+ sfr_metrics.ws_event_duration.record(attributes.duration_ms, labels);
227
+
228
+ if (attributes.error) {
229
+ sfr_metrics.ws_errors_total.add(1, labels);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Convenience function to record an MQ message.
235
+ */
236
+ export function record_mq_message(attributes: {
237
+ queue: string;
238
+ pattern: string;
239
+ duration_ms: number;
240
+ error?: boolean;
241
+ rejected?: boolean;
242
+ acked?: boolean;
243
+ }): void {
244
+ if (!sfr_metrics) return;
245
+
246
+ const labels = {
247
+ queue: attributes.queue,
248
+ pattern: attributes.pattern
249
+ };
250
+
251
+ sfr_metrics.mq_messages_received.add(1, labels);
252
+ sfr_metrics.mq_processing_duration.record(attributes.duration_ms, labels);
253
+
254
+ if (attributes.error) {
255
+ sfr_metrics.mq_errors_total.add(1, labels);
256
+ }
257
+
258
+ if (attributes.rejected) {
259
+ sfr_metrics.mq_messages_rejected.add(1, labels);
260
+ }
261
+
262
+ if (attributes.acked) {
263
+ sfr_metrics.mq_messages_acked.add(1, labels);
264
+ }
265
+ }
266
+