@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.
- package/README.md +890 -45
- package/dist/index.mjs +20 -2
- package/dist/logger.mjs +9 -37
- package/dist/mq.mjs +46 -0
- package/dist/observability/index.mjs +143 -0
- package/dist/observability/logger.mjs +128 -0
- package/dist/observability/metrics.mjs +177 -0
- package/dist/observability/middleware/mq.mjs +156 -0
- package/dist/observability/middleware/rest.mjs +120 -0
- package/dist/observability/middleware/ws.mjs +135 -0
- package/dist/observability/tracer.mjs +163 -0
- package/dist/sfr-pipeline.mjs +412 -12
- package/dist/templates.mjs +8 -1
- package/dist/types/index.d.mts +8 -1
- package/dist/types/logger.d.mts +9 -3
- package/dist/types/mq.d.mts +19 -0
- package/dist/types/observability/index.d.mts +45 -0
- package/dist/types/observability/logger.d.mts +54 -0
- package/dist/types/observability/metrics.d.mts +74 -0
- package/dist/types/observability/middleware/mq.d.mts +46 -0
- package/dist/types/observability/middleware/rest.d.mts +33 -0
- package/dist/types/observability/middleware/ws.d.mts +35 -0
- package/dist/types/observability/tracer.d.mts +90 -0
- package/dist/types/sfr-pipeline.d.mts +42 -1
- package/dist/types/templates.d.mts +1 -6
- package/package.json +29 -4
- package/src/index.mts +66 -3
- package/src/logger.mts +16 -51
- package/src/mq.mts +49 -0
- package/src/observability/index.mts +184 -0
- package/src/observability/logger.mts +169 -0
- package/src/observability/metrics.mts +266 -0
- package/src/observability/middleware/mq.mts +187 -0
- package/src/observability/middleware/rest.mts +143 -0
- package/src/observability/middleware/ws.mts +162 -0
- package/src/observability/tracer.mts +205 -0
- package/src/sfr-pipeline.mts +468 -18
- package/src/templates.mts +14 -5
- package/src/types/index.d.ts +241 -16
- package/dist/example.mjs +0 -33
- package/dist/types/example.d.mts +0 -11
- 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
|
+
|