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