@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
@@ -0,0 +1,156 @@
1
+ /**
2
+ * SFR MQ Telemetry Instrumentation
3
+ *
4
+ * Instruments AMQP/RabbitMQ with tracing and metrics.
5
+ */
6
+ import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
7
+ import { get_tracer } from "../tracer.mjs";
8
+ import { get_sfr_metrics, record_mq_message } from "../metrics.mjs";
9
+ import { sfr_logger } from "../logger.mjs";
10
+ /**
11
+ * Wraps an MQ message handler with tracing and metrics.
12
+ * Extracts trace context from message headers for distributed tracing.
13
+ *
14
+ * @param queue - The queue name
15
+ * @param pattern - The communication pattern (e.g., "Point-to-Point", "Fanout")
16
+ * @param handler - The original handler function
17
+ * @returns Instrumented handler function
18
+ */
19
+ export function wrap_mq_handler(queue, pattern, handler) {
20
+ return async (msg) => {
21
+ const start_time = Date.now();
22
+ const tracer = get_tracer();
23
+ const metrics = get_sfr_metrics();
24
+ // Extract trace context from message headers if present
25
+ const headers = (msg.properties?.headers ?? {});
26
+ const parent_context = propagation.extract(context.active(), headers);
27
+ const span = tracer.startSpan(`MQ ${pattern} ${queue}`, {
28
+ kind: SpanKind.CONSUMER,
29
+ attributes: {
30
+ "sfr.protocol": "MQ",
31
+ "messaging.system": "rabbitmq",
32
+ "messaging.destination": queue,
33
+ "messaging.operation": "receive",
34
+ "messaging.message_id": msg.properties?.messageId ?? "unknown",
35
+ "messaging.correlation_id": msg.properties?.correlationId ?? "unknown",
36
+ "sfr.mq.pattern": pattern
37
+ }
38
+ }, parent_context);
39
+ const ctx = trace.setSpan(parent_context, span);
40
+ // Record received metric
41
+ if (metrics) {
42
+ metrics.mq_messages_received.add(1, { queue, pattern });
43
+ }
44
+ try {
45
+ await context.with(ctx, async () => {
46
+ await handler(msg);
47
+ });
48
+ span.setStatus({ code: SpanStatusCode.OK });
49
+ const duration = Date.now() - start_time;
50
+ if (metrics) {
51
+ record_mq_message({
52
+ queue,
53
+ pattern,
54
+ duration_ms: duration
55
+ });
56
+ }
57
+ sfr_logger.debug(`[MQ] Message processed: ${queue}`, {
58
+ queue,
59
+ pattern,
60
+ duration_ms: duration,
61
+ message_id: msg.properties?.messageId
62
+ });
63
+ }
64
+ catch (error) {
65
+ const duration = Date.now() - start_time;
66
+ span.setStatus({
67
+ code: SpanStatusCode.ERROR,
68
+ message: error instanceof Error ? error.message : "Unknown error"
69
+ });
70
+ span.recordException(error);
71
+ if (metrics) {
72
+ record_mq_message({
73
+ queue,
74
+ pattern,
75
+ duration_ms: duration,
76
+ error: true
77
+ });
78
+ }
79
+ sfr_logger.error(`[MQ] Message error: ${queue}`, {
80
+ queue,
81
+ pattern,
82
+ duration_ms: duration,
83
+ error: error instanceof Error ? error.message : "Unknown error"
84
+ });
85
+ throw error;
86
+ }
87
+ finally {
88
+ span.end();
89
+ }
90
+ };
91
+ }
92
+ /**
93
+ * Injects trace context into MQ message headers for distributed tracing.
94
+ * Call this before publishing a message to propagate the trace.
95
+ *
96
+ * @param options - The publish options object (will be modified)
97
+ * @returns The options object with trace context in headers
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const options = inject_mq_trace_context({});
102
+ * channel.publish(exchange, routingKey, content, options);
103
+ * ```
104
+ */
105
+ export function inject_mq_trace_context(options = {}) {
106
+ const headers = (options.headers ?? {});
107
+ propagation.inject(context.active(), headers);
108
+ return {
109
+ ...options,
110
+ headers
111
+ };
112
+ }
113
+ /**
114
+ * Records an MQ publish event for metrics.
115
+ */
116
+ export function record_mq_publish(exchange, pattern) {
117
+ const metrics = get_sfr_metrics();
118
+ if (metrics) {
119
+ metrics.mq_messages_published.add(1, { exchange, pattern });
120
+ }
121
+ sfr_logger.debug(`[MQ] Message published: ${exchange}`, {
122
+ exchange,
123
+ pattern
124
+ });
125
+ }
126
+ /**
127
+ * Records an MQ message acknowledgment.
128
+ */
129
+ export function record_mq_ack(queue, pattern) {
130
+ const metrics = get_sfr_metrics();
131
+ if (metrics) {
132
+ metrics.mq_messages_acked.add(1, { queue, pattern });
133
+ }
134
+ }
135
+ /**
136
+ * Records an MQ message rejection.
137
+ */
138
+ export function record_mq_reject(queue, pattern, requeue) {
139
+ const metrics = get_sfr_metrics();
140
+ if (metrics) {
141
+ metrics.mq_messages_rejected.add(1, { queue, pattern, requeue: String(requeue) });
142
+ }
143
+ sfr_logger.warn(`[MQ] Message rejected: ${queue}`, {
144
+ queue,
145
+ pattern,
146
+ requeue
147
+ });
148
+ }
149
+ /**
150
+ * Extracts trace context from the current span for manual propagation.
151
+ */
152
+ export function get_mq_trace_headers() {
153
+ const headers = {};
154
+ propagation.inject(context.active(), headers);
155
+ return headers;
156
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SFR REST Telemetry Middleware
3
+ *
4
+ * Express middleware that instruments REST handlers with tracing and metrics.
5
+ */
6
+ import { SpanKind, SpanStatusCode, trace, context } from "@opentelemetry/api";
7
+ import { get_tracer, get_trace_context, extract_trace_context } from "../tracer.mjs";
8
+ import { get_sfr_metrics, record_rest_request } from "../metrics.mjs";
9
+ import { sfr_logger } from "../logger.mjs";
10
+ /**
11
+ * Express middleware that adds OpenTelemetry instrumentation to REST handlers.
12
+ *
13
+ * Features:
14
+ * - Creates spans for each request
15
+ * - Extracts trace context from incoming headers (distributed tracing)
16
+ * - Records request/response metrics
17
+ * - Logs HTTP traffic with trace correlation
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * app.use(sfr_rest_telemetry());
22
+ * ```
23
+ */
24
+ export function sfr_rest_telemetry() {
25
+ return (req, res, next) => {
26
+ const tracer = get_tracer();
27
+ const start_time = Date.now();
28
+ // Extract trace context from incoming headers for distributed tracing
29
+ const parent_context = extract_trace_context(req.headers);
30
+ // Determine route pattern (use matched route if available, fallback to path)
31
+ const route = req.route?.path ?? req.path;
32
+ const span = tracer.startSpan(`HTTP ${req.method} ${route}`, {
33
+ kind: SpanKind.SERVER,
34
+ attributes: {
35
+ "http.method": req.method,
36
+ "http.url": req.originalUrl,
37
+ "http.target": req.path,
38
+ "http.host": req.get("host") ?? "unknown",
39
+ "http.scheme": req.protocol,
40
+ "http.user_agent": req.get("user-agent") ?? "unknown",
41
+ "http.request_content_length": req.get("content-length") ?? 0,
42
+ "sfr.protocol": "REST",
43
+ "sfr.route": route
44
+ }
45
+ }, parent_context);
46
+ // Make the span active and attach trace context to request
47
+ const ctx = trace.setSpan(parent_context, span);
48
+ // Attach trace context to request for handler access
49
+ req.sfr_trace = get_trace_context();
50
+ req.sfr_span = span;
51
+ // Run the rest of the middleware chain within this span's context
52
+ context.with(ctx, () => {
53
+ // Capture response by wrapping res.end
54
+ const original_end = res.end;
55
+ const original_json = res.json;
56
+ let response_body_size = 0;
57
+ // Wrap res.json to capture response size
58
+ res.json = function (body) {
59
+ if (body) {
60
+ response_body_size = Buffer.byteLength(JSON.stringify(body), "utf8");
61
+ }
62
+ return original_json.call(this, body);
63
+ };
64
+ res.end = function (...args) {
65
+ const duration = Date.now() - start_time;
66
+ const status_code = res.statusCode;
67
+ // Set span attributes
68
+ span.setAttribute("http.status_code", status_code);
69
+ span.setAttribute("http.response_content_length", response_body_size || res.get("content-length") || 0);
70
+ // Set span status based on HTTP status code
71
+ if (status_code >= 500) {
72
+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status_code}` });
73
+ }
74
+ else if (status_code >= 400) {
75
+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status_code}` });
76
+ }
77
+ else {
78
+ span.setStatus({ code: SpanStatusCode.OK });
79
+ }
80
+ // Record metrics
81
+ const metrics = get_sfr_metrics();
82
+ if (metrics) {
83
+ record_rest_request({
84
+ method: req.method,
85
+ route,
86
+ status_code,
87
+ duration_ms: duration,
88
+ request_size: parseInt(req.get("content-length") || "0", 10),
89
+ response_size: response_body_size
90
+ });
91
+ }
92
+ // Log the request
93
+ const log_level = status_code >= 500 ? "error" : status_code >= 400 ? "warn" : "http";
94
+ sfr_logger[log_level](`${req.method} ${req.path} ${status_code}`, {
95
+ duration_ms: duration,
96
+ status_code,
97
+ method: req.method,
98
+ path: req.path,
99
+ route
100
+ });
101
+ span.end();
102
+ return original_end.apply(res, args);
103
+ };
104
+ next();
105
+ });
106
+ };
107
+ }
108
+ /**
109
+ * Extracts the SFR span from a request.
110
+ * Useful for adding custom attributes or events within handlers.
111
+ */
112
+ export function get_request_span(req) {
113
+ return req.sfr_span;
114
+ }
115
+ /**
116
+ * Extracts the trace context from a request.
117
+ */
118
+ export function get_request_trace(req) {
119
+ return req.sfr_trace ?? null;
120
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * SFR WebSocket Telemetry Instrumentation
3
+ *
4
+ * Instruments Socket.IO with tracing and metrics.
5
+ */
6
+ import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
7
+ import { get_tracer, get_trace_context } from "../tracer.mjs";
8
+ import { get_sfr_metrics, record_ws_event } from "../metrics.mjs";
9
+ import { sfr_logger } from "../logger.mjs";
10
+ /**
11
+ * Instruments a Socket.IO server with OpenTelemetry tracing and metrics.
12
+ *
13
+ * Features:
14
+ * - Tracks active connections
15
+ * - Creates spans for each event
16
+ * - Records event metrics
17
+ * - Logs connection lifecycle with trace correlation
18
+ *
19
+ * @param io - The Socket.IO server instance
20
+ */
21
+ export function instrument_socket_io(io) {
22
+ const metrics = get_sfr_metrics();
23
+ io.on("connection", (socket) => {
24
+ const namespace = socket.nsp.name;
25
+ const socket_id = socket.id;
26
+ // Record connection metrics
27
+ if (metrics) {
28
+ metrics.ws_connections_active.add(1, { namespace });
29
+ metrics.ws_connections_total.add(1, { namespace });
30
+ }
31
+ sfr_logger.info("[WS] Client connected", {
32
+ socket_id,
33
+ namespace,
34
+ remote_address: socket.handshake.address
35
+ });
36
+ // Attach trace context to socket for access in handlers
37
+ socket.sfr_trace = get_trace_context();
38
+ socket.on("disconnect", (reason) => {
39
+ if (metrics) {
40
+ metrics.ws_connections_active.add(-1, { namespace });
41
+ }
42
+ sfr_logger.info("[WS] Client disconnected", {
43
+ socket_id,
44
+ namespace,
45
+ reason
46
+ });
47
+ });
48
+ socket.on("error", (error) => {
49
+ if (metrics) {
50
+ metrics.ws_errors_total.add(1, { namespace, event: "connection" });
51
+ }
52
+ sfr_logger.error("[WS] Socket error", {
53
+ socket_id,
54
+ namespace,
55
+ error: error.message
56
+ });
57
+ });
58
+ });
59
+ }
60
+ /**
61
+ * Wraps a WebSocket event handler with tracing and metrics.
62
+ * Used internally by SFR to instrument registered handlers.
63
+ *
64
+ * @param namespace - The Socket.IO namespace
65
+ * @param event - The event name
66
+ * @param handler - The original handler function
67
+ * @returns Instrumented handler function
68
+ */
69
+ export function wrap_ws_handler(namespace, event, handler) {
70
+ return async (...args) => {
71
+ const start_time = Date.now();
72
+ const tracer = get_tracer();
73
+ const metrics = get_sfr_metrics();
74
+ const span = tracer.startSpan(`WS ${namespace}/${event}`, {
75
+ kind: SpanKind.SERVER,
76
+ attributes: {
77
+ "sfr.protocol": "WS",
78
+ "ws.namespace": namespace,
79
+ "ws.event": event
80
+ }
81
+ });
82
+ const ctx = trace.setSpan(context.active(), span);
83
+ try {
84
+ await context.with(ctx, async () => {
85
+ await handler(...args);
86
+ });
87
+ span.setStatus({ code: SpanStatusCode.OK });
88
+ const duration = Date.now() - start_time;
89
+ if (metrics) {
90
+ record_ws_event({
91
+ namespace,
92
+ event,
93
+ duration_ms: duration
94
+ });
95
+ }
96
+ sfr_logger.debug(`[WS] Event handled: ${event}`, {
97
+ namespace,
98
+ event,
99
+ duration_ms: duration
100
+ });
101
+ }
102
+ catch (error) {
103
+ const duration = Date.now() - start_time;
104
+ span.setStatus({
105
+ code: SpanStatusCode.ERROR,
106
+ message: error instanceof Error ? error.message : "Unknown error"
107
+ });
108
+ span.recordException(error);
109
+ if (metrics) {
110
+ record_ws_event({
111
+ namespace,
112
+ event,
113
+ duration_ms: duration,
114
+ error: true
115
+ });
116
+ }
117
+ sfr_logger.error(`[WS] Event error: ${event}`, {
118
+ namespace,
119
+ event,
120
+ duration_ms: duration,
121
+ error: error instanceof Error ? error.message : "Unknown error"
122
+ });
123
+ throw error;
124
+ }
125
+ finally {
126
+ span.end();
127
+ }
128
+ };
129
+ }
130
+ /**
131
+ * Gets the trace context attached to a socket.
132
+ */
133
+ export function get_socket_trace(socket) {
134
+ return socket.sfr_trace ?? null;
135
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * SFR Tracing Module
3
+ *
4
+ * Provides utilities for creating and managing OpenTelemetry spans.
5
+ */
6
+ import { trace, context, SpanStatusCode, SpanKind, propagation } from "@opentelemetry/api";
7
+ const TRACER_NAME = "@avtechno/sfr";
8
+ const TRACER_VERSION = "1.0.0";
9
+ /**
10
+ * Gets the SFR tracer instance.
11
+ */
12
+ export function get_tracer() {
13
+ return trace.getTracer(TRACER_NAME, TRACER_VERSION);
14
+ }
15
+ /**
16
+ * Wraps an async function with a span for automatic tracing.
17
+ * Handles errors, sets status, and ensures span is ended.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const result = await with_span({
22
+ * name: "db:fetch_user",
23
+ * attributes: { "user.id": userId }
24
+ * }, async (span) => {
25
+ * span.setAttribute("custom.attr", "value");
26
+ * return await db.users.findById(userId);
27
+ * });
28
+ * ```
29
+ */
30
+ export async function with_span(options, fn) {
31
+ const tracer = get_tracer();
32
+ const parent = options.parent_context ?? context.active();
33
+ return tracer.startActiveSpan(options.name, {
34
+ kind: options.kind ?? SpanKind.INTERNAL,
35
+ attributes: options.attributes
36
+ }, parent, async (span) => {
37
+ try {
38
+ const result = await fn(span);
39
+ span.setStatus({ code: SpanStatusCode.OK });
40
+ return result;
41
+ }
42
+ catch (error) {
43
+ span.setStatus({
44
+ code: SpanStatusCode.ERROR,
45
+ message: error instanceof Error ? error.message : "Unknown error"
46
+ });
47
+ span.recordException(error);
48
+ throw error;
49
+ }
50
+ finally {
51
+ span.end();
52
+ }
53
+ });
54
+ }
55
+ /**
56
+ * Synchronous version of with_span for non-async operations.
57
+ */
58
+ export function with_span_sync(options, fn) {
59
+ const tracer = get_tracer();
60
+ const parent = options.parent_context ?? context.active();
61
+ const span = tracer.startSpan(options.name, {
62
+ kind: options.kind ?? SpanKind.INTERNAL,
63
+ attributes: options.attributes
64
+ }, parent);
65
+ try {
66
+ const result = context.with(trace.setSpan(parent, span), () => fn(span));
67
+ span.setStatus({ code: SpanStatusCode.OK });
68
+ return result;
69
+ }
70
+ catch (error) {
71
+ span.setStatus({
72
+ code: SpanStatusCode.ERROR,
73
+ message: error instanceof Error ? error.message : "Unknown error"
74
+ });
75
+ span.recordException(error);
76
+ throw error;
77
+ }
78
+ finally {
79
+ span.end();
80
+ }
81
+ }
82
+ /**
83
+ * Gets the current trace context (trace_id, span_id) if available.
84
+ * Useful for correlating logs and external systems.
85
+ */
86
+ export function get_trace_context() {
87
+ const span = trace.getActiveSpan();
88
+ if (!span)
89
+ return null;
90
+ const ctx = span.spanContext();
91
+ return {
92
+ trace_id: ctx.traceId,
93
+ span_id: ctx.spanId,
94
+ trace_flags: ctx.traceFlags
95
+ };
96
+ }
97
+ /**
98
+ * Injects the current trace context into a carrier object (e.g., HTTP headers, MQ message properties).
99
+ * Used for distributed trace propagation.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const headers = {};
104
+ * inject_trace_context(headers);
105
+ * // headers now contains traceparent, tracestate, etc.
106
+ * ```
107
+ */
108
+ export function inject_trace_context(carrier) {
109
+ propagation.inject(context.active(), carrier);
110
+ }
111
+ /**
112
+ * Extracts trace context from a carrier object and returns the context.
113
+ * Used to continue a distributed trace from an incoming request/message.
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const parent_ctx = extract_trace_context(req.headers);
118
+ * await with_span({ name: "handler", parent_context: parent_ctx }, async (span) => {
119
+ * // This span is a child of the extracted context
120
+ * });
121
+ * ```
122
+ */
123
+ export function extract_trace_context(carrier) {
124
+ return propagation.extract(context.active(), carrier);
125
+ }
126
+ /**
127
+ * Adds attributes to the current active span.
128
+ * No-op if no span is active.
129
+ */
130
+ export function add_span_attributes(attributes) {
131
+ const span = trace.getActiveSpan();
132
+ if (span) {
133
+ Object.entries(attributes).forEach(([key, value]) => {
134
+ span.setAttribute(key, value);
135
+ });
136
+ }
137
+ }
138
+ /**
139
+ * Records an event on the current active span.
140
+ * No-op if no span is active.
141
+ */
142
+ export function add_span_event(name, attributes) {
143
+ const span = trace.getActiveSpan();
144
+ if (span) {
145
+ span.addEvent(name, attributes);
146
+ }
147
+ }
148
+ /**
149
+ * Sets an error status on the current active span.
150
+ * No-op if no span is active.
151
+ */
152
+ export function set_span_error(error) {
153
+ const span = trace.getActiveSpan();
154
+ if (span) {
155
+ span.setStatus({
156
+ code: SpanStatusCode.ERROR,
157
+ message: error.message
158
+ });
159
+ span.recordException(error);
160
+ }
161
+ }
162
+ // Re-export SpanKind for convenience
163
+ export { SpanKind };