@superblocksteam/shared 0.9131.0 → 0.9133.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.
@@ -0,0 +1,43 @@
1
+ import { Tracer } from '@opentelemetry/api';
2
+ import WebSocket from 'isomorphic-ws';
3
+ import { ISocket, SocketError, SocketMessage } from './socket';
4
+ import { GenericMiddleware, ISocketClient, MethodHandler, MethodHandlers, RequestContextBase, SocketTimeouts } from './types';
5
+ export declare class TracedSocket<ImplementedMethods, CallableMethods, RequestContext extends RequestContextBase> extends ISocket<ImplementedMethods, CallableMethods, RequestContext> {
6
+ /**
7
+ * The TracedSocket class extends the ISocket class to add tracing capabilities for each connection. It manages
8
+ * spans for handler calls to provide detailed tracing information.
9
+ *
10
+ * Unlike Express, the ISocket handler call chain is fully recursive. This recursion makes it challenging to determine
11
+ * when to create and end spans for specific handlers, as handlers may call other handlers before returning a result.
12
+ *
13
+ * An example span for recursive call
14
+ * Main span ----------------------------------------------------------------------------------
15
+ * Handler1 ----------------------------------------------------------------------------
16
+ * Handler2 ----------------------------------------------------------------------
17
+ *
18
+ * To manage this, we keep a reference to the most recently created span and complete that span when we go one step deeper into the recursion.
19
+ * This approach works for now since we don't support much parallelism, but it will need to be updated when we start handling requests concurrently.
20
+ *
21
+ * An example span for recursive call after the above fixes
22
+ * Main span ----------------------------------------------------------------------------------
23
+ * Handler1 -----------------------------
24
+ * Handler2 ------------------------------------------------
25
+ *
26
+ */
27
+ private activeSpanByRequestId;
28
+ private middlewareSpanByReqId;
29
+ private lastActiveSpan;
30
+ private tracer;
31
+ constructor(ws: WebSocket, requestHandlers: MethodHandlers<ImplementedMethods, CallableMethods, RequestContext>, globalMiddlewares: GenericMiddleware<CallableMethods, RequestContext>[], tracer: Tracer, timeouts?: SocketTimeouts, logger?: {
32
+ error: (message?: string) => void;
33
+ });
34
+ protected callHandler<Params, Result, CallableMethods, RequestContext extends RequestContextBase>(handler: MethodHandler<Params, Result, CallableMethods, RequestContext>, params: Params, ctx: RequestContext, client: ISocketClient<CallableMethods>, next: () => Promise<Result>): Promise<Result>;
35
+ protected handleMessage(message: SocketMessage): Promise<void>;
36
+ request<Params, Result>(method: string, params: Params, authorization?: string): Promise<Result>;
37
+ protected decorateToSend(message: SocketMessage): SocketMessage;
38
+ protected respond<Result>(requestId: number, result: Result): void;
39
+ protected respondError(requestId: number, error: SocketError, exception?: Error): void;
40
+ private addEvent;
41
+ close(): void;
42
+ }
43
+ //# sourceMappingURL=tracedSocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracedSocket.d.ts","sourceRoot":"","sources":["../../src/socket/tracedSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,MAAM,EAEP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,SAAS,MAAM,eAAe,CAAC;AAUtC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAmC9H,qBAAa,YAAY,CAAC,kBAAkB,EAAE,eAAe,EAAE,cAAc,SAAS,kBAAkB,CAAE,SAAQ,OAAO,CACvH,kBAAkB,EAClB,eAAe,EACf,cAAc,CACf;IACC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,qBAAqB,CAA2B;IACxD,OAAO,CAAC,qBAAqB,CAA2B;IACxD,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,MAAM,CAAS;gBAGrB,EAAE,EAAE,SAAS,EACb,eAAe,EAAE,cAAc,CAAC,kBAAkB,EAAE,eAAe,EAAE,cAAc,CAAC,EACpF,iBAAiB,EAAE,iBAAiB,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,EACvE,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,cAAc,EACzB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;cAMhC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,SAAS,kBAAkB,EACpG,OAAO,EAAE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,CAAC,EACvE,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EACtC,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,MAAM,CAAC;cAkCF,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKvG,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa;IAK/D,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAKlE,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,IAAI;IAStF,OAAO,CAAC,QAAQ;IAQT,KAAK,IAAI,IAAI;CAQrB"}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TracedSocket = void 0;
4
+ const api_1 = require("@opentelemetry/api");
5
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
6
+ const observability_1 = require("../observability");
7
+ const socket_1 = require("./socket");
8
+ function isPromise(obj) {
9
+ return obj !== null && typeof obj === 'object' && typeof obj.then === 'function';
10
+ }
11
+ // Taken from https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-ws/src/index.ts#L21
12
+ const endSpan = (traced, span) => {
13
+ try {
14
+ const result = traced();
15
+ if (isPromise(result)) {
16
+ return Promise.resolve(result)
17
+ .catch((err) => {
18
+ setHttpStatusFromError(span, typeof err === 'string' ? new Error(err) : err);
19
+ throw err;
20
+ })
21
+ .finally(() => span.end());
22
+ }
23
+ else {
24
+ span.end();
25
+ return result;
26
+ }
27
+ }
28
+ catch (error) {
29
+ setHttpStatusFromError(span, error);
30
+ span.end();
31
+ throw error;
32
+ }
33
+ };
34
+ function setHttpStatusFromError(span, error) {
35
+ // Simplified error handling without server-side dependencies
36
+ span.setAttribute(observability_1.OBS_TAG_HTTP_STATUS_CODE, 500);
37
+ span.recordException(error);
38
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
39
+ }
40
+ class TracedSocket extends socket_1.ISocket {
41
+ constructor(ws, requestHandlers, globalMiddlewares, tracer, timeouts, logger) {
42
+ super(ws, requestHandlers, globalMiddlewares, timeouts, logger);
43
+ /**
44
+ * The TracedSocket class extends the ISocket class to add tracing capabilities for each connection. It manages
45
+ * spans for handler calls to provide detailed tracing information.
46
+ *
47
+ * Unlike Express, the ISocket handler call chain is fully recursive. This recursion makes it challenging to determine
48
+ * when to create and end spans for specific handlers, as handlers may call other handlers before returning a result.
49
+ *
50
+ * An example span for recursive call
51
+ * Main span ----------------------------------------------------------------------------------
52
+ * Handler1 ----------------------------------------------------------------------------
53
+ * Handler2 ----------------------------------------------------------------------
54
+ *
55
+ * To manage this, we keep a reference to the most recently created span and complete that span when we go one step deeper into the recursion.
56
+ * This approach works for now since we don't support much parallelism, but it will need to be updated when we start handling requests concurrently.
57
+ *
58
+ * An example span for recursive call after the above fixes
59
+ * Main span ----------------------------------------------------------------------------------
60
+ * Handler1 -----------------------------
61
+ * Handler2 ------------------------------------------------
62
+ *
63
+ */
64
+ this.activeSpanByRequestId = new Map();
65
+ this.middlewareSpanByReqId = new Map();
66
+ this.tracer = tracer;
67
+ }
68
+ async callHandler(handler, params, ctx, client, next) {
69
+ let currentContext = api_1.context.active();
70
+ const middleWareSpan = this.middlewareSpanByReqId.get(ctx.requestId);
71
+ const activeSpan = this.activeSpanByRequestId.get(ctx.requestId);
72
+ // We need to end the middleware span since callHandler is called recursively
73
+ // Otherwise spans will be nested which is not representing actual time taken to execute a middleware
74
+ // Since we are ending the span here, it won't capture error state of nested handlers
75
+ if (middleWareSpan) {
76
+ middleWareSpan.end();
77
+ if (activeSpan) {
78
+ currentContext = api_1.trace.setSpan(api_1.context.active(), activeSpan);
79
+ }
80
+ }
81
+ let result;
82
+ await api_1.context.with(currentContext, async () => {
83
+ await this.tracer.startActiveSpan(`WS HANDLER ${handler.name === '' ? ctx.method : handler.name}`, {
84
+ attributes: {
85
+ [semantic_conventions_1.SEMATTRS_MESSAGING_SYSTEM]: 'ws',
86
+ [semantic_conventions_1.SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket'
87
+ },
88
+ kind: api_1.SpanKind.SERVER
89
+ }, async (span) => {
90
+ this.middlewareSpanByReqId.set(ctx.requestId, span);
91
+ result = await endSpan(() => super.callHandler(handler, params, ctx, client, next), span);
92
+ this.middlewareSpanByReqId.delete(ctx.requestId);
93
+ });
94
+ });
95
+ return result;
96
+ }
97
+ async handleMessage(message) {
98
+ if (message.request) {
99
+ const spanName = message.request.method;
100
+ const requestId = message.request.id;
101
+ const payload = message.request.payload;
102
+ await api_1.context.with(api_1.propagation.extract(api_1.ROOT_CONTEXT, message.request), async () => await this.tracer.startActiveSpan(`WS SERVER ${spanName}`, {
103
+ attributes: {
104
+ [semantic_conventions_1.SEMATTRS_MESSAGING_SYSTEM]: 'ws',
105
+ [semantic_conventions_1.SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket',
106
+ [observability_1.OBS_TAG_HTTP_ROUTE]: spanName,
107
+ [observability_1.OBS_TAG_APPLICATION_ID]: payload?.['applicationId'],
108
+ [observability_1.OBS_TAG_API_ID]: payload?.['apiId'],
109
+ [observability_1.OBS_TAG_BRANCH]: payload?.['branch'] ?? payload?.['branchName'],
110
+ [observability_1.OBS_TAG_COMMIT_ID]: payload?.['commitId']
111
+ },
112
+ kind: api_1.SpanKind.SERVER
113
+ }, async (span) => {
114
+ this.lastActiveSpan = span;
115
+ this.activeSpanByRequestId.set(requestId, span);
116
+ const result = await endSpan(() => super.handleMessage(message), span);
117
+ this.activeSpanByRequestId.delete(requestId);
118
+ return result;
119
+ }));
120
+ }
121
+ else if (message.response) {
122
+ this.addEvent('ws.received-response', { ['ws.response.id']: message.response.id });
123
+ return await super.handleMessage(message);
124
+ }
125
+ }
126
+ request(method, params, authorization) {
127
+ this.addEvent('ws.send-request', { ['ws.request.method']: method, ['ws.request.id']: this.nxtRequestId });
128
+ return super.request(method, params, authorization);
129
+ }
130
+ decorateToSend(message) {
131
+ api_1.propagation.inject(api_1.context.active(), message.request);
132
+ return message;
133
+ }
134
+ respond(requestId, result) {
135
+ this.addEvent('ws.send-response', { ['ws.response.id']: requestId });
136
+ return super.respond(requestId, result);
137
+ }
138
+ respondError(requestId, error, exception) {
139
+ this.addEvent('ws.send-error', { ['ws.response.id']: requestId });
140
+ if (this.lastActiveSpan) {
141
+ setHttpStatusFromError(this.lastActiveSpan, exception ?? new Error(error.message));
142
+ this.lastActiveSpan.setAttribute(observability_1.OBS_SOCKET_STATUS_CODE, error.code);
143
+ }
144
+ return super.respondError(requestId, error, exception);
145
+ }
146
+ addEvent(eventName, attributes) {
147
+ // Since we do not have parent requestId in all the methods, we are assuming latest created span is the only active span
148
+ // This assumption can be broken if we start sending multiple request through the same connection.
149
+ if (this.lastActiveSpan) {
150
+ this.lastActiveSpan.addEvent(eventName, attributes, new Date());
151
+ }
152
+ }
153
+ close() {
154
+ for (const span of this.activeSpanByRequestId.values()) {
155
+ span.end();
156
+ }
157
+ this.activeSpanByRequestId = new Map();
158
+ this.lastActiveSpan = undefined;
159
+ super.close();
160
+ }
161
+ }
162
+ exports.TracedSocket = TracedSocket;
163
+ //# sourceMappingURL=tracedSocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracedSocket.js","sourceRoot":"","sources":["../../src/socket/tracedSocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { Tracer } from '@opentelemetry/api';
2
+ import WebSocket from 'isomorphic-ws';
3
+ import { ISocket, SocketError, SocketMessage } from './socket';
4
+ import { GenericMiddleware, ISocketClient, MethodHandler, MethodHandlers, RequestContextBase, SocketTimeouts } from './types';
5
+ export declare class TracedSocket<ImplementedMethods, CallableMethods, RequestContext extends RequestContextBase> extends ISocket<ImplementedMethods, CallableMethods, RequestContext> {
6
+ /**
7
+ * The TracedSocket class extends the ISocket class to add tracing capabilities for each connection. It manages
8
+ * spans for handler calls to provide detailed tracing information.
9
+ *
10
+ * Unlike Express, the ISocket handler call chain is fully recursive. This recursion makes it challenging to determine
11
+ * when to create and end spans for specific handlers, as handlers may call other handlers before returning a result.
12
+ *
13
+ * An example span for recursive call
14
+ * Main span ----------------------------------------------------------------------------------
15
+ * Handler1 ----------------------------------------------------------------------------
16
+ * Handler2 ----------------------------------------------------------------------
17
+ *
18
+ * To manage this, we keep a reference to the most recently created span and complete that span when we go one step deeper into the recursion.
19
+ * This approach works for now since we don't support much parallelism, but it will need to be updated when we start handling requests concurrently.
20
+ *
21
+ * An example span for recursive call after the above fixes
22
+ * Main span ----------------------------------------------------------------------------------
23
+ * Handler1 -----------------------------
24
+ * Handler2 ------------------------------------------------
25
+ *
26
+ */
27
+ private activeSpanByRequestId;
28
+ private middlewareSpanByReqId;
29
+ private lastActiveSpan;
30
+ private tracer;
31
+ constructor(ws: WebSocket, requestHandlers: MethodHandlers<ImplementedMethods, CallableMethods, RequestContext>, globalMiddlewares: GenericMiddleware<CallableMethods, RequestContext>[], tracer: Tracer, timeouts?: SocketTimeouts, logger?: {
32
+ error: (message?: string) => void;
33
+ });
34
+ protected callHandler<Params, Result, CallableMethods, RequestContext extends RequestContextBase>(handler: MethodHandler<Params, Result, CallableMethods, RequestContext>, params: Params, ctx: RequestContext, client: ISocketClient<CallableMethods>, next: () => Promise<Result>): Promise<Result>;
35
+ protected handleMessage(message: SocketMessage): Promise<void>;
36
+ request<Params, Result>(method: string, params: Params, authorization?: string): Promise<Result>;
37
+ protected decorateToSend(message: SocketMessage): SocketMessage;
38
+ protected respond<Result>(requestId: number, result: Result): void;
39
+ protected respondError(requestId: number, error: SocketError, exception?: Error): void;
40
+ private addEvent;
41
+ close(): void;
42
+ }
43
+ //# sourceMappingURL=tracedSocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracedSocket.d.ts","sourceRoot":"","sources":["../../src/socket/tracedSocket.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,MAAM,EAEP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,SAAS,MAAM,eAAe,CAAC;AAUtC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAmC9H,qBAAa,YAAY,CAAC,kBAAkB,EAAE,eAAe,EAAE,cAAc,SAAS,kBAAkB,CAAE,SAAQ,OAAO,CACvH,kBAAkB,EAClB,eAAe,EACf,cAAc,CACf;IACC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,qBAAqB,CAA2B;IACxD,OAAO,CAAC,qBAAqB,CAA2B;IACxD,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,MAAM,CAAS;gBAGrB,EAAE,EAAE,SAAS,EACb,eAAe,EAAE,cAAc,CAAC,kBAAkB,EAAE,eAAe,EAAE,cAAc,CAAC,EACpF,iBAAiB,EAAE,iBAAiB,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,EACvE,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,cAAc,EACzB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;cAMhC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,SAAS,kBAAkB,EACpG,OAAO,EAAE,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,cAAc,CAAC,EACvE,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,EACtC,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,MAAM,CAAC;cAkCF,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKvG,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa;IAK/D,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAKlE,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,IAAI;IAStF,OAAO,CAAC,QAAQ;IAQT,KAAK,IAAI,IAAI;CAQrB"}
@@ -0,0 +1,159 @@
1
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context as otelCtx, propagation, trace } from '@opentelemetry/api';
2
+ import { SEMATTRS_MESSAGING_DESTINATION_KIND, SEMATTRS_MESSAGING_SYSTEM } from '@opentelemetry/semantic-conventions';
3
+ import { OBS_SOCKET_STATUS_CODE, OBS_TAG_API_ID, OBS_TAG_APPLICATION_ID, OBS_TAG_BRANCH, OBS_TAG_COMMIT_ID, OBS_TAG_HTTP_ROUTE, OBS_TAG_HTTP_STATUS_CODE } from '../observability';
4
+ import { ISocket } from './socket';
5
+ function isPromise(obj) {
6
+ return obj !== null && typeof obj === 'object' && typeof obj.then === 'function';
7
+ }
8
+ // Taken from https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-ws/src/index.ts#L21
9
+ const endSpan = (traced, span) => {
10
+ try {
11
+ const result = traced();
12
+ if (isPromise(result)) {
13
+ return Promise.resolve(result)
14
+ .catch((err) => {
15
+ setHttpStatusFromError(span, typeof err === 'string' ? new Error(err) : err);
16
+ throw err;
17
+ })
18
+ .finally(() => span.end());
19
+ }
20
+ else {
21
+ span.end();
22
+ return result;
23
+ }
24
+ }
25
+ catch (error) {
26
+ setHttpStatusFromError(span, error);
27
+ span.end();
28
+ throw error;
29
+ }
30
+ };
31
+ function setHttpStatusFromError(span, error) {
32
+ // Simplified error handling without server-side dependencies
33
+ span.setAttribute(OBS_TAG_HTTP_STATUS_CODE, 500);
34
+ span.recordException(error);
35
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
36
+ }
37
+ export class TracedSocket extends ISocket {
38
+ constructor(ws, requestHandlers, globalMiddlewares, tracer, timeouts, logger) {
39
+ super(ws, requestHandlers, globalMiddlewares, timeouts, logger);
40
+ /**
41
+ * The TracedSocket class extends the ISocket class to add tracing capabilities for each connection. It manages
42
+ * spans for handler calls to provide detailed tracing information.
43
+ *
44
+ * Unlike Express, the ISocket handler call chain is fully recursive. This recursion makes it challenging to determine
45
+ * when to create and end spans for specific handlers, as handlers may call other handlers before returning a result.
46
+ *
47
+ * An example span for recursive call
48
+ * Main span ----------------------------------------------------------------------------------
49
+ * Handler1 ----------------------------------------------------------------------------
50
+ * Handler2 ----------------------------------------------------------------------
51
+ *
52
+ * To manage this, we keep a reference to the most recently created span and complete that span when we go one step deeper into the recursion.
53
+ * This approach works for now since we don't support much parallelism, but it will need to be updated when we start handling requests concurrently.
54
+ *
55
+ * An example span for recursive call after the above fixes
56
+ * Main span ----------------------------------------------------------------------------------
57
+ * Handler1 -----------------------------
58
+ * Handler2 ------------------------------------------------
59
+ *
60
+ */
61
+ this.activeSpanByRequestId = new Map();
62
+ this.middlewareSpanByReqId = new Map();
63
+ this.tracer = tracer;
64
+ }
65
+ async callHandler(handler, params, ctx, client, next) {
66
+ let currentContext = otelCtx.active();
67
+ const middleWareSpan = this.middlewareSpanByReqId.get(ctx.requestId);
68
+ const activeSpan = this.activeSpanByRequestId.get(ctx.requestId);
69
+ // We need to end the middleware span since callHandler is called recursively
70
+ // Otherwise spans will be nested which is not representing actual time taken to execute a middleware
71
+ // Since we are ending the span here, it won't capture error state of nested handlers
72
+ if (middleWareSpan) {
73
+ middleWareSpan.end();
74
+ if (activeSpan) {
75
+ currentContext = trace.setSpan(otelCtx.active(), activeSpan);
76
+ }
77
+ }
78
+ let result;
79
+ await otelCtx.with(currentContext, async () => {
80
+ await this.tracer.startActiveSpan(`WS HANDLER ${handler.name === '' ? ctx.method : handler.name}`, {
81
+ attributes: {
82
+ [SEMATTRS_MESSAGING_SYSTEM]: 'ws',
83
+ [SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket'
84
+ },
85
+ kind: SpanKind.SERVER
86
+ }, async (span) => {
87
+ this.middlewareSpanByReqId.set(ctx.requestId, span);
88
+ result = await endSpan(() => super.callHandler(handler, params, ctx, client, next), span);
89
+ this.middlewareSpanByReqId.delete(ctx.requestId);
90
+ });
91
+ });
92
+ return result;
93
+ }
94
+ async handleMessage(message) {
95
+ if (message.request) {
96
+ const spanName = message.request.method;
97
+ const requestId = message.request.id;
98
+ const payload = message.request.payload;
99
+ await otelCtx.with(propagation.extract(ROOT_CONTEXT, message.request), async () => await this.tracer.startActiveSpan(`WS SERVER ${spanName}`, {
100
+ attributes: {
101
+ [SEMATTRS_MESSAGING_SYSTEM]: 'ws',
102
+ [SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket',
103
+ [OBS_TAG_HTTP_ROUTE]: spanName,
104
+ [OBS_TAG_APPLICATION_ID]: payload?.['applicationId'],
105
+ [OBS_TAG_API_ID]: payload?.['apiId'],
106
+ [OBS_TAG_BRANCH]: payload?.['branch'] ?? payload?.['branchName'],
107
+ [OBS_TAG_COMMIT_ID]: payload?.['commitId']
108
+ },
109
+ kind: SpanKind.SERVER
110
+ }, async (span) => {
111
+ this.lastActiveSpan = span;
112
+ this.activeSpanByRequestId.set(requestId, span);
113
+ const result = await endSpan(() => super.handleMessage(message), span);
114
+ this.activeSpanByRequestId.delete(requestId);
115
+ return result;
116
+ }));
117
+ }
118
+ else if (message.response) {
119
+ this.addEvent('ws.received-response', { ['ws.response.id']: message.response.id });
120
+ return await super.handleMessage(message);
121
+ }
122
+ }
123
+ request(method, params, authorization) {
124
+ this.addEvent('ws.send-request', { ['ws.request.method']: method, ['ws.request.id']: this.nxtRequestId });
125
+ return super.request(method, params, authorization);
126
+ }
127
+ decorateToSend(message) {
128
+ propagation.inject(otelCtx.active(), message.request);
129
+ return message;
130
+ }
131
+ respond(requestId, result) {
132
+ this.addEvent('ws.send-response', { ['ws.response.id']: requestId });
133
+ return super.respond(requestId, result);
134
+ }
135
+ respondError(requestId, error, exception) {
136
+ this.addEvent('ws.send-error', { ['ws.response.id']: requestId });
137
+ if (this.lastActiveSpan) {
138
+ setHttpStatusFromError(this.lastActiveSpan, exception ?? new Error(error.message));
139
+ this.lastActiveSpan.setAttribute(OBS_SOCKET_STATUS_CODE, error.code);
140
+ }
141
+ return super.respondError(requestId, error, exception);
142
+ }
143
+ addEvent(eventName, attributes) {
144
+ // Since we do not have parent requestId in all the methods, we are assuming latest created span is the only active span
145
+ // This assumption can be broken if we start sending multiple request through the same connection.
146
+ if (this.lastActiveSpan) {
147
+ this.lastActiveSpan.addEvent(eventName, attributes, new Date());
148
+ }
149
+ }
150
+ close() {
151
+ for (const span of this.activeSpanByRequestId.values()) {
152
+ span.end();
153
+ }
154
+ this.activeSpanByRequestId = new Map();
155
+ this.lastActiveSpan = undefined;
156
+ super.close();
157
+ }
158
+ }
159
+ //# sourceMappingURL=tracedSocket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracedSocket.js","sourceRoot":"","sources":["../../src/socket/tracedSocket.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superblocksteam/shared",
3
- "version": "0.9131.0",
3
+ "version": "0.9133.0",
4
4
  "description": "Superblocks Shared Resources",
5
5
  "repository": "https://github.com/superblocksteam/shared.git",
6
6
  "license": "Superblocks Community Software License",
@@ -0,0 +1,220 @@
1
+ import {
2
+ Attributes,
3
+ ROOT_CONTEXT,
4
+ Span,
5
+ SpanKind,
6
+ SpanStatusCode,
7
+ TimeInput,
8
+ context as otelCtx,
9
+ propagation,
10
+ Tracer,
11
+ trace
12
+ } from '@opentelemetry/api';
13
+ import { SEMATTRS_MESSAGING_DESTINATION_KIND, SEMATTRS_MESSAGING_SYSTEM } from '@opentelemetry/semantic-conventions';
14
+ import WebSocket from 'isomorphic-ws';
15
+ import {
16
+ OBS_SOCKET_STATUS_CODE,
17
+ OBS_TAG_API_ID,
18
+ OBS_TAG_APPLICATION_ID,
19
+ OBS_TAG_BRANCH,
20
+ OBS_TAG_COMMIT_ID,
21
+ OBS_TAG_HTTP_ROUTE,
22
+ OBS_TAG_HTTP_STATUS_CODE
23
+ } from '../observability';
24
+ import { ISocket, SocketError, SocketMessage } from './socket';
25
+ import { GenericMiddleware, ISocketClient, MethodHandler, MethodHandlers, RequestContextBase, SocketTimeouts } from './types';
26
+
27
+ function isPromise(obj: unknown): obj is Promise<unknown> {
28
+ return obj !== null && typeof obj === 'object' && typeof (obj as Promise<unknown>).then === 'function';
29
+ }
30
+
31
+ // Taken from https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-ws/src/index.ts#L21
32
+ const endSpan = (traced: () => unknown | Promise<unknown>, span: Span) => {
33
+ try {
34
+ const result = traced();
35
+ if (isPromise(result)) {
36
+ return Promise.resolve(result)
37
+ .catch((err) => {
38
+ setHttpStatusFromError(span, typeof err === 'string' ? new Error(err) : err);
39
+ throw err;
40
+ })
41
+ .finally(() => span.end());
42
+ } else {
43
+ span.end();
44
+ return result;
45
+ }
46
+ } catch (error) {
47
+ setHttpStatusFromError(span, error);
48
+ span.end();
49
+ throw error;
50
+ }
51
+ };
52
+
53
+ function setHttpStatusFromError(span: Span, error: Error): void {
54
+ // Simplified error handling without server-side dependencies
55
+ span.setAttribute(OBS_TAG_HTTP_STATUS_CODE, 500);
56
+ span.recordException(error);
57
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
58
+ }
59
+
60
+ export class TracedSocket<ImplementedMethods, CallableMethods, RequestContext extends RequestContextBase> extends ISocket<
61
+ ImplementedMethods,
62
+ CallableMethods,
63
+ RequestContext
64
+ > {
65
+ /**
66
+ * The TracedSocket class extends the ISocket class to add tracing capabilities for each connection. It manages
67
+ * spans for handler calls to provide detailed tracing information.
68
+ *
69
+ * Unlike Express, the ISocket handler call chain is fully recursive. This recursion makes it challenging to determine
70
+ * when to create and end spans for specific handlers, as handlers may call other handlers before returning a result.
71
+ *
72
+ * An example span for recursive call
73
+ * Main span ----------------------------------------------------------------------------------
74
+ * Handler1 ----------------------------------------------------------------------------
75
+ * Handler2 ----------------------------------------------------------------------
76
+ *
77
+ * To manage this, we keep a reference to the most recently created span and complete that span when we go one step deeper into the recursion.
78
+ * This approach works for now since we don't support much parallelism, but it will need to be updated when we start handling requests concurrently.
79
+ *
80
+ * An example span for recursive call after the above fixes
81
+ * Main span ----------------------------------------------------------------------------------
82
+ * Handler1 -----------------------------
83
+ * Handler2 ------------------------------------------------
84
+ *
85
+ */
86
+ private activeSpanByRequestId = new Map<number, Span>();
87
+ private middlewareSpanByReqId = new Map<number, Span>();
88
+ private lastActiveSpan: Span | undefined;
89
+ private tracer: Tracer;
90
+
91
+ constructor(
92
+ ws: WebSocket,
93
+ requestHandlers: MethodHandlers<ImplementedMethods, CallableMethods, RequestContext>,
94
+ globalMiddlewares: GenericMiddleware<CallableMethods, RequestContext>[],
95
+ tracer: Tracer,
96
+ timeouts?: SocketTimeouts,
97
+ logger?: { error: (message?: string) => void }
98
+ ) {
99
+ super(ws, requestHandlers, globalMiddlewares, timeouts, logger);
100
+ this.tracer = tracer;
101
+ }
102
+
103
+ protected async callHandler<Params, Result, CallableMethods, RequestContext extends RequestContextBase>(
104
+ handler: MethodHandler<Params, Result, CallableMethods, RequestContext>,
105
+ params: Params,
106
+ ctx: RequestContext,
107
+ client: ISocketClient<CallableMethods>,
108
+ next: () => Promise<Result>
109
+ ): Promise<Result> {
110
+ let currentContext = otelCtx.active();
111
+ const middleWareSpan = this.middlewareSpanByReqId.get(ctx.requestId);
112
+ const activeSpan = this.activeSpanByRequestId.get(ctx.requestId);
113
+ // We need to end the middleware span since callHandler is called recursively
114
+ // Otherwise spans will be nested which is not representing actual time taken to execute a middleware
115
+ // Since we are ending the span here, it won't capture error state of nested handlers
116
+ if (middleWareSpan) {
117
+ middleWareSpan.end();
118
+ if (activeSpan) {
119
+ currentContext = trace.setSpan(otelCtx.active(), activeSpan);
120
+ }
121
+ }
122
+ let result;
123
+ await otelCtx.with(currentContext, async () => {
124
+ await this.tracer.startActiveSpan(
125
+ `WS HANDLER ${handler.name === '' ? ctx.method : handler.name}`,
126
+ {
127
+ attributes: {
128
+ [SEMATTRS_MESSAGING_SYSTEM]: 'ws',
129
+ [SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket'
130
+ },
131
+ kind: SpanKind.SERVER
132
+ },
133
+ async (span: Span) => {
134
+ this.middlewareSpanByReqId.set(ctx.requestId, span);
135
+ result = await endSpan(() => super.callHandler(handler, params, ctx, client, next), span);
136
+ this.middlewareSpanByReqId.delete(ctx.requestId);
137
+ }
138
+ );
139
+ });
140
+ return result as Result;
141
+ }
142
+
143
+ protected async handleMessage(message: SocketMessage): Promise<void> {
144
+ if (message.request) {
145
+ const spanName = message.request.method;
146
+ const requestId = message.request.id;
147
+ const payload = message.request.payload;
148
+ await otelCtx.with(
149
+ propagation.extract(ROOT_CONTEXT, message.request),
150
+ async () =>
151
+ await this.tracer.startActiveSpan(
152
+ `WS SERVER ${spanName}`,
153
+ {
154
+ attributes: {
155
+ [SEMATTRS_MESSAGING_SYSTEM]: 'ws',
156
+ [SEMATTRS_MESSAGING_DESTINATION_KIND]: 'websocket',
157
+ [OBS_TAG_HTTP_ROUTE]: spanName,
158
+ [OBS_TAG_APPLICATION_ID]: payload?.['applicationId'],
159
+ [OBS_TAG_API_ID]: payload?.['apiId'],
160
+ [OBS_TAG_BRANCH]: payload?.['branch'] ?? payload?.['branchName'],
161
+ [OBS_TAG_COMMIT_ID]: payload?.['commitId']
162
+ },
163
+ kind: SpanKind.SERVER
164
+ },
165
+ async (span: Span) => {
166
+ this.lastActiveSpan = span;
167
+ this.activeSpanByRequestId.set(requestId, span);
168
+ const result = await endSpan(() => super.handleMessage(message), span);
169
+ this.activeSpanByRequestId.delete(requestId);
170
+ return result;
171
+ }
172
+ )
173
+ );
174
+ } else if (message.response) {
175
+ this.addEvent('ws.received-response', { ['ws.response.id']: message.response.id });
176
+ return await super.handleMessage(message);
177
+ }
178
+ }
179
+
180
+ public request<Params, Result>(method: string, params: Params, authorization?: string): Promise<Result> {
181
+ this.addEvent('ws.send-request', { ['ws.request.method']: method, ['ws.request.id']: this.nxtRequestId });
182
+ return super.request(method, params, authorization);
183
+ }
184
+
185
+ protected decorateToSend(message: SocketMessage): SocketMessage {
186
+ propagation.inject(otelCtx.active(), message.request);
187
+ return message;
188
+ }
189
+
190
+ protected respond<Result>(requestId: number, result: Result): void {
191
+ this.addEvent('ws.send-response', { ['ws.response.id']: requestId });
192
+ return super.respond(requestId, result);
193
+ }
194
+
195
+ protected respondError(requestId: number, error: SocketError, exception?: Error): void {
196
+ this.addEvent('ws.send-error', { ['ws.response.id']: requestId });
197
+ if (this.lastActiveSpan) {
198
+ setHttpStatusFromError(this.lastActiveSpan, exception ?? new Error(error.message));
199
+ this.lastActiveSpan.setAttribute(OBS_SOCKET_STATUS_CODE, error.code);
200
+ }
201
+ return super.respondError(requestId, error, exception);
202
+ }
203
+
204
+ private addEvent(eventName: string, attributes: Attributes | TimeInput): void {
205
+ // Since we do not have parent requestId in all the methods, we are assuming latest created span is the only active span
206
+ // This assumption can be broken if we start sending multiple request through the same connection.
207
+ if (this.lastActiveSpan) {
208
+ this.lastActiveSpan.addEvent(eventName, attributes, new Date());
209
+ }
210
+ }
211
+
212
+ public close(): void {
213
+ for (const span of this.activeSpanByRequestId.values()) {
214
+ span.end();
215
+ }
216
+ this.activeSpanByRequestId = new Map();
217
+ this.lastActiveSpan = undefined;
218
+ super.close();
219
+ }
220
+ }