@lowerdeck/rpc-server 1.0.7 → 1.0.9
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/package.json +5 -4
- package/src/rpcMux.ts +290 -184
- package/src/server.ts +110 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowerdeck/rpc-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -36,12 +36,13 @@
|
|
|
36
36
|
"vitest": "^3.2.4"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@lowerdeck/error": "^1.0
|
|
40
|
-
"@lowerdeck/execution-context": "^1.0.
|
|
41
|
-
"@lowerdeck/id": "^1.0.
|
|
39
|
+
"@lowerdeck/error": "^1.2.0",
|
|
40
|
+
"@lowerdeck/execution-context": "^1.0.4",
|
|
41
|
+
"@lowerdeck/id": "^1.0.7",
|
|
42
42
|
"@lowerdeck/memo": "^1.0.4",
|
|
43
43
|
"@lowerdeck/sentry": "^1.0.2",
|
|
44
44
|
"@lowerdeck/serialize": "^1.0.4",
|
|
45
|
+
"@lowerdeck/telemetry": "^1.0.1",
|
|
45
46
|
"@lowerdeck/validation": "^1.0.4",
|
|
46
47
|
"cookie": "^1.1.1"
|
|
47
48
|
}
|
package/src/rpcMux.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { generateCustomId } from '@lowerdeck/id';
|
|
|
9
9
|
import { memo } from '@lowerdeck/memo';
|
|
10
10
|
import { getSentry } from '@lowerdeck/sentry';
|
|
11
11
|
import { serialize } from '@lowerdeck/serialize';
|
|
12
|
+
import {
|
|
13
|
+
isTelemetryEnabled,
|
|
14
|
+
otelContext,
|
|
15
|
+
propagation,
|
|
16
|
+
SpanKind,
|
|
17
|
+
SpanStatusCode,
|
|
18
|
+
trace
|
|
19
|
+
} from '@lowerdeck/telemetry';
|
|
12
20
|
import { v } from '@lowerdeck/validation';
|
|
13
21
|
import * as Cookie from 'cookie';
|
|
14
22
|
import { ServiceRequest } from './controller';
|
|
@@ -17,6 +25,7 @@ import { parseForwardedFor } from './extractIp';
|
|
|
17
25
|
let verbose = process.env.NODE_ENV !== 'production';
|
|
18
26
|
|
|
19
27
|
let Sentry = getSentry();
|
|
28
|
+
let tracer = trace.getTracer('lowerdeck.rpc-server');
|
|
20
29
|
|
|
21
30
|
let validation = v.object({
|
|
22
31
|
calls: v.array(
|
|
@@ -28,9 +37,38 @@ let validation = v.object({
|
|
|
28
37
|
)
|
|
29
38
|
});
|
|
30
39
|
|
|
40
|
+
let summarizeRpcTarget = (url: URL, body: any) => {
|
|
41
|
+
let pathParts = url.pathname.split('/').filter(Boolean);
|
|
42
|
+
let lastPart = pathParts[pathParts.length - 1] ?? '';
|
|
43
|
+
|
|
44
|
+
if (lastPart.startsWith('$') && lastPart.length > 1) {
|
|
45
|
+
return lastPart.slice(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let callNames: string[] = [];
|
|
49
|
+
if (body && typeof body == 'object' && Array.isArray(body.calls)) {
|
|
50
|
+
callNames = body.calls
|
|
51
|
+
.map((call: { name?: unknown }) =>
|
|
52
|
+
typeof call?.name == 'string' ? call.name.trim() : ''
|
|
53
|
+
)
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (callNames.length == 1) return callNames[0];
|
|
58
|
+
|
|
59
|
+
if (callNames.length > 1) {
|
|
60
|
+
let preview = callNames.slice(0, 3).join(', ');
|
|
61
|
+
if (callNames.length > 3) return `${preview}, +${callNames.length - 3} more`;
|
|
62
|
+
return preview;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return url.pathname;
|
|
66
|
+
};
|
|
67
|
+
|
|
31
68
|
export let rpcMux = (
|
|
32
69
|
opts: {
|
|
33
70
|
path: string;
|
|
71
|
+
allowRootSpan?: boolean;
|
|
34
72
|
cors?: {
|
|
35
73
|
headers?: string[];
|
|
36
74
|
} & ({ domains: string[] } | { check: (origin: string) => boolean });
|
|
@@ -87,7 +125,7 @@ export let rpcMux = (
|
|
|
87
125
|
? {
|
|
88
126
|
'access-control-allow-origin': origin,
|
|
89
127
|
'access-control-allow-methods': 'POST, OPTIONS',
|
|
90
|
-
'access-control-allow-headers': `Content-Type, Authorization, Baggage, Sentry-Trace${
|
|
128
|
+
'access-control-allow-headers': `Content-Type, Authorization, Baggage, Sentry-Trace, traceparent, tracestate${
|
|
91
129
|
additionalCorsHeaders ?? ''
|
|
92
130
|
}`,
|
|
93
131
|
'access-control-max-age': '604800',
|
|
@@ -154,137 +192,121 @@ export let rpcMux = (
|
|
|
154
192
|
}
|
|
155
193
|
}
|
|
156
194
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
url: req.url,
|
|
184
|
-
headers: req.headers,
|
|
185
|
-
query: url.searchParams,
|
|
186
|
-
body,
|
|
187
|
-
rawBody: body,
|
|
195
|
+
let extractedTraceContext = propagation.extract(otelContext.active(), req.headers, {
|
|
196
|
+
get: (carrier, key) => carrier.get(key) ?? undefined,
|
|
197
|
+
keys: carrier => Array.from(carrier.keys())
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let incomingParent = trace.getSpanContext(extractedTraceContext);
|
|
201
|
+
let canTrace = isTelemetryEnabled() && (!!incomingParent || !!opts.allowRootSpan);
|
|
202
|
+
let requestSpanTarget = summarizeRpcTarget(url, body);
|
|
203
|
+
let requestSpanName = `rpc request: ${requestSpanTarget}`;
|
|
204
|
+
let requestSpanOp = 'rpc.server.request';
|
|
205
|
+
|
|
206
|
+
let executeRequest = async () =>
|
|
207
|
+
await Sentry.withIsolationScope(
|
|
208
|
+
async () =>
|
|
209
|
+
await Sentry.continueTrace(
|
|
210
|
+
{ sentryTrace, baggage },
|
|
211
|
+
async () =>
|
|
212
|
+
await Sentry.startSpan(
|
|
213
|
+
{
|
|
214
|
+
name: requestSpanName,
|
|
215
|
+
op: requestSpanOp,
|
|
216
|
+
attributes: {
|
|
217
|
+
'sentry.op': requestSpanOp,
|
|
218
|
+
'rpc.request.target': requestSpanTarget,
|
|
219
|
+
'rpc.description': requestSpanName,
|
|
220
|
+
'sentry.description': requestSpanName,
|
|
188
221
|
ip,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
transport: 'http',
|
|
223
|
+
ua: req.headers.get('user-agent') ?? '',
|
|
224
|
+
origin: req.headers.get('origin') ?? ''
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
async () => {
|
|
228
|
+
try {
|
|
229
|
+
let beforeSends: Array<() => Promise<any>> = [];
|
|
230
|
+
let id = generateCustomId('req_');
|
|
231
|
+
|
|
232
|
+
let parseCookies = memo(() =>
|
|
233
|
+
Cookie.parse(req.headers.get('cookie') ?? '')
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
let request: ServiceRequest = {
|
|
237
|
+
url: req.url,
|
|
238
|
+
headers: req.headers,
|
|
239
|
+
query: url.searchParams,
|
|
240
|
+
body,
|
|
241
|
+
rawBody: body,
|
|
242
|
+
ip,
|
|
243
|
+
requestId: id,
|
|
244
|
+
|
|
245
|
+
getCookies: () => parseCookies(),
|
|
246
|
+
getCookie: (name: string) => parseCookies()[name],
|
|
247
|
+
setCookie: (name: string, value: string, opts?: any) => {
|
|
248
|
+
let cookie = Cookie.serialize(name, value, opts);
|
|
249
|
+
// @ts-ignore
|
|
250
|
+
headers.append('Set-Cookie', cookie);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
beforeSend: (handler: () => Promise<any>) => {
|
|
254
|
+
beforeSends.push(handler);
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
sharedMiddlewareMemo: new Map<string, Promise<any>>(),
|
|
258
|
+
|
|
259
|
+
appendHeaders: (newHeaders: Record<string, string | string[]>) => {
|
|
260
|
+
for (let [key, value] of Object.entries(newHeaders)) {
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
for (let v of value) headers.append(key, v);
|
|
263
|
+
} else {
|
|
264
|
+
headers.append(key, value);
|
|
265
|
+
}
|
|
211
266
|
}
|
|
212
267
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let rpcIndex = handlerNameToRpcMap.get(id);
|
|
256
|
-
if (rpcIndex == undefined) {
|
|
257
|
-
return new Response(
|
|
258
|
-
JSON.stringify(
|
|
259
|
-
notFoundError({ entity: 'handler' }).toResponse()
|
|
260
|
-
),
|
|
261
|
-
{ status: 404, headers }
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
let calls = callsByRpc.get(rpcIndex) ?? [];
|
|
266
|
-
calls.push({
|
|
267
|
-
id: generateCustomId('call_'),
|
|
268
|
-
name: id,
|
|
269
|
-
payload: body
|
|
270
|
-
});
|
|
271
|
-
callsByRpc.set(rpcIndex, calls);
|
|
272
|
-
} else {
|
|
273
|
-
let valRes = validation.validate(body);
|
|
274
|
-
if (!valRes.success) {
|
|
275
|
-
return new Response(
|
|
276
|
-
JSON.stringify(
|
|
277
|
-
validationError({
|
|
278
|
-
errors: valRes.errors,
|
|
279
|
-
entity: 'request_data'
|
|
280
|
-
}).toResponse()
|
|
281
|
-
),
|
|
282
|
-
{ status: 406, headers }
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (let call of valRes.value.calls) {
|
|
287
|
-
let rpcIndex = handlerNameToRpcMap.get(call.name);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
Sentry.getCurrentScope().setContext('rpc.request', {
|
|
271
|
+
url: req.url,
|
|
272
|
+
query: Object.fromEntries(url.searchParams.entries())
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
Sentry.getCurrentScope().addAttachment({
|
|
276
|
+
filename: 'rpc.request.body.json',
|
|
277
|
+
data: body,
|
|
278
|
+
contentType: 'application/json'
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return provideExecutionContext(
|
|
282
|
+
createExecutionContext({
|
|
283
|
+
type: 'request',
|
|
284
|
+
contextId: id,
|
|
285
|
+
ip: ip ?? '0.0.0.0',
|
|
286
|
+
userAgent: req.headers.get('user-agent') ?? ''
|
|
287
|
+
}),
|
|
288
|
+
async () => {
|
|
289
|
+
let callsByRpc = new Map<
|
|
290
|
+
number,
|
|
291
|
+
{ id: string; name: string; payload: any }[]
|
|
292
|
+
>();
|
|
293
|
+
|
|
294
|
+
let resRef = {
|
|
295
|
+
body: {
|
|
296
|
+
__typename: 'rpc.response',
|
|
297
|
+
calls: [] as any[]
|
|
298
|
+
},
|
|
299
|
+
status: 200
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
let pathParts = url.pathname.split('/').filter(Boolean);
|
|
303
|
+
let lastPart = pathParts[pathParts.length - 1];
|
|
304
|
+
|
|
305
|
+
let isSingle = lastPart[0] == '$';
|
|
306
|
+
|
|
307
|
+
if (isSingle) {
|
|
308
|
+
let id = lastPart.slice(1);
|
|
309
|
+
let rpcIndex = handlerNameToRpcMap.get(id);
|
|
288
310
|
if (rpcIndex == undefined) {
|
|
289
311
|
return new Response(
|
|
290
312
|
JSON.stringify(
|
|
@@ -295,66 +317,150 @@ export let rpcMux = (
|
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
let calls = callsByRpc.get(rpcIndex) ?? [];
|
|
298
|
-
calls.push(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
await Promise.all(
|
|
304
|
-
Array.from(callsByRpc.entries()).map(async ([rpcIndex, calls]) => {
|
|
305
|
-
let rpc = rpcs[rpcIndex];
|
|
306
|
-
let res = await rpc.runMany(request, {
|
|
307
|
-
requestId: id,
|
|
308
|
-
calls
|
|
320
|
+
calls.push({
|
|
321
|
+
id: generateCustomId('call_'),
|
|
322
|
+
name: id,
|
|
323
|
+
payload: body
|
|
309
324
|
});
|
|
325
|
+
callsByRpc.set(rpcIndex, calls);
|
|
326
|
+
} else {
|
|
327
|
+
let valRes = validation.validate(body);
|
|
328
|
+
if (!valRes.success) {
|
|
329
|
+
return new Response(
|
|
330
|
+
JSON.stringify(
|
|
331
|
+
validationError({
|
|
332
|
+
errors: valRes.errors,
|
|
333
|
+
entity: 'request_data'
|
|
334
|
+
}).toResponse()
|
|
335
|
+
),
|
|
336
|
+
{ status: 406, headers }
|
|
337
|
+
);
|
|
338
|
+
}
|
|
310
339
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
340
|
+
for (let call of valRes.value.calls) {
|
|
341
|
+
let rpcIndex = handlerNameToRpcMap.get(call.name);
|
|
342
|
+
if (rpcIndex == undefined) {
|
|
343
|
+
return new Response(
|
|
344
|
+
JSON.stringify(
|
|
345
|
+
notFoundError({ entity: 'handler' }).toResponse()
|
|
346
|
+
),
|
|
347
|
+
{ status: 404, headers }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let calls = callsByRpc.get(rpcIndex) ?? [];
|
|
352
|
+
calls.push(call as any);
|
|
353
|
+
callsByRpc.set(rpcIndex, calls);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
315
356
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
357
|
+
await Promise.all(
|
|
358
|
+
Array.from(callsByRpc.entries()).map(async ([rpcIndex, calls]) => {
|
|
359
|
+
let rpc = rpcs[rpcIndex];
|
|
360
|
+
let res = await rpc.runMany(request, {
|
|
361
|
+
requestId: id,
|
|
362
|
+
calls
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
resRef.status = Math.max(resRef.status, res.status);
|
|
366
|
+
resRef.body.calls.push(...res.body.calls);
|
|
367
|
+
})
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
headers.append('x-req-id', id);
|
|
371
|
+
headers.append('content-type', 'application/rpc+json');
|
|
372
|
+
headers.append('x-powered-by', 'lowerdeck RPC');
|
|
373
|
+
|
|
374
|
+
await Promise.all(beforeSends.map(s => s()));
|
|
375
|
+
|
|
376
|
+
return new Response(
|
|
377
|
+
serialize.encode(
|
|
378
|
+
isSingle ? resRef.body.calls[0].result : resRef.body
|
|
379
|
+
),
|
|
380
|
+
{
|
|
381
|
+
status: resRef.status,
|
|
382
|
+
headers
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
if (verbose) console.error(e);
|
|
389
|
+
|
|
390
|
+
Sentry.captureException(e, {
|
|
391
|
+
extra: { url: req.url, method: req.method, ip, body }
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return new Response(
|
|
395
|
+
JSON.stringify(
|
|
396
|
+
internalServerError({
|
|
397
|
+
inner: verbose
|
|
398
|
+
? e instanceof Error
|
|
399
|
+
? { message: e.message, stack: e.stack }
|
|
400
|
+
: { error: e }
|
|
401
|
+
: undefined
|
|
402
|
+
}).toResponse()
|
|
403
|
+
),
|
|
404
|
+
{
|
|
405
|
+
status: 500,
|
|
406
|
+
headers
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
319
414
|
|
|
320
|
-
|
|
415
|
+
return await otelContext.with(extractedTraceContext, async () => {
|
|
416
|
+
if (!canTrace) {
|
|
417
|
+
return await executeRequest();
|
|
418
|
+
}
|
|
321
419
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
420
|
+
return await tracer.startActiveSpan(
|
|
421
|
+
requestSpanName,
|
|
422
|
+
{
|
|
423
|
+
kind: SpanKind.SERVER,
|
|
424
|
+
attributes: {
|
|
425
|
+
'sentry.op': requestSpanOp,
|
|
426
|
+
'rpc.system': 'lowerdeck',
|
|
427
|
+
'rpc.request.target': requestSpanTarget,
|
|
428
|
+
'rpc.description': requestSpanName,
|
|
429
|
+
'sentry.description': requestSpanName,
|
|
430
|
+
'http.request.method': req.method,
|
|
431
|
+
'url.path': url.pathname,
|
|
432
|
+
ip: ip ?? '',
|
|
433
|
+
transport: 'http',
|
|
434
|
+
ua: req.headers.get('user-agent') ?? '',
|
|
435
|
+
origin: req.headers.get('origin') ?? ''
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
async span => {
|
|
439
|
+
try {
|
|
440
|
+
let response = await executeRequest();
|
|
441
|
+
|
|
442
|
+
span.setAttribute('http.response.status_code', response.status);
|
|
443
|
+
if (response.status >= 500) {
|
|
444
|
+
span.setStatus({
|
|
445
|
+
code: SpanStatusCode.ERROR,
|
|
446
|
+
message: `RPC request failed with status ${response.status}`
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return response;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
span.recordException(error as Error);
|
|
453
|
+
span.setStatus({
|
|
454
|
+
code: SpanStatusCode.ERROR,
|
|
455
|
+
message: error instanceof Error ? error.message : String(error)
|
|
456
|
+
});
|
|
457
|
+
throw error;
|
|
458
|
+
} finally {
|
|
459
|
+
span.end();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
});
|
|
358
464
|
}
|
|
359
465
|
};
|
|
360
466
|
};
|
package/src/server.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { internalServerError, isServiceError, notFoundError } from '@lowerdeck/error';
|
|
2
2
|
import { getSentry } from '@lowerdeck/sentry';
|
|
3
|
+
import {
|
|
4
|
+
hasActiveSpan,
|
|
5
|
+
isTelemetryEnabled,
|
|
6
|
+
SpanStatusCode,
|
|
7
|
+
trace
|
|
8
|
+
} from '@lowerdeck/telemetry';
|
|
3
9
|
import { Controller, Handler, ServiceRequest } from './controller';
|
|
4
10
|
|
|
5
11
|
let Sentry = getSentry();
|
|
12
|
+
let tracer = trace.getTracer('lowerdeck.rpc-server.calls');
|
|
13
|
+
|
|
14
|
+
let verbose = process.env.NODE_ENV !== 'production';
|
|
6
15
|
|
|
7
16
|
export let createServer =
|
|
8
17
|
(opts: {
|
|
@@ -57,59 +66,121 @@ export let createServer =
|
|
|
57
66
|
}> => {
|
|
58
67
|
let request = { ...req, body: call.payload };
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
let executeCall = async () => {
|
|
70
|
+
try {
|
|
71
|
+
let handler = findHandler(call.name);
|
|
72
|
+
|
|
73
|
+
if (!handler) {
|
|
74
|
+
return {
|
|
75
|
+
request,
|
|
76
|
+
status: 404,
|
|
77
|
+
response: notFoundError({ entity: 'handler' }).toResponse()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let response = await handler.run(request, {});
|
|
62
82
|
|
|
63
|
-
if (!handler) {
|
|
64
83
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
response:
|
|
84
|
+
status: 200,
|
|
85
|
+
request: req,
|
|
86
|
+
response: response.response
|
|
68
87
|
};
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (verbose) console.error(e);
|
|
90
|
+
|
|
91
|
+
if (isServiceError(e)) {
|
|
92
|
+
if (e.data.status >= 500) {
|
|
93
|
+
Sentry.captureException(e, {
|
|
94
|
+
tags: { reqId }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
request,
|
|
100
|
+
status: e.data.status,
|
|
101
|
+
response: e.toResponse()
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Sentry.captureException(e, {
|
|
106
|
+
tags: { reqId }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
opts.onError?.({
|
|
110
|
+
callName: call.name,
|
|
111
|
+
callId: call.id,
|
|
112
|
+
request: req,
|
|
113
|
+
error: e,
|
|
114
|
+
reqId
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
throw e;
|
|
69
118
|
}
|
|
119
|
+
};
|
|
70
120
|
|
|
71
|
-
|
|
121
|
+
let canTrace = isTelemetryEnabled() && hasActiveSpan();
|
|
122
|
+
if (!canTrace) {
|
|
123
|
+
let result = await executeCall();
|
|
72
124
|
|
|
73
125
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
response:
|
|
126
|
+
request: result.request,
|
|
127
|
+
status: result.status,
|
|
128
|
+
response: result.response
|
|
77
129
|
};
|
|
78
|
-
}
|
|
79
|
-
console.error(e);
|
|
130
|
+
}
|
|
80
131
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
let callSpanName = `rpc call: ${call.name}`;
|
|
133
|
+
let callSpanOp = 'rpc.server.call';
|
|
134
|
+
|
|
135
|
+
return await tracer.startActiveSpan(callSpanName, async span => {
|
|
136
|
+
span.setAttribute('sentry.op', callSpanOp);
|
|
137
|
+
span.setAttribute('rpc.system', 'lowerdeck');
|
|
138
|
+
span.setAttribute('rpc.method', call.name);
|
|
139
|
+
span.setAttribute('rpc.request_id', reqId);
|
|
140
|
+
span.setAttribute('rpc.call_id', call.id);
|
|
141
|
+
span.setAttribute('rpc.description', callSpanName);
|
|
142
|
+
span.setAttribute('sentry.description', callSpanName);
|
|
143
|
+
|
|
144
|
+
let finalize = (result: {
|
|
145
|
+
request: ServiceRequest;
|
|
146
|
+
status: number;
|
|
147
|
+
response: any;
|
|
148
|
+
}) => {
|
|
149
|
+
span.setAttribute('rpc.response.status_code', result.status);
|
|
150
|
+
if (result.status >= 500) {
|
|
151
|
+
span.setStatus({
|
|
152
|
+
code: SpanStatusCode.ERROR,
|
|
153
|
+
message: `RPC call failed with status ${result.status}`
|
|
85
154
|
});
|
|
86
155
|
}
|
|
87
156
|
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
status: e.data.status,
|
|
91
|
-
response: e.toResponse()
|
|
92
|
-
};
|
|
93
|
-
}
|
|
157
|
+
return result;
|
|
158
|
+
};
|
|
94
159
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
160
|
+
try {
|
|
161
|
+
let result = await executeCall();
|
|
98
162
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
163
|
+
return finalize({
|
|
164
|
+
request: result.request,
|
|
165
|
+
status: result.status,
|
|
166
|
+
response: result.response
|
|
167
|
+
});
|
|
168
|
+
} catch (e) {
|
|
169
|
+
span.recordException(e as Error);
|
|
170
|
+
span.setStatus({
|
|
171
|
+
code: SpanStatusCode.ERROR,
|
|
172
|
+
message: e instanceof Error ? e.message : String(e)
|
|
173
|
+
});
|
|
106
174
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
return finalize({
|
|
176
|
+
request,
|
|
177
|
+
status: 500,
|
|
178
|
+
response: internalServerError().toResponse()
|
|
179
|
+
});
|
|
180
|
+
} finally {
|
|
181
|
+
span.end();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
113
184
|
};
|
|
114
185
|
|
|
115
186
|
let runMany = async (
|
|
@@ -140,7 +211,7 @@ export let createServer =
|
|
|
140
211
|
});
|
|
141
212
|
} catch (e) {
|
|
142
213
|
Sentry.captureException(e);
|
|
143
|
-
console.error(e);
|
|
214
|
+
if (verbose) console.error(e);
|
|
144
215
|
}
|
|
145
216
|
|
|
146
217
|
return {
|