@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.
- package/README.md +893 -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 +240 -16
- package/dist/example.mjs +0 -33
- package/dist/types/example.d.mts +0 -11
- 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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|