@avtechno/sfr 1.0.18 → 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 +240 -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,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
+