@qlever-llc/trellis 0.10.10 → 0.10.12
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/esm/client.d.ts +2 -0
- package/esm/client.d.ts.map +1 -1
- package/esm/client.js +2 -0
- package/esm/client_connect.d.ts +3 -2
- package/esm/client_connect.d.ts.map +1 -1
- package/esm/client_connect.js +4 -1
- package/esm/errors/TrellisError.d.ts +3 -3
- package/esm/errors/TrellisError.js +3 -3
- package/esm/server/health.d.ts.map +1 -1
- package/esm/server/health.js +34 -3
- package/esm/server/internal_jobs/job-manager.d.ts.map +1 -1
- package/esm/server/internal_jobs/job-manager.js +32 -1
- package/esm/server/runtime.d.ts +3 -0
- package/esm/server/runtime.d.ts.map +1 -1
- package/esm/server/service.d.ts +15 -0
- package/esm/server/service.d.ts.map +1 -1
- package/esm/server/service.js +41 -3
- package/esm/server.d.ts.map +1 -1
- package/esm/server.js +99 -10
- package/esm/service/deno.d.ts +1 -1
- package/esm/service/deno.d.ts.map +1 -1
- package/esm/service/mod.d.ts +1 -1
- package/esm/service/mod.d.ts.map +1 -1
- package/esm/service/node.d.ts +1 -1
- package/esm/service/node.d.ts.map +1 -1
- package/esm/service/outbox_inbox.d.ts.map +1 -1
- package/esm/service/outbox_inbox.js +14 -0
- package/esm/telemetry/core.d.ts.map +1 -1
- package/esm/telemetry/core.js +1 -1
- package/esm/telemetry/env.d.ts.map +1 -1
- package/esm/telemetry/env.js +6 -1
- package/esm/telemetry/init.d.ts +3 -0
- package/esm/telemetry/init.d.ts.map +1 -0
- package/esm/telemetry/init.js +7 -0
- package/esm/telemetry/metrics.d.ts +34 -0
- package/esm/telemetry/metrics.d.ts.map +1 -0
- package/esm/telemetry/metrics.js +181 -0
- package/esm/telemetry/mod.d.ts +3 -0
- package/esm/telemetry/mod.d.ts.map +1 -1
- package/esm/telemetry/mod.js +2 -0
- package/esm/telemetry/runtime.d.ts +2 -0
- package/esm/telemetry/runtime.d.ts.map +1 -0
- package/esm/telemetry/runtime.js +134 -0
- package/esm/telemetry.d.ts +3 -0
- package/esm/telemetry.d.ts.map +1 -0
- package/esm/telemetry.js +2 -0
- package/esm/transfer.d.ts.map +1 -1
- package/esm/transfer.js +27 -16
- package/esm/trellis.d.ts +28 -4
- package/esm/trellis.d.ts.map +1 -1
- package/esm/trellis.js +575 -80
- package/package.json +7 -5
- package/script/client.d.ts +2 -0
- package/script/client.d.ts.map +1 -1
- package/script/client.js +2 -0
- package/script/client_connect.d.ts +3 -2
- package/script/client_connect.d.ts.map +1 -1
- package/script/client_connect.js +4 -1
- package/script/errors/TrellisError.d.ts +3 -3
- package/script/errors/TrellisError.js +3 -3
- package/script/server/health.d.ts.map +1 -1
- package/script/server/health.js +34 -3
- package/script/server/internal_jobs/job-manager.d.ts.map +1 -1
- package/script/server/internal_jobs/job-manager.js +32 -1
- package/script/server/runtime.d.ts +3 -0
- package/script/server/runtime.d.ts.map +1 -1
- package/script/server/service.d.ts +15 -0
- package/script/server/service.d.ts.map +1 -1
- package/script/server/service.js +40 -2
- package/script/server.d.ts.map +1 -1
- package/script/server.js +98 -9
- package/script/service/deno.d.ts +1 -1
- package/script/service/deno.d.ts.map +1 -1
- package/script/service/mod.d.ts +1 -1
- package/script/service/mod.d.ts.map +1 -1
- package/script/service/node.d.ts +1 -1
- package/script/service/node.d.ts.map +1 -1
- package/script/service/outbox_inbox.d.ts.map +1 -1
- package/script/service/outbox_inbox.js +14 -0
- package/script/telemetry/core.d.ts.map +1 -1
- package/script/telemetry/core.js +1 -1
- package/script/telemetry/env.d.ts.map +1 -1
- package/script/telemetry/env.js +6 -1
- package/script/telemetry/init.d.ts +3 -0
- package/script/telemetry/init.d.ts.map +1 -0
- package/script/telemetry/init.js +10 -0
- package/script/telemetry/metrics.d.ts +34 -0
- package/script/telemetry/metrics.d.ts.map +1 -0
- package/script/telemetry/metrics.js +186 -0
- package/script/telemetry/mod.d.ts +3 -0
- package/script/telemetry/mod.d.ts.map +1 -1
- package/script/telemetry/mod.js +7 -1
- package/script/telemetry/runtime.d.ts +2 -0
- package/script/telemetry/runtime.d.ts.map +1 -0
- package/script/telemetry/runtime.js +137 -0
- package/script/telemetry.d.ts +3 -0
- package/script/telemetry.d.ts.map +1 -0
- package/script/telemetry.js +18 -0
- package/script/transfer.d.ts.map +1 -1
- package/script/transfer.js +28 -17
- package/script/trellis.d.ts +28 -4
- package/script/trellis.d.ts.map +1 -1
- package/script/trellis.js +606 -110
- package/src/client.ts +4 -0
- package/src/client_connect.ts +11 -9
- package/src/errors/TrellisError.ts +4 -4
- package/src/server/health.ts +41 -3
- package/src/server/internal_jobs/job-manager.ts +35 -5
- package/src/server/runtime.ts +4 -0
- package/src/server/service.ts +75 -3
- package/src/server.ts +124 -14
- package/src/service/deno.ts +1 -0
- package/src/service/mod.ts +1 -0
- package/src/service/node.ts +1 -0
- package/src/service/outbox_inbox.ts +14 -0
- package/src/telemetry/core.ts +1 -1
- package/src/telemetry/env.ts +5 -1
- package/src/telemetry/init.ts +8 -0
- package/src/telemetry/metrics.ts +294 -0
- package/src/telemetry/mod.ts +7 -0
- package/src/telemetry/runtime.ts +218 -0
- package/src/telemetry.ts +2 -0
- package/src/transfer.ts +69 -30
- package/src/trellis.ts +652 -141
- package/esm/tracing.d.ts +0 -5
- package/esm/tracing.d.ts.map +0 -1
- package/esm/tracing.js +0 -8
- package/script/tracing.d.ts +0 -5
- package/script/tracing.d.ts.map +0 -1
- package/script/tracing.js +0 -27
- package/src/tracing.ts +0 -28
package/src/trellis.ts
CHANGED
|
@@ -47,12 +47,14 @@ import {
|
|
|
47
47
|
createNatsHeaderCarrier,
|
|
48
48
|
extractTraceContext,
|
|
49
49
|
injectTraceContext,
|
|
50
|
+
recordTrellisError,
|
|
50
51
|
SpanStatusCode,
|
|
51
52
|
startClientSpan,
|
|
52
53
|
startServerSpan,
|
|
53
54
|
trace,
|
|
55
|
+
type TrellisErrorMetricAttributes,
|
|
54
56
|
withSpanAsync,
|
|
55
|
-
} from "./
|
|
57
|
+
} from "./telemetry/mod.js";
|
|
56
58
|
import { Type } from "typebox";
|
|
57
59
|
import { AssertError, Pointer } from "typebox/value";
|
|
58
60
|
import { ulid } from "ulid";
|
|
@@ -732,6 +734,65 @@ export function isResultLike(
|
|
|
732
734
|
): value is Result<unknown, BaseError> {
|
|
733
735
|
return value instanceof Result;
|
|
734
736
|
}
|
|
737
|
+
|
|
738
|
+
type SerializableRuntimeError = {
|
|
739
|
+
id?: string;
|
|
740
|
+
type: string;
|
|
741
|
+
message: string;
|
|
742
|
+
context?: Record<string, unknown>;
|
|
743
|
+
traceId?: string;
|
|
744
|
+
} & Record<string, unknown>;
|
|
745
|
+
|
|
746
|
+
export type HandlerErrorAnnotationContext = {
|
|
747
|
+
method?: string;
|
|
748
|
+
event?: string;
|
|
749
|
+
feed?: string;
|
|
750
|
+
operation?: string;
|
|
751
|
+
jobType?: string;
|
|
752
|
+
requestId?: string;
|
|
753
|
+
service?: string;
|
|
754
|
+
contractId?: string;
|
|
755
|
+
contractDigest?: string;
|
|
756
|
+
traceId?: string;
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
function compactHandlerErrorContext(
|
|
760
|
+
context: HandlerErrorAnnotationContext,
|
|
761
|
+
): Record<string, unknown> {
|
|
762
|
+
return Object.fromEntries(
|
|
763
|
+
Object.entries(context).filter(([key, value]) =>
|
|
764
|
+
key !== "traceId" && value !== undefined
|
|
765
|
+
),
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function sanitizeHandlerErrorContext(error: BaseError): void {
|
|
770
|
+
delete error.getContext().subject;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function annotateHandlerBoundaryError(
|
|
774
|
+
cause: unknown,
|
|
775
|
+
context: HandlerErrorAnnotationContext,
|
|
776
|
+
): BaseError {
|
|
777
|
+
const error = cause instanceof BaseError && !(cause instanceof RemoteError)
|
|
778
|
+
? cause
|
|
779
|
+
: new UnexpectedError({ cause });
|
|
780
|
+
sanitizeHandlerErrorContext(error);
|
|
781
|
+
error.withContext(compactHandlerErrorContext(context));
|
|
782
|
+
error.withTraceId(context.traceId);
|
|
783
|
+
return error;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function recordRuntimeError(
|
|
787
|
+
error: unknown,
|
|
788
|
+
attributes: TrellisErrorMetricAttributes,
|
|
789
|
+
): void {
|
|
790
|
+
recordTrellisError(error, {
|
|
791
|
+
messagingSystem: "nats",
|
|
792
|
+
...attributes,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
735
796
|
export type RuntimeOperationDesc = {
|
|
736
797
|
subject: string;
|
|
737
798
|
input: unknown;
|
|
@@ -788,10 +849,7 @@ export type RuntimeOperationSnapshot = {
|
|
|
788
849
|
progress?: unknown;
|
|
789
850
|
transfer?: RuntimeOperationTransferProgress;
|
|
790
851
|
output?: unknown;
|
|
791
|
-
error?:
|
|
792
|
-
type: string;
|
|
793
|
-
message: string;
|
|
794
|
-
};
|
|
852
|
+
error?: SerializableRuntimeError;
|
|
795
853
|
};
|
|
796
854
|
|
|
797
855
|
export type RuntimeOperationRecord = {
|
|
@@ -848,8 +906,11 @@ const DurableOperationSnapshotSchema = Type.Object({
|
|
|
848
906
|
})),
|
|
849
907
|
output: Type.Optional(Type.Any()),
|
|
850
908
|
error: Type.Optional(Type.Object({
|
|
909
|
+
id: Type.Optional(Type.String()),
|
|
851
910
|
type: Type.String(),
|
|
852
911
|
message: Type.String(),
|
|
912
|
+
context: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
913
|
+
traceId: Type.Optional(Type.String()),
|
|
853
914
|
})),
|
|
854
915
|
});
|
|
855
916
|
|
|
@@ -990,6 +1051,8 @@ export type TrellisOpts<TA extends AnyTrellisAPI> = {
|
|
|
990
1051
|
state?: RuntimeStateStores;
|
|
991
1052
|
connection?: TrellisConnection;
|
|
992
1053
|
onSessionNotFound?: () => MaybePromise<void>;
|
|
1054
|
+
contractId?: string;
|
|
1055
|
+
contractDigest?: string;
|
|
993
1056
|
};
|
|
994
1057
|
|
|
995
1058
|
export type RequestOpts = {
|
|
@@ -1960,6 +2023,8 @@ export class Trellis<
|
|
|
1960
2023
|
readonly handle: { readonly rpc: ActiveRpcHandleFacade<TA, TRequests> };
|
|
1961
2024
|
/** Framework-neutral lifecycle handle for this Trellis runtime connection. */
|
|
1962
2025
|
readonly connection: TrellisConnection;
|
|
2026
|
+
readonly contractId?: string;
|
|
2027
|
+
readonly contractDigest?: string;
|
|
1963
2028
|
|
|
1964
2029
|
protected nats: NatsConnection;
|
|
1965
2030
|
protected js: JetStreamClient;
|
|
@@ -1991,6 +2056,8 @@ export class Trellis<
|
|
|
1991
2056
|
this.#log = (opts?.log ?? logger).child({ lib: "trellis" });
|
|
1992
2057
|
this.timeout = opts?.timeout ?? 3000;
|
|
1993
2058
|
this.stream = opts?.stream ?? "trellis";
|
|
2059
|
+
this.contractId = opts?.contractId;
|
|
2060
|
+
this.contractDigest = opts?.contractDigest;
|
|
1994
2061
|
this.#hasExplicitApi = api !== undefined;
|
|
1995
2062
|
this.#noResponderMaxRetries = opts?.noResponderRetry?.maxAttempts ??
|
|
1996
2063
|
DEFAULT_NO_RESPONDER_MAX_RETRIES;
|
|
@@ -2409,11 +2476,23 @@ export class Trellis<
|
|
|
2409
2476
|
|
|
2410
2477
|
const msg = encodeRuntimeSchema(ctx.input, input).take();
|
|
2411
2478
|
if (isErr(msg)) {
|
|
2479
|
+
recordRuntimeError(msg.error, {
|
|
2480
|
+
surface: "rpc",
|
|
2481
|
+
direction: "client",
|
|
2482
|
+
operation: method,
|
|
2483
|
+
phase: "request_encoding",
|
|
2484
|
+
});
|
|
2412
2485
|
return msg;
|
|
2413
2486
|
}
|
|
2414
2487
|
|
|
2415
2488
|
const subject = this.template(ctx.subject, input).take();
|
|
2416
2489
|
if (isErr(subject)) {
|
|
2490
|
+
recordRuntimeError(subject.error, {
|
|
2491
|
+
surface: "rpc",
|
|
2492
|
+
direction: "client",
|
|
2493
|
+
operation: method,
|
|
2494
|
+
phase: "request_encoding",
|
|
2495
|
+
});
|
|
2417
2496
|
return subject;
|
|
2418
2497
|
}
|
|
2419
2498
|
|
|
@@ -2438,13 +2517,19 @@ export class Trellis<
|
|
|
2438
2517
|
});
|
|
2439
2518
|
const response = msgResult.take();
|
|
2440
2519
|
if (isErr(response)) {
|
|
2520
|
+
recordRuntimeError(response.error, {
|
|
2521
|
+
surface: "rpc",
|
|
2522
|
+
direction: "client",
|
|
2523
|
+
operation: method,
|
|
2524
|
+
phase: "request_send",
|
|
2525
|
+
});
|
|
2441
2526
|
return response;
|
|
2442
2527
|
}
|
|
2443
2528
|
|
|
2444
2529
|
if (response.headers?.get("status") === "error") {
|
|
2445
2530
|
const json = safeJson(response).take();
|
|
2446
2531
|
if (isErr(json)) {
|
|
2447
|
-
|
|
2532
|
+
const error = requestFailedTransportError({
|
|
2448
2533
|
code: "trellis.request.invalid_response",
|
|
2449
2534
|
message: "Trellis returned an invalid response.",
|
|
2450
2535
|
hint:
|
|
@@ -2452,12 +2537,19 @@ export class Trellis<
|
|
|
2452
2537
|
method,
|
|
2453
2538
|
subject,
|
|
2454
2539
|
cause: json.error.cause,
|
|
2455
|
-
})
|
|
2540
|
+
});
|
|
2541
|
+
recordRuntimeError(error, {
|
|
2542
|
+
surface: "rpc",
|
|
2543
|
+
direction: "client",
|
|
2544
|
+
operation: method,
|
|
2545
|
+
phase: "response_decoding",
|
|
2546
|
+
});
|
|
2547
|
+
return err(error);
|
|
2456
2548
|
}
|
|
2457
2549
|
|
|
2458
2550
|
const errorData = parse(TrellisErrorDataSchema, json).take();
|
|
2459
2551
|
if (isErr(errorData)) {
|
|
2460
|
-
|
|
2552
|
+
const error = requestFailedTransportError({
|
|
2461
2553
|
code: "trellis.request.invalid_response",
|
|
2462
2554
|
message: "Trellis returned an invalid response.",
|
|
2463
2555
|
hint:
|
|
@@ -2465,7 +2557,14 @@ export class Trellis<
|
|
|
2465
2557
|
method,
|
|
2466
2558
|
subject,
|
|
2467
2559
|
cause: errorData.error,
|
|
2468
|
-
})
|
|
2560
|
+
});
|
|
2561
|
+
recordRuntimeError(error, {
|
|
2562
|
+
surface: "rpc",
|
|
2563
|
+
direction: "client",
|
|
2564
|
+
operation: method,
|
|
2565
|
+
phase: "response_decoding",
|
|
2566
|
+
});
|
|
2567
|
+
return err(error);
|
|
2469
2568
|
}
|
|
2470
2569
|
|
|
2471
2570
|
const declaredErrorTypes = Array.isArray(ctx.declaredErrorTypes)
|
|
@@ -2484,17 +2583,29 @@ export class Trellis<
|
|
|
2484
2583
|
);
|
|
2485
2584
|
if (reconstructed) {
|
|
2486
2585
|
await this.#handleBrowserAuthRequired(reconstructed);
|
|
2586
|
+
recordRuntimeError(new RemoteError({ error: errorData }), {
|
|
2587
|
+
surface: "rpc",
|
|
2588
|
+
direction: "client",
|
|
2589
|
+
operation: method,
|
|
2590
|
+
phase: "remote_error",
|
|
2591
|
+
});
|
|
2487
2592
|
return err(reconstructed);
|
|
2488
2593
|
}
|
|
2489
2594
|
|
|
2490
2595
|
const remoteError = new RemoteError({ error: errorData });
|
|
2491
2596
|
await this.#handleBrowserAuthRequired(remoteError);
|
|
2597
|
+
recordRuntimeError(remoteError, {
|
|
2598
|
+
surface: "rpc",
|
|
2599
|
+
direction: "client",
|
|
2600
|
+
operation: method,
|
|
2601
|
+
phase: "remote_error",
|
|
2602
|
+
});
|
|
2492
2603
|
return err(remoteError);
|
|
2493
2604
|
}
|
|
2494
2605
|
|
|
2495
2606
|
const json = safeJson(response).take();
|
|
2496
2607
|
if (isErr(json)) {
|
|
2497
|
-
|
|
2608
|
+
const error = requestFailedTransportError({
|
|
2498
2609
|
code: "trellis.request.invalid_response",
|
|
2499
2610
|
message: "Trellis returned an invalid response.",
|
|
2500
2611
|
hint:
|
|
@@ -2502,11 +2613,24 @@ export class Trellis<
|
|
|
2502
2613
|
method,
|
|
2503
2614
|
subject,
|
|
2504
2615
|
cause: json.error.cause,
|
|
2505
|
-
})
|
|
2616
|
+
});
|
|
2617
|
+
recordRuntimeError(error, {
|
|
2618
|
+
surface: "rpc",
|
|
2619
|
+
direction: "client",
|
|
2620
|
+
operation: method,
|
|
2621
|
+
phase: "response_decoding",
|
|
2622
|
+
});
|
|
2623
|
+
return err(error);
|
|
2506
2624
|
}
|
|
2507
2625
|
|
|
2508
2626
|
const outputResult = parseRuntimeSchema(ctx.output, json).take();
|
|
2509
2627
|
if (isErr(outputResult)) {
|
|
2628
|
+
recordRuntimeError(outputResult.error, {
|
|
2629
|
+
surface: "rpc",
|
|
2630
|
+
direction: "client",
|
|
2631
|
+
operation: method,
|
|
2632
|
+
phase: "response_decoding",
|
|
2633
|
+
});
|
|
2510
2634
|
return err(outputResult.error);
|
|
2511
2635
|
}
|
|
2512
2636
|
|
|
@@ -2535,6 +2659,12 @@ export class Trellis<
|
|
|
2535
2659
|
message: unexpected.message,
|
|
2536
2660
|
});
|
|
2537
2661
|
span.recordException(unexpected);
|
|
2662
|
+
recordRuntimeError(unexpected, {
|
|
2663
|
+
surface: "rpc",
|
|
2664
|
+
direction: "client",
|
|
2665
|
+
operation: method,
|
|
2666
|
+
phase: "unexpected",
|
|
2667
|
+
});
|
|
2538
2668
|
return err(unexpected);
|
|
2539
2669
|
} finally {
|
|
2540
2670
|
span.end();
|
|
@@ -2683,13 +2813,29 @@ export class Trellis<
|
|
|
2683
2813
|
): AsyncResult<FeedSubscription<TEvent>, BaseError> {
|
|
2684
2814
|
return AsyncResult.from((async () => {
|
|
2685
2815
|
const payload = encodeRuntimeSchema(descriptor.input, input).take();
|
|
2686
|
-
if (isErr(payload))
|
|
2816
|
+
if (isErr(payload)) {
|
|
2817
|
+
recordRuntimeError(payload.error, {
|
|
2818
|
+
surface: "feed",
|
|
2819
|
+
direction: "client",
|
|
2820
|
+
operation: feed,
|
|
2821
|
+
phase: "request_encoding",
|
|
2822
|
+
});
|
|
2823
|
+
return payload;
|
|
2824
|
+
}
|
|
2687
2825
|
|
|
2688
2826
|
const subject = this.template(
|
|
2689
2827
|
descriptor.subject,
|
|
2690
2828
|
input as Record<string, unknown>,
|
|
2691
2829
|
).take();
|
|
2692
|
-
if (isErr(subject))
|
|
2830
|
+
if (isErr(subject)) {
|
|
2831
|
+
recordRuntimeError(subject.error, {
|
|
2832
|
+
surface: "feed",
|
|
2833
|
+
direction: "client",
|
|
2834
|
+
operation: feed,
|
|
2835
|
+
phase: "request_template",
|
|
2836
|
+
});
|
|
2837
|
+
return subject;
|
|
2838
|
+
}
|
|
2693
2839
|
|
|
2694
2840
|
const authHeaders = await this.#createProof(subject, payload);
|
|
2695
2841
|
const headers = natsHeaders();
|
|
@@ -2711,14 +2857,21 @@ export class Trellis<
|
|
|
2711
2857
|
} catch (cause) {
|
|
2712
2858
|
opts?.signal?.removeEventListener("abort", abort);
|
|
2713
2859
|
sub.unsubscribe();
|
|
2714
|
-
|
|
2860
|
+
const error = createTransportError({
|
|
2715
2861
|
code: "trellis.feed.subscribe_failed",
|
|
2716
2862
|
message: "Trellis could not subscribe to the feed.",
|
|
2717
2863
|
hint:
|
|
2718
2864
|
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2719
2865
|
cause,
|
|
2720
2866
|
context: { feed, subject },
|
|
2721
|
-
})
|
|
2867
|
+
});
|
|
2868
|
+
recordRuntimeError(error, {
|
|
2869
|
+
surface: "feed",
|
|
2870
|
+
direction: "client",
|
|
2871
|
+
operation: feed,
|
|
2872
|
+
phase: "request_send",
|
|
2873
|
+
});
|
|
2874
|
+
return err(error);
|
|
2722
2875
|
}
|
|
2723
2876
|
|
|
2724
2877
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -2749,7 +2902,7 @@ export class Trellis<
|
|
|
2749
2902
|
if (firstFrame === "timeout" || firstFrame === "aborted") {
|
|
2750
2903
|
opts?.signal?.removeEventListener("abort", abort);
|
|
2751
2904
|
sub.unsubscribe();
|
|
2752
|
-
|
|
2905
|
+
const error = createTransportError({
|
|
2753
2906
|
code: firstFrame === "timeout"
|
|
2754
2907
|
? "trellis.feed.subscribe_timeout"
|
|
2755
2908
|
: "trellis.feed.subscribe_aborted",
|
|
@@ -2760,30 +2913,51 @@ export class Trellis<
|
|
|
2760
2913
|
? "Check that the target service is running and has the current deployment digest, then retry."
|
|
2761
2914
|
: "Retry the subscription if the feed is still needed.",
|
|
2762
2915
|
context: { feed, subject },
|
|
2763
|
-
})
|
|
2916
|
+
});
|
|
2917
|
+
recordRuntimeError(error, {
|
|
2918
|
+
surface: "feed",
|
|
2919
|
+
direction: "client",
|
|
2920
|
+
operation: feed,
|
|
2921
|
+
phase: "handshake",
|
|
2922
|
+
});
|
|
2923
|
+
return err(error);
|
|
2764
2924
|
}
|
|
2765
2925
|
if (firstFrame.done) {
|
|
2766
2926
|
opts?.signal?.removeEventListener("abort", abort);
|
|
2767
2927
|
sub.unsubscribe();
|
|
2768
|
-
|
|
2928
|
+
const error = createTransportError({
|
|
2769
2929
|
code: "trellis.feed.subscribe_closed",
|
|
2770
2930
|
message: "Trellis closed the feed before acknowledging it.",
|
|
2771
2931
|
hint:
|
|
2772
2932
|
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2773
2933
|
context: { feed, subject },
|
|
2774
|
-
})
|
|
2934
|
+
});
|
|
2935
|
+
recordRuntimeError(error, {
|
|
2936
|
+
surface: "feed",
|
|
2937
|
+
direction: "client",
|
|
2938
|
+
operation: feed,
|
|
2939
|
+
phase: "handshake",
|
|
2940
|
+
});
|
|
2941
|
+
return err(error);
|
|
2775
2942
|
}
|
|
2776
2943
|
const firstMessage = firstFrame.value;
|
|
2777
2944
|
if (firstMessage.headers?.get("status") === "error") {
|
|
2778
2945
|
opts?.signal?.removeEventListener("abort", abort);
|
|
2779
2946
|
sub.unsubscribe();
|
|
2780
|
-
|
|
2947
|
+
const error = createTransportError({
|
|
2781
2948
|
code: "trellis.feed.failed",
|
|
2782
2949
|
message: "Trellis rejected the feed subscription.",
|
|
2783
2950
|
hint:
|
|
2784
2951
|
"Retry the subscription. If it keeps failing, check Trellis runtime health and permissions.",
|
|
2785
2952
|
context: { feed, subject, frame: firstMessage.string() },
|
|
2786
|
-
})
|
|
2953
|
+
});
|
|
2954
|
+
recordRuntimeError(error, {
|
|
2955
|
+
surface: "feed",
|
|
2956
|
+
direction: "client",
|
|
2957
|
+
operation: feed,
|
|
2958
|
+
phase: "remote_error",
|
|
2959
|
+
});
|
|
2960
|
+
return err(error);
|
|
2787
2961
|
}
|
|
2788
2962
|
const firstEvent = firstMessage.headers?.get("feed-status") === "ready"
|
|
2789
2963
|
? undefined
|
|
@@ -2794,18 +2968,41 @@ export class Trellis<
|
|
|
2794
2968
|
try {
|
|
2795
2969
|
const parseFeedFrame = (msg: Msg): TEvent => {
|
|
2796
2970
|
if (msg.headers?.get("status") === "error") {
|
|
2797
|
-
|
|
2971
|
+
const error = createTransportError({
|
|
2798
2972
|
code: "trellis.feed.failed",
|
|
2799
2973
|
message: "Trellis stopped the feed.",
|
|
2800
2974
|
hint:
|
|
2801
2975
|
"Retry the subscription. If it keeps failing, check Trellis runtime health.",
|
|
2802
2976
|
context: { feed, subject, frame: msg.string() },
|
|
2803
2977
|
});
|
|
2978
|
+
recordRuntimeError(error, {
|
|
2979
|
+
surface: "feed",
|
|
2980
|
+
direction: "client",
|
|
2981
|
+
operation: feed,
|
|
2982
|
+
phase: "remote_error",
|
|
2983
|
+
});
|
|
2984
|
+
throw error;
|
|
2804
2985
|
}
|
|
2805
2986
|
const json = safeJson(msg).take();
|
|
2806
|
-
if (isErr(json))
|
|
2987
|
+
if (isErr(json)) {
|
|
2988
|
+
recordRuntimeError(json.error, {
|
|
2989
|
+
surface: "feed",
|
|
2990
|
+
direction: "client",
|
|
2991
|
+
operation: feed,
|
|
2992
|
+
phase: "event_decoding",
|
|
2993
|
+
});
|
|
2994
|
+
throw json.error;
|
|
2995
|
+
}
|
|
2807
2996
|
const parsed = parseRuntimeSchema(eventSchema, json).take();
|
|
2808
|
-
if (isErr(parsed))
|
|
2997
|
+
if (isErr(parsed)) {
|
|
2998
|
+
recordRuntimeError(parsed.error, {
|
|
2999
|
+
surface: "feed",
|
|
3000
|
+
direction: "client",
|
|
3001
|
+
operation: feed,
|
|
3002
|
+
phase: "event_validation",
|
|
3003
|
+
});
|
|
3004
|
+
throw parsed.error;
|
|
3005
|
+
}
|
|
2809
3006
|
return parsed as TEvent;
|
|
2810
3007
|
};
|
|
2811
3008
|
if (firstEvent) yield parseFeedFrame(firstEvent);
|
|
@@ -2836,7 +3033,7 @@ export class Trellis<
|
|
|
2836
3033
|
sub = this.nats.subscribe(subject);
|
|
2837
3034
|
await this.nats.flush();
|
|
2838
3035
|
} catch (cause) {
|
|
2839
|
-
|
|
3036
|
+
const error = createTransportError({
|
|
2840
3037
|
code: "trellis.feed.listen_failed",
|
|
2841
3038
|
message: "Trellis could not listen for feed requests.",
|
|
2842
3039
|
hint:
|
|
@@ -2844,6 +3041,13 @@ export class Trellis<
|
|
|
2844
3041
|
cause,
|
|
2845
3042
|
context: { feed, subject },
|
|
2846
3043
|
});
|
|
3044
|
+
recordRuntimeError(error, {
|
|
3045
|
+
surface: "feed",
|
|
3046
|
+
direction: "server",
|
|
3047
|
+
operation: feed,
|
|
3048
|
+
phase: "listen",
|
|
3049
|
+
});
|
|
3050
|
+
throw error;
|
|
2847
3051
|
}
|
|
2848
3052
|
const task = AsyncResult.try(async () => {
|
|
2849
3053
|
for await (const msg of sub) {
|
|
@@ -2860,9 +3064,20 @@ export class Trellis<
|
|
|
2860
3064
|
this.#respondWithError(msg, value.error);
|
|
2861
3065
|
}
|
|
2862
3066
|
} catch (cause) {
|
|
2863
|
-
const error = cause
|
|
2864
|
-
|
|
2865
|
-
:
|
|
3067
|
+
const error = annotateHandlerBoundaryError(cause, {
|
|
3068
|
+
feed,
|
|
3069
|
+
requestId: msg.headers?.get("request-id"),
|
|
3070
|
+
service: this.name,
|
|
3071
|
+
contractId: this.contractId,
|
|
3072
|
+
contractDigest: this.contractDigest,
|
|
3073
|
+
traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
|
|
3074
|
+
});
|
|
3075
|
+
recordRuntimeError(error, {
|
|
3076
|
+
surface: "feed",
|
|
3077
|
+
direction: "server",
|
|
3078
|
+
operation: feed,
|
|
3079
|
+
phase: "handler_throw",
|
|
3080
|
+
});
|
|
2866
3081
|
this.#respondWithError(msg, error);
|
|
2867
3082
|
}
|
|
2868
3083
|
})();
|
|
@@ -2880,9 +3095,25 @@ export class Trellis<
|
|
|
2880
3095
|
) => unknown | Promise<unknown>,
|
|
2881
3096
|
): Promise<Result<void, BaseError>> {
|
|
2882
3097
|
const json = safeJson(msg).take();
|
|
2883
|
-
if (isErr(json))
|
|
3098
|
+
if (isErr(json)) {
|
|
3099
|
+
recordRuntimeError(json.error, {
|
|
3100
|
+
surface: "feed",
|
|
3101
|
+
direction: "server",
|
|
3102
|
+
operation: feed,
|
|
3103
|
+
phase: "request_decoding",
|
|
3104
|
+
});
|
|
3105
|
+
return json;
|
|
3106
|
+
}
|
|
2884
3107
|
const parsed = parseRuntimeSchema(descriptor.input, json).take();
|
|
2885
|
-
if (isErr(parsed))
|
|
3108
|
+
if (isErr(parsed)) {
|
|
3109
|
+
recordRuntimeError(parsed.error, {
|
|
3110
|
+
surface: "feed",
|
|
3111
|
+
direction: "server",
|
|
3112
|
+
operation: feed,
|
|
3113
|
+
phase: "input_validation",
|
|
3114
|
+
});
|
|
3115
|
+
return parsed;
|
|
3116
|
+
}
|
|
2886
3117
|
|
|
2887
3118
|
const caller = await this.#authenticateFeedRequest({
|
|
2888
3119
|
feed,
|
|
@@ -2892,13 +3123,26 @@ export class Trellis<
|
|
|
2892
3123
|
requiredCapabilities: descriptor.subscribeCapabilities,
|
|
2893
3124
|
});
|
|
2894
3125
|
const callerValue = caller.take();
|
|
2895
|
-
if (isErr(callerValue))
|
|
3126
|
+
if (isErr(callerValue)) {
|
|
3127
|
+
recordRuntimeError(callerValue.error, {
|
|
3128
|
+
surface: "feed",
|
|
3129
|
+
direction: "server",
|
|
3130
|
+
operation: feed,
|
|
3131
|
+
phase: "auth",
|
|
3132
|
+
});
|
|
3133
|
+
return callerValue;
|
|
3134
|
+
}
|
|
2896
3135
|
if (!msg.reply) {
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3136
|
+
const error = new UnexpectedError({
|
|
3137
|
+
context: { feed, reason: "missing_reply" },
|
|
3138
|
+
});
|
|
3139
|
+
recordRuntimeError(error, {
|
|
3140
|
+
surface: "feed",
|
|
3141
|
+
direction: "server",
|
|
3142
|
+
operation: feed,
|
|
3143
|
+
phase: "handshake",
|
|
3144
|
+
});
|
|
3145
|
+
return err(error);
|
|
2902
3146
|
}
|
|
2903
3147
|
const readyHeaders = natsHeaders();
|
|
2904
3148
|
readyHeaders.set("feed-status", "ready");
|
|
@@ -2907,26 +3151,73 @@ export class Trellis<
|
|
|
2907
3151
|
|
|
2908
3152
|
const controller = new AbortController();
|
|
2909
3153
|
try {
|
|
2910
|
-
await handler({
|
|
3154
|
+
const handlerResult = await handler({
|
|
2911
3155
|
input: parsed as TInput,
|
|
2912
3156
|
caller: callerValue,
|
|
2913
3157
|
signal: controller.signal,
|
|
2914
3158
|
emit: (event: TEvent) =>
|
|
2915
3159
|
AsyncResult.from((async () => {
|
|
2916
3160
|
const payload = encodeRuntimeSchema(descriptor.event, event).take();
|
|
2917
|
-
if (isErr(payload))
|
|
3161
|
+
if (isErr(payload)) {
|
|
3162
|
+
recordRuntimeError(payload.error, {
|
|
3163
|
+
surface: "feed",
|
|
3164
|
+
direction: "server",
|
|
3165
|
+
operation: feed,
|
|
3166
|
+
phase: "event_encoding",
|
|
3167
|
+
});
|
|
3168
|
+
return payload;
|
|
3169
|
+
}
|
|
2918
3170
|
if (!msg.reply) {
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3171
|
+
const error = new UnexpectedError({
|
|
3172
|
+
context: { feed, reason: "missing_reply" },
|
|
3173
|
+
});
|
|
3174
|
+
recordRuntimeError(error, {
|
|
3175
|
+
surface: "feed",
|
|
3176
|
+
direction: "server",
|
|
3177
|
+
operation: feed,
|
|
3178
|
+
phase: "event_publish",
|
|
3179
|
+
});
|
|
3180
|
+
return err(error);
|
|
3181
|
+
}
|
|
3182
|
+
try {
|
|
3183
|
+
this.nats.publish(msg.reply, payload);
|
|
3184
|
+
await this.nats.flush();
|
|
3185
|
+
} catch (cause) {
|
|
3186
|
+
const error = new UnexpectedError({
|
|
3187
|
+
cause,
|
|
3188
|
+
context: { feed },
|
|
3189
|
+
});
|
|
3190
|
+
recordRuntimeError(error, {
|
|
3191
|
+
surface: "feed",
|
|
3192
|
+
direction: "server",
|
|
3193
|
+
operation: feed,
|
|
3194
|
+
phase: "event_publish",
|
|
3195
|
+
});
|
|
3196
|
+
return err(error);
|
|
2924
3197
|
}
|
|
2925
|
-
this.nats.publish(msg.reply, payload);
|
|
2926
|
-
await this.nats.flush();
|
|
2927
3198
|
return ok(undefined);
|
|
2928
3199
|
})()),
|
|
2929
3200
|
});
|
|
3201
|
+
const handlerOutcome = isResultLike(handlerResult)
|
|
3202
|
+
? handlerResult.take()
|
|
3203
|
+
: handlerResult;
|
|
3204
|
+
if (isErr(handlerOutcome)) {
|
|
3205
|
+
const error = annotateHandlerBoundaryError(handlerOutcome.error, {
|
|
3206
|
+
feed,
|
|
3207
|
+
requestId: msg.headers?.get("request-id"),
|
|
3208
|
+
service: this.name,
|
|
3209
|
+
contractId: this.contractId,
|
|
3210
|
+
contractDigest: this.contractDigest,
|
|
3211
|
+
traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
|
|
3212
|
+
});
|
|
3213
|
+
recordRuntimeError(error, {
|
|
3214
|
+
surface: "feed",
|
|
3215
|
+
direction: "server",
|
|
3216
|
+
operation: feed,
|
|
3217
|
+
phase: "handler_result",
|
|
3218
|
+
});
|
|
3219
|
+
return err(error);
|
|
3220
|
+
}
|
|
2930
3221
|
return ok(undefined);
|
|
2931
3222
|
} finally {
|
|
2932
3223
|
controller.abort();
|
|
@@ -3113,6 +3404,12 @@ export class Trellis<
|
|
|
3113
3404
|
code: SpanStatusCode.ERROR,
|
|
3114
3405
|
message: "Failed to parse JSON",
|
|
3115
3406
|
});
|
|
3407
|
+
recordRuntimeError(jsonData.error, {
|
|
3408
|
+
surface: "rpc",
|
|
3409
|
+
direction: "server",
|
|
3410
|
+
operation: String(method),
|
|
3411
|
+
phase: "parse",
|
|
3412
|
+
});
|
|
3116
3413
|
return jsonData;
|
|
3117
3414
|
}
|
|
3118
3415
|
|
|
@@ -3122,6 +3419,12 @@ export class Trellis<
|
|
|
3122
3419
|
code: SpanStatusCode.ERROR,
|
|
3123
3420
|
message: "Input validation failed",
|
|
3124
3421
|
});
|
|
3422
|
+
recordRuntimeError(parsedInput.error, {
|
|
3423
|
+
surface: "rpc",
|
|
3424
|
+
direction: "server",
|
|
3425
|
+
operation: String(method),
|
|
3426
|
+
phase: "input_validation",
|
|
3427
|
+
});
|
|
3125
3428
|
return parsedInput;
|
|
3126
3429
|
}
|
|
3127
3430
|
|
|
@@ -3148,7 +3451,14 @@ export class Trellis<
|
|
|
3148
3451
|
code: SpanStatusCode.ERROR,
|
|
3149
3452
|
message: "Missing session-key",
|
|
3150
3453
|
});
|
|
3151
|
-
|
|
3454
|
+
const error = new AuthError({ reason: "missing_session_key" });
|
|
3455
|
+
recordRuntimeError(error, {
|
|
3456
|
+
surface: "rpc",
|
|
3457
|
+
direction: "server",
|
|
3458
|
+
operation: String(method),
|
|
3459
|
+
phase: "auth",
|
|
3460
|
+
});
|
|
3461
|
+
return err(error);
|
|
3152
3462
|
}
|
|
3153
3463
|
if (!proof) {
|
|
3154
3464
|
this.#log.warn({ method }, "Missing proof in request");
|
|
@@ -3156,11 +3466,25 @@ export class Trellis<
|
|
|
3156
3466
|
code: SpanStatusCode.ERROR,
|
|
3157
3467
|
message: "Missing proof",
|
|
3158
3468
|
});
|
|
3159
|
-
|
|
3469
|
+
const error = new AuthError({ reason: "missing_proof" });
|
|
3470
|
+
recordRuntimeError(error, {
|
|
3471
|
+
surface: "rpc",
|
|
3472
|
+
direction: "server",
|
|
3473
|
+
operation: String(method),
|
|
3474
|
+
phase: "auth",
|
|
3475
|
+
});
|
|
3476
|
+
return err(error);
|
|
3160
3477
|
}
|
|
3161
3478
|
const iat = Number(iatHeader);
|
|
3162
3479
|
if (!Number.isSafeInteger(iat) || !requestId) {
|
|
3163
|
-
|
|
3480
|
+
const error = new AuthError({ reason: "invalid_signature" });
|
|
3481
|
+
recordRuntimeError(error, {
|
|
3482
|
+
surface: "rpc",
|
|
3483
|
+
direction: "server",
|
|
3484
|
+
operation: String(method),
|
|
3485
|
+
phase: "auth",
|
|
3486
|
+
});
|
|
3487
|
+
return err(error);
|
|
3164
3488
|
}
|
|
3165
3489
|
|
|
3166
3490
|
// Verify proof signature locally using the raw request bytes we received.
|
|
@@ -3199,12 +3523,17 @@ export class Trellis<
|
|
|
3199
3523
|
code: SpanStatusCode.ERROR,
|
|
3200
3524
|
message: "Invalid signature",
|
|
3201
3525
|
});
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3526
|
+
const error = new AuthError({
|
|
3527
|
+
reason: "invalid_signature",
|
|
3528
|
+
context: { sessionKey },
|
|
3529
|
+
});
|
|
3530
|
+
recordRuntimeError(error, {
|
|
3531
|
+
surface: "rpc",
|
|
3532
|
+
direction: "server",
|
|
3533
|
+
operation: String(method),
|
|
3534
|
+
phase: "auth",
|
|
3535
|
+
});
|
|
3536
|
+
return err(error);
|
|
3208
3537
|
}
|
|
3209
3538
|
|
|
3210
3539
|
let auth:
|
|
@@ -3250,11 +3579,16 @@ export class Trellis<
|
|
|
3250
3579
|
}
|
|
3251
3580
|
|
|
3252
3581
|
if (!auth) {
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3582
|
+
const error = new UnexpectedError({
|
|
3583
|
+
context: { reason: "missing_auth_validate_result" },
|
|
3584
|
+
});
|
|
3585
|
+
recordRuntimeError(error, {
|
|
3586
|
+
surface: "rpc",
|
|
3587
|
+
direction: "server",
|
|
3588
|
+
operation: String(method),
|
|
3589
|
+
phase: "auth",
|
|
3590
|
+
});
|
|
3591
|
+
return err(error);
|
|
3258
3592
|
}
|
|
3259
3593
|
|
|
3260
3594
|
if (auth instanceof Error) {
|
|
@@ -3274,9 +3608,22 @@ export class Trellis<
|
|
|
3274
3608
|
message: "Auth.Requests.Validate failed",
|
|
3275
3609
|
});
|
|
3276
3610
|
if (auth instanceof BaseError) {
|
|
3611
|
+
recordRuntimeError(auth, {
|
|
3612
|
+
surface: "rpc",
|
|
3613
|
+
direction: "server",
|
|
3614
|
+
operation: String(method),
|
|
3615
|
+
phase: "auth",
|
|
3616
|
+
});
|
|
3277
3617
|
return err(auth);
|
|
3278
3618
|
}
|
|
3279
|
-
|
|
3619
|
+
const error = new UnexpectedError({ cause: auth });
|
|
3620
|
+
recordRuntimeError(error, {
|
|
3621
|
+
surface: "rpc",
|
|
3622
|
+
direction: "server",
|
|
3623
|
+
operation: String(method),
|
|
3624
|
+
phase: "auth",
|
|
3625
|
+
});
|
|
3626
|
+
return err(error);
|
|
3280
3627
|
}
|
|
3281
3628
|
|
|
3282
3629
|
if (!auth.allowed) {
|
|
@@ -3284,15 +3631,20 @@ export class Trellis<
|
|
|
3284
3631
|
code: SpanStatusCode.ERROR,
|
|
3285
3632
|
message: "Insufficient permissions",
|
|
3286
3633
|
});
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3634
|
+
const error = new AuthError({
|
|
3635
|
+
reason: "insufficient_permissions",
|
|
3636
|
+
context: {
|
|
3637
|
+
requiredCapabilities: ctx.callerCapabilities,
|
|
3638
|
+
userCapabilities: auth.caller.capabilities,
|
|
3639
|
+
},
|
|
3640
|
+
});
|
|
3641
|
+
recordRuntimeError(error, {
|
|
3642
|
+
surface: "rpc",
|
|
3643
|
+
direction: "server",
|
|
3644
|
+
operation: String(method),
|
|
3645
|
+
phase: "auth",
|
|
3646
|
+
});
|
|
3647
|
+
return err(error);
|
|
3296
3648
|
}
|
|
3297
3649
|
|
|
3298
3650
|
if (
|
|
@@ -3303,12 +3655,17 @@ export class Trellis<
|
|
|
3303
3655
|
code: SpanStatusCode.ERROR,
|
|
3304
3656
|
message: "Reply subject mismatch",
|
|
3305
3657
|
});
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3658
|
+
const error = new AuthError({
|
|
3659
|
+
reason: "reply_subject_mismatch",
|
|
3660
|
+
context: { expected: auth.inboxPrefix, actual: msg.reply },
|
|
3661
|
+
});
|
|
3662
|
+
recordRuntimeError(error, {
|
|
3663
|
+
surface: "rpc",
|
|
3664
|
+
direction: "server",
|
|
3665
|
+
operation: String(method),
|
|
3666
|
+
phase: "auth",
|
|
3667
|
+
});
|
|
3668
|
+
return err(error);
|
|
3312
3669
|
}
|
|
3313
3670
|
|
|
3314
3671
|
caller = auth.caller;
|
|
@@ -3350,7 +3707,17 @@ export class Trellis<
|
|
|
3350
3707
|
);
|
|
3351
3708
|
|
|
3352
3709
|
if (handlerResultWrapped.isErr()) {
|
|
3353
|
-
const error =
|
|
3710
|
+
const error = annotateHandlerBoundaryError(
|
|
3711
|
+
handlerResultWrapped.error,
|
|
3712
|
+
{
|
|
3713
|
+
method: String(method),
|
|
3714
|
+
requestId: msg.headers?.get("request-id"),
|
|
3715
|
+
service: this.name,
|
|
3716
|
+
contractId: this.contractId,
|
|
3717
|
+
contractDigest: this.contractDigest,
|
|
3718
|
+
traceId: activeTraceId(span) ?? incomingTraceId,
|
|
3719
|
+
},
|
|
3720
|
+
);
|
|
3354
3721
|
this.#log.error(
|
|
3355
3722
|
{
|
|
3356
3723
|
method,
|
|
@@ -3366,6 +3733,12 @@ export class Trellis<
|
|
|
3366
3733
|
message: error.message,
|
|
3367
3734
|
});
|
|
3368
3735
|
span.recordException(error);
|
|
3736
|
+
recordRuntimeError(error, {
|
|
3737
|
+
surface: "rpc",
|
|
3738
|
+
direction: "server",
|
|
3739
|
+
operation: String(method),
|
|
3740
|
+
phase: "handler_throw",
|
|
3741
|
+
});
|
|
3369
3742
|
return err(error);
|
|
3370
3743
|
}
|
|
3371
3744
|
|
|
@@ -3374,12 +3747,14 @@ export class Trellis<
|
|
|
3374
3747
|
};
|
|
3375
3748
|
const handlerOutcome = handlerResult.take();
|
|
3376
3749
|
if (isErr(handlerOutcome)) {
|
|
3377
|
-
const
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
:
|
|
3750
|
+
const error = annotateHandlerBoundaryError(handlerOutcome.error, {
|
|
3751
|
+
method: String(method),
|
|
3752
|
+
requestId: msg.headers?.get("request-id"),
|
|
3753
|
+
service: this.name,
|
|
3754
|
+
contractId: this.contractId,
|
|
3755
|
+
contractDigest: this.contractDigest,
|
|
3756
|
+
traceId: activeTraceId(span) ?? incomingTraceId,
|
|
3757
|
+
});
|
|
3383
3758
|
|
|
3384
3759
|
this.#log.error(
|
|
3385
3760
|
{
|
|
@@ -3396,6 +3771,12 @@ export class Trellis<
|
|
|
3396
3771
|
code: SpanStatusCode.ERROR,
|
|
3397
3772
|
message: error.message,
|
|
3398
3773
|
});
|
|
3774
|
+
recordRuntimeError(error, {
|
|
3775
|
+
surface: "rpc",
|
|
3776
|
+
direction: "server",
|
|
3777
|
+
operation: String(method),
|
|
3778
|
+
phase: "handler_result",
|
|
3779
|
+
});
|
|
3399
3780
|
return err(error);
|
|
3400
3781
|
}
|
|
3401
3782
|
|
|
@@ -3405,6 +3786,12 @@ export class Trellis<
|
|
|
3405
3786
|
code: SpanStatusCode.ERROR,
|
|
3406
3787
|
message: "Output encoding failed",
|
|
3407
3788
|
});
|
|
3789
|
+
recordRuntimeError(encoded.error, {
|
|
3790
|
+
surface: "rpc",
|
|
3791
|
+
direction: "server",
|
|
3792
|
+
operation: String(method),
|
|
3793
|
+
phase: "output_encoding",
|
|
3794
|
+
});
|
|
3408
3795
|
return encoded;
|
|
3409
3796
|
}
|
|
3410
3797
|
|
|
@@ -3529,17 +3916,28 @@ export class Trellis<
|
|
|
3529
3916
|
typeof eventName
|
|
3530
3917
|
>;
|
|
3531
3918
|
if (!ctx) {
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3919
|
+
const error = new UnexpectedError({
|
|
3920
|
+
cause: this.#unknownApiError("event", event.toString()),
|
|
3921
|
+
context: { event: event.toString() },
|
|
3922
|
+
});
|
|
3923
|
+
recordRuntimeError(error, {
|
|
3924
|
+
surface: "event",
|
|
3925
|
+
direction: "publisher",
|
|
3926
|
+
operation: event,
|
|
3927
|
+
phase: "prepare",
|
|
3928
|
+
});
|
|
3929
|
+
return err(error);
|
|
3538
3930
|
}
|
|
3539
3931
|
|
|
3540
3932
|
const subject = this.template(ctx.subject, data).take();
|
|
3541
3933
|
if (isErr(subject)) {
|
|
3542
3934
|
logger.error({ err: subject.error }, "Failed to template event.");
|
|
3935
|
+
recordRuntimeError(subject.error, {
|
|
3936
|
+
surface: "event",
|
|
3937
|
+
direction: "publisher",
|
|
3938
|
+
operation: event,
|
|
3939
|
+
phase: "request_encoding",
|
|
3940
|
+
});
|
|
3543
3941
|
return subject;
|
|
3544
3942
|
}
|
|
3545
3943
|
|
|
@@ -3554,7 +3952,14 @@ export class Trellis<
|
|
|
3554
3952
|
const msg = encodeSchema(ctx.event, payload).take();
|
|
3555
3953
|
if (isErr(msg)) {
|
|
3556
3954
|
logger.error({ err: msg.error }, "Failed to encode event.");
|
|
3557
|
-
|
|
3955
|
+
const error = new UnexpectedError({ cause: msg.error });
|
|
3956
|
+
recordRuntimeError(error, {
|
|
3957
|
+
surface: "event",
|
|
3958
|
+
direction: "publisher",
|
|
3959
|
+
operation: event,
|
|
3960
|
+
phase: "request_encoding",
|
|
3961
|
+
});
|
|
3962
|
+
return err(error);
|
|
3558
3963
|
}
|
|
3559
3964
|
|
|
3560
3965
|
const headers = natsHeaders();
|
|
@@ -3574,9 +3979,17 @@ export class Trellis<
|
|
|
3574
3979
|
headers: Object.freeze(headerRecord),
|
|
3575
3980
|
}));
|
|
3576
3981
|
} catch (cause) {
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3982
|
+
const error = new UnexpectedError({
|
|
3983
|
+
cause,
|
|
3984
|
+
context: { event: event.toString() },
|
|
3985
|
+
});
|
|
3986
|
+
recordRuntimeError(error, {
|
|
3987
|
+
surface: "event",
|
|
3988
|
+
direction: "publisher",
|
|
3989
|
+
operation: event,
|
|
3990
|
+
phase: "prepare",
|
|
3991
|
+
});
|
|
3992
|
+
return err(error);
|
|
3580
3993
|
}
|
|
3581
3994
|
}
|
|
3582
3995
|
|
|
@@ -3601,9 +4014,17 @@ export class Trellis<
|
|
|
3601
4014
|
await this.js.publish(event.subject, event.encodedPayload, { headers });
|
|
3602
4015
|
return ok(undefined);
|
|
3603
4016
|
} catch (cause) {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
4017
|
+
const error = new UnexpectedError({
|
|
4018
|
+
cause,
|
|
4019
|
+
context: { event: event.event },
|
|
4020
|
+
});
|
|
4021
|
+
recordRuntimeError(error, {
|
|
4022
|
+
surface: "event",
|
|
4023
|
+
direction: "publisher",
|
|
4024
|
+
operation: event.event,
|
|
4025
|
+
phase: "publish",
|
|
4026
|
+
});
|
|
4027
|
+
return err(error);
|
|
3607
4028
|
}
|
|
3608
4029
|
})());
|
|
3609
4030
|
}
|
|
@@ -3710,24 +4131,33 @@ export class Trellis<
|
|
|
3710
4131
|
const m = parsedEvent.take();
|
|
3711
4132
|
if (isErr(m)) {
|
|
3712
4133
|
this.#log.error({ error: m.error }, "Event validation failed");
|
|
4134
|
+
recordRuntimeError(m.error, {
|
|
4135
|
+
surface: "event",
|
|
4136
|
+
direction: "consumer",
|
|
4137
|
+
operation: String(event),
|
|
4138
|
+
phase: "input_validation",
|
|
4139
|
+
});
|
|
3713
4140
|
continue;
|
|
3714
4141
|
}
|
|
3715
4142
|
|
|
3716
|
-
const handlerResult = await
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
4143
|
+
const handlerResult = await this.#invokeEventHandler({
|
|
4144
|
+
event,
|
|
4145
|
+
payload: m,
|
|
4146
|
+
mode: "ephemeral",
|
|
4147
|
+
message: msg,
|
|
4148
|
+
fn,
|
|
4149
|
+
});
|
|
4150
|
+
const handlerValue = handlerResult.take();
|
|
4151
|
+
if (isErr(handlerValue)) {
|
|
4152
|
+
recordRuntimeError(handlerValue.error, {
|
|
4153
|
+
surface: "event",
|
|
4154
|
+
direction: "consumer",
|
|
4155
|
+
operation: String(event),
|
|
4156
|
+
phase: "handler_result",
|
|
4157
|
+
});
|
|
3728
4158
|
this.#log.error(
|
|
3729
4159
|
{
|
|
3730
|
-
error:
|
|
4160
|
+
error: handlerValue.error.toSerializable(),
|
|
3731
4161
|
event,
|
|
3732
4162
|
subject: msg.subject,
|
|
3733
4163
|
},
|
|
@@ -3741,6 +4171,42 @@ export class Trellis<
|
|
|
3741
4171
|
return ok(undefined);
|
|
3742
4172
|
}
|
|
3743
4173
|
|
|
4174
|
+
async #invokeEventHandler(args: {
|
|
4175
|
+
event: EventsOf<TA>;
|
|
4176
|
+
payload: unknown;
|
|
4177
|
+
mode: "durable" | "ephemeral";
|
|
4178
|
+
group?: string;
|
|
4179
|
+
message: Pick<Msg, "headers" | "subject"> & object;
|
|
4180
|
+
fn: EventCallback<EventOf<TA, EventsOf<TA>>>;
|
|
4181
|
+
}): Promise<Result<void, BaseError>> {
|
|
4182
|
+
const annotation = {
|
|
4183
|
+
event: String(args.event),
|
|
4184
|
+
service: this.name,
|
|
4185
|
+
contractId: this.contractId,
|
|
4186
|
+
contractDigest: this.contractDigest,
|
|
4187
|
+
traceId: traceIdFromTraceparent(args.message.headers?.get("traceparent")),
|
|
4188
|
+
};
|
|
4189
|
+
try {
|
|
4190
|
+
const result = await Promise.resolve(args.fn(
|
|
4191
|
+
args.payload as EventOf<TA, EventsOf<TA>>,
|
|
4192
|
+
createEventListenerContext({
|
|
4193
|
+
payload: args.payload,
|
|
4194
|
+
subject: args.message.subject,
|
|
4195
|
+
mode: args.mode,
|
|
4196
|
+
...(args.group ? { group: args.group } : {}),
|
|
4197
|
+
message: args.message,
|
|
4198
|
+
}),
|
|
4199
|
+
));
|
|
4200
|
+
const outcome = isResultLike(result) ? result.take() : result;
|
|
4201
|
+
if (isErr(outcome)) {
|
|
4202
|
+
return err(annotateHandlerBoundaryError(outcome.error, annotation));
|
|
4203
|
+
}
|
|
4204
|
+
return ok(undefined);
|
|
4205
|
+
} catch (cause) {
|
|
4206
|
+
return err(annotateHandlerBoundaryError(cause, annotation));
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
|
|
3744
4210
|
#resolveEventConsumerGroup(
|
|
3745
4211
|
event: EventsOf<TA>,
|
|
3746
4212
|
opts: EventOpts | undefined,
|
|
@@ -3944,21 +4410,18 @@ export class Trellis<
|
|
|
3944
4410
|
continue;
|
|
3945
4411
|
}
|
|
3946
4412
|
|
|
3947
|
-
const handlerResult = await
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
),
|
|
3957
|
-
);
|
|
3958
|
-
if (handlerResult.isErr()) {
|
|
4413
|
+
const handlerResult = await this.#invokeEventHandler({
|
|
4414
|
+
event,
|
|
4415
|
+
payload: m,
|
|
4416
|
+
mode: "durable",
|
|
4417
|
+
message: msg,
|
|
4418
|
+
fn,
|
|
4419
|
+
});
|
|
4420
|
+
const handlerValue = handlerResult.take();
|
|
4421
|
+
if (isErr(handlerValue)) {
|
|
3959
4422
|
this.#log.error(
|
|
3960
4423
|
{
|
|
3961
|
-
error:
|
|
4424
|
+
error: handlerValue.error.toSerializable(),
|
|
3962
4425
|
event,
|
|
3963
4426
|
subject: msg.subject,
|
|
3964
4427
|
},
|
|
@@ -4009,27 +4472,36 @@ export class Trellis<
|
|
|
4009
4472
|
{ error: eventPayload.error },
|
|
4010
4473
|
"Event validation failed",
|
|
4011
4474
|
);
|
|
4475
|
+
recordRuntimeError(eventPayload.error, {
|
|
4476
|
+
surface: "event",
|
|
4477
|
+
direction: "consumer",
|
|
4478
|
+
operation: String(registration.event),
|
|
4479
|
+
phase: "input_validation",
|
|
4480
|
+
});
|
|
4012
4481
|
msg.term();
|
|
4013
4482
|
failed = true;
|
|
4014
4483
|
break;
|
|
4015
4484
|
}
|
|
4016
4485
|
|
|
4017
|
-
const handlerResult = await
|
|
4018
|
-
registration.
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4486
|
+
const handlerResult = await this.#invokeEventHandler({
|
|
4487
|
+
event: registration.event,
|
|
4488
|
+
payload: eventPayload,
|
|
4489
|
+
mode: "durable",
|
|
4490
|
+
group,
|
|
4491
|
+
message: msg,
|
|
4492
|
+
fn: registration.fn,
|
|
4493
|
+
});
|
|
4494
|
+
const handlerValue = handlerResult.take();
|
|
4495
|
+
if (isErr(handlerValue)) {
|
|
4496
|
+
recordRuntimeError(handlerValue.error, {
|
|
4497
|
+
surface: "event",
|
|
4498
|
+
direction: "consumer",
|
|
4499
|
+
operation: String(registration.event),
|
|
4500
|
+
phase: "handler_result",
|
|
4501
|
+
});
|
|
4030
4502
|
this.#log.error(
|
|
4031
4503
|
{
|
|
4032
|
-
error:
|
|
4504
|
+
error: handlerValue.error.toSerializable(),
|
|
4033
4505
|
event: registration.event,
|
|
4034
4506
|
subject: msg.subject,
|
|
4035
4507
|
},
|
|
@@ -4236,6 +4708,12 @@ export class Trellis<
|
|
|
4236
4708
|
code: SpanStatusCode.ERROR,
|
|
4237
4709
|
message: response.error.message,
|
|
4238
4710
|
});
|
|
4711
|
+
recordRuntimeError(response.error, {
|
|
4712
|
+
surface: "operation",
|
|
4713
|
+
direction: "client",
|
|
4714
|
+
operation: "requestJson",
|
|
4715
|
+
phase: "request_send",
|
|
4716
|
+
});
|
|
4239
4717
|
return response;
|
|
4240
4718
|
}
|
|
4241
4719
|
|
|
@@ -4253,6 +4731,12 @@ export class Trellis<
|
|
|
4253
4731
|
code: SpanStatusCode.ERROR,
|
|
4254
4732
|
message: error.message,
|
|
4255
4733
|
});
|
|
4734
|
+
recordRuntimeError(error, {
|
|
4735
|
+
surface: "operation",
|
|
4736
|
+
direction: "client",
|
|
4737
|
+
operation: "requestJson",
|
|
4738
|
+
phase: "response_decoding",
|
|
4739
|
+
});
|
|
4256
4740
|
return err(error);
|
|
4257
4741
|
}
|
|
4258
4742
|
|
|
@@ -4265,6 +4749,12 @@ export class Trellis<
|
|
|
4265
4749
|
message: error.message,
|
|
4266
4750
|
});
|
|
4267
4751
|
span.recordException(error);
|
|
4752
|
+
recordRuntimeError(error, {
|
|
4753
|
+
surface: "operation",
|
|
4754
|
+
direction: "client",
|
|
4755
|
+
operation: "requestJson",
|
|
4756
|
+
phase: "unexpected",
|
|
4757
|
+
});
|
|
4268
4758
|
return err(error);
|
|
4269
4759
|
} finally {
|
|
4270
4760
|
span.end();
|
|
@@ -4301,40 +4791,61 @@ export class Trellis<
|
|
|
4301
4791
|
await this.nats.flush();
|
|
4302
4792
|
} catch (cause) {
|
|
4303
4793
|
sub.unsubscribe();
|
|
4304
|
-
|
|
4794
|
+
const error = createTransportError({
|
|
4305
4795
|
code: "trellis.watch.failed",
|
|
4306
4796
|
message: "Trellis could not start the operation watch.",
|
|
4307
4797
|
hint:
|
|
4308
4798
|
"Retry watching the operation. If it keeps failing, reconnect to Trellis and try again.",
|
|
4309
4799
|
cause,
|
|
4310
4800
|
context: { subject },
|
|
4311
|
-
})
|
|
4801
|
+
});
|
|
4802
|
+
recordRuntimeError(error, {
|
|
4803
|
+
surface: "operation",
|
|
4804
|
+
direction: "client",
|
|
4805
|
+
operation: "watchJson",
|
|
4806
|
+
phase: "request_send",
|
|
4807
|
+
});
|
|
4808
|
+
return err(error);
|
|
4312
4809
|
}
|
|
4313
4810
|
|
|
4314
4811
|
return ok((async function* () {
|
|
4315
4812
|
try {
|
|
4316
4813
|
for await (const msg of sub) {
|
|
4317
4814
|
if (msg.headers?.get("status") === "error") {
|
|
4318
|
-
|
|
4815
|
+
const error = createTransportError({
|
|
4319
4816
|
code: "trellis.watch.failed",
|
|
4320
4817
|
message: "Trellis stopped the operation watch.",
|
|
4321
4818
|
hint:
|
|
4322
4819
|
"Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
|
|
4323
4820
|
context: { subject, frame: msg.string() },
|
|
4324
|
-
})
|
|
4821
|
+
});
|
|
4822
|
+
recordRuntimeError(error, {
|
|
4823
|
+
surface: "operation",
|
|
4824
|
+
direction: "client",
|
|
4825
|
+
operation: "watchJson",
|
|
4826
|
+
phase: "remote_error",
|
|
4827
|
+
});
|
|
4828
|
+
yield err(error);
|
|
4325
4829
|
continue;
|
|
4326
4830
|
}
|
|
4327
4831
|
|
|
4328
4832
|
const json = safeJson(msg).take();
|
|
4329
4833
|
if (isErr(json)) {
|
|
4330
|
-
|
|
4834
|
+
const error = createTransportError({
|
|
4331
4835
|
code: "trellis.watch.invalid_response",
|
|
4332
4836
|
message: "Trellis returned an invalid watch update.",
|
|
4333
4837
|
hint:
|
|
4334
4838
|
"Retry watching the operation. If it keeps happening, reconnect to Trellis and try again.",
|
|
4335
4839
|
cause: json.error.cause,
|
|
4336
4840
|
context: { subject },
|
|
4337
|
-
})
|
|
4841
|
+
});
|
|
4842
|
+
recordRuntimeError(error, {
|
|
4843
|
+
surface: "operation",
|
|
4844
|
+
direction: "client",
|
|
4845
|
+
operation: "watchJson",
|
|
4846
|
+
phase: "response_decoding",
|
|
4847
|
+
});
|
|
4848
|
+
yield err(error);
|
|
4338
4849
|
continue;
|
|
4339
4850
|
}
|
|
4340
4851
|
|