@benzenejs/mesh-fleet-aws-xray 0.0.0-stage → 0.1.0-beta.2

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,27 @@
1
+ import { IBenzeneServiceContainer } from '@benzenejs/abstractions';
2
+ import { XRayTraceSourceOptions } from './XRayTraceSourceOptions';
3
+ /**
4
+ * Registers an {@link XRayTraceSource} as the {@link IMeshTraceSource} and composes it - with whatever
5
+ * `IMeshUsageSource`s are registered (e.g. a CloudWatch usage source) - into a
6
+ * `CompositeMeshFleetReadModel` serving {@link IMeshFleetReadModel}, so the whole fleet view (trace +
7
+ * correlation from X-Ray, topic stats from the usage feed, recent flows + anonymous services from X-Ray)
8
+ * answers on the AWS plane. Wire the read side with `useMessageHandlers(meshCollectorQueries)` and point the
9
+ * mesh UI's live Fleet plane at it with `useMeshUi(..., envelopeUrl: '/benzene/invoke')`; no
10
+ * `MeshCollectorStore` is needed - there is no push ingestion, only queries against X-Ray + the usage
11
+ * backend. C# extension method -> free function.
12
+ *
13
+ * Add a usage source separately for topic stats; without one the composite still serves
14
+ * traces/correlation/recent-flows/services and reports no topic stats (honest empty), rather than
15
+ * fabricating counts. Per-service and single-topic pages stay omitted (no descriptor feed on this plane).
16
+ *
17
+ * Divergence from the C# original: the C# registers a default `IAmazonXRay` in the container (unless one is
18
+ * already registered), the same pattern as `AddCloudWatchUsage`; this port has no ambient
19
+ * container-registered AWS client, so the `IMeshTraceSource` factory constructs `new XRayClient({})`
20
+ * directly - resolving region and credentials from the ambient AWS environment (on Lambda, the execution
21
+ * role), the same pattern as the `addSqs*` extensions.
22
+ *
23
+ * @param services The service container to register with.
24
+ * @param options Correlation + recent-flows tuning (lookback windows); defaults if omitted.
25
+ * @returns The service container for method chaining.
26
+ */
27
+ export declare function addXRayFleetReadModel(services: IBenzeneServiceContainer, options?: XRayTraceSourceOptions): IBenzeneServiceContainer;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.addXRayFleetReadModel = addXRayFleetReadModel;
4
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.Extensions. */
5
+ const client_xray_1 = require("@aws-sdk/client-xray");
6
+ const mesh_collector_1 = require("@benzenejs/mesh-collector");
7
+ const mesh_contracts_1 = require("@benzenejs/mesh-contracts");
8
+ const XRayTraceSource_1 = require("./XRayTraceSource");
9
+ const XRayTraceSourceOptions_1 = require("./XRayTraceSourceOptions");
10
+ /**
11
+ * Registers an {@link XRayTraceSource} as the {@link IMeshTraceSource} and composes it - with whatever
12
+ * `IMeshUsageSource`s are registered (e.g. a CloudWatch usage source) - into a
13
+ * `CompositeMeshFleetReadModel` serving {@link IMeshFleetReadModel}, so the whole fleet view (trace +
14
+ * correlation from X-Ray, topic stats from the usage feed, recent flows + anonymous services from X-Ray)
15
+ * answers on the AWS plane. Wire the read side with `useMessageHandlers(meshCollectorQueries)` and point the
16
+ * mesh UI's live Fleet plane at it with `useMeshUi(..., envelopeUrl: '/benzene/invoke')`; no
17
+ * `MeshCollectorStore` is needed - there is no push ingestion, only queries against X-Ray + the usage
18
+ * backend. C# extension method -> free function.
19
+ *
20
+ * Add a usage source separately for topic stats; without one the composite still serves
21
+ * traces/correlation/recent-flows/services and reports no topic stats (honest empty), rather than
22
+ * fabricating counts. Per-service and single-topic pages stay omitted (no descriptor feed on this plane).
23
+ *
24
+ * Divergence from the C# original: the C# registers a default `IAmazonXRay` in the container (unless one is
25
+ * already registered), the same pattern as `AddCloudWatchUsage`; this port has no ambient
26
+ * container-registered AWS client, so the `IMeshTraceSource` factory constructs `new XRayClient({})`
27
+ * directly - resolving region and credentials from the ambient AWS environment (on Lambda, the execution
28
+ * role), the same pattern as the `addSqs*` extensions.
29
+ *
30
+ * @param services The service container to register with.
31
+ * @param options Correlation + recent-flows tuning (lookback windows); defaults if omitted.
32
+ * @returns The service container for method chaining.
33
+ */
34
+ function addXRayFleetReadModel(services, options = new XRayTraceSourceOptions_1.XRayTraceSourceOptions()) {
35
+ services.addSingletonInstance(XRayTraceSourceOptions_1.XRayTraceSourceOptions, options);
36
+ services.addSingletonFactory(mesh_collector_1.IMeshTraceSource, (resolver) => new XRayTraceSource_1.XRayTraceSource(new client_xray_1.XRayClient({}), resolver.getService(XRayTraceSourceOptions_1.XRayTraceSourceOptions)));
37
+ services.addSingletonFactory(mesh_collector_1.IMeshFleetReadModel, (resolver) => new mesh_collector_1.CompositeMeshFleetReadModel(resolver.getService(mesh_collector_1.IMeshTraceSource), resolver.getServices(mesh_contracts_1.IMeshUsageSource)));
38
+ return services;
39
+ }
@@ -0,0 +1,27 @@
1
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRaySegmentMapper. */
2
+ import { MeshTraceEvent } from '@benzenejs/mesh-wire';
3
+ /**
4
+ * Maps the raw X-Ray segment documents of one trace (the JSON strings X-Ray returns from `BatchGetTraces`)
5
+ * into the mesh's {@link MeshTraceEvent} shape. Only nodes that carry a Benzene topic attribute become
6
+ * events - X-Ray traces mix in transport/AWS-SDK spans the mesh view has no place for, so this filters to
7
+ * the topic-bearing spans the pipeline stamps (`Benzene.Diagnostics.ActivityMiddlewareDecorator`; see
8
+ * `work/otel-fleet-adapter-scope.md`).
9
+ *
10
+ * A Benzene span's attributes may land in X-Ray as either `annotations` (keys sanitised to underscores -
11
+ * `benzene_topic`) or `metadata` (dotted keys preserved - `benzene.topic`, possibly nested under a namespace
12
+ * like `default`), depending on how the OTel->X-Ray exporter is configured. This reads both forms so the
13
+ * adapter works regardless of that choice. Segment/subsegment nesting gives parentage; the enclosing
14
+ * segment's `name` is the emitting service.
15
+ *
16
+ * `System.Text.Json.JsonDocument` -> `JSON.parse` + `unknown` type guards (mirroring `TempoTraceMapper`);
17
+ * `DateTimeOffset` -> epoch-ms `number`. A document that fails to parse is skipped (X-Ray traces are read
18
+ * best-effort), exactly as the C# catches `JsonException`.
19
+ */
20
+ export declare const XRaySegmentMapper: {
21
+ /**
22
+ * Parse each segment document and emit one {@link MeshTraceEvent} per topic-bearing node, carrying the
23
+ * queried `meshTraceId` and ordered by start time. A document that fails to parse is skipped rather than
24
+ * failing the whole lookup. Returns an empty list when no node carried a Benzene topic.
25
+ */
26
+ readonly map: (meshTraceId: string, segmentDocuments: Iterable<string | undefined>) => MeshTraceEvent[];
27
+ };
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XRaySegmentMapper = void 0;
4
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRaySegmentMapper. */
5
+ const mesh_wire_1 = require("@benzenejs/mesh-wire");
6
+ /**
7
+ * Maps the raw X-Ray segment documents of one trace (the JSON strings X-Ray returns from `BatchGetTraces`)
8
+ * into the mesh's {@link MeshTraceEvent} shape. Only nodes that carry a Benzene topic attribute become
9
+ * events - X-Ray traces mix in transport/AWS-SDK spans the mesh view has no place for, so this filters to
10
+ * the topic-bearing spans the pipeline stamps (`Benzene.Diagnostics.ActivityMiddlewareDecorator`; see
11
+ * `work/otel-fleet-adapter-scope.md`).
12
+ *
13
+ * A Benzene span's attributes may land in X-Ray as either `annotations` (keys sanitised to underscores -
14
+ * `benzene_topic`) or `metadata` (dotted keys preserved - `benzene.topic`, possibly nested under a namespace
15
+ * like `default`), depending on how the OTel->X-Ray exporter is configured. This reads both forms so the
16
+ * adapter works regardless of that choice. Segment/subsegment nesting gives parentage; the enclosing
17
+ * segment's `name` is the emitting service.
18
+ *
19
+ * `System.Text.Json.JsonDocument` -> `JSON.parse` + `unknown` type guards (mirroring `TempoTraceMapper`);
20
+ * `DateTimeOffset` -> epoch-ms `number`. A document that fails to parse is skipped (X-Ray traces are read
21
+ * best-effort), exactly as the C# catches `JsonException`.
22
+ */
23
+ exports.XRaySegmentMapper = {
24
+ /**
25
+ * Parse each segment document and emit one {@link MeshTraceEvent} per topic-bearing node, carrying the
26
+ * queried `meshTraceId` and ordered by start time. A document that fails to parse is skipped rather than
27
+ * failing the whole lookup. Returns an empty list when no node carried a Benzene topic.
28
+ */
29
+ map(meshTraceId, segmentDocuments) {
30
+ const events = [];
31
+ for (const document of segmentDocuments) {
32
+ if (document === undefined || document.trim().length === 0) {
33
+ continue;
34
+ }
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(document);
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ if (!isObject(parsed)) {
43
+ continue;
44
+ }
45
+ const service = tryGetString(parsed, 'name');
46
+ walk(meshTraceId, parsed, service, events);
47
+ }
48
+ events.sort((a, b) => a.startedAt - b.startedAt);
49
+ return events;
50
+ },
51
+ };
52
+ /**
53
+ * Recursively walk a segment/subsegment node, emitting an event for any topic-bearing node and descending
54
+ * into its subsegments.
55
+ */
56
+ function walk(meshTraceId, node, service, events) {
57
+ const topic = readBenzene(node, 'topic');
58
+ if (topic !== undefined && topic.length > 0) {
59
+ const event = new mesh_wire_1.MeshTraceEvent();
60
+ event.traceId = meshTraceId;
61
+ event.spanId = tryGetString(node, 'id') ?? '';
62
+ event.parentSpanId = tryGetString(node, 'parent_id');
63
+ // Prefer the Benzene service name the pipeline stamps (benzene.service) over the X-Ray segment name: on
64
+ // Lambda the segment is named by ADOT after the handler (e.g. "ApiGatewayLambdaHandler"), not the
65
+ // service, so the segment name is only a fallback for a span that predates the tag.
66
+ event.service = readBenzene(node, 'service') ?? service;
67
+ event.topic = topic;
68
+ event.topicVersion = readBenzene(node, 'version');
69
+ event.status = readBenzene(node, 'status') ?? '';
70
+ // The failure's WHY (spec §3 exceptionType): the thrown exception's type name, stamped by the pipeline
71
+ // on the same topic-bearing span as benzene.status. Undefined for non-exception failures or spans
72
+ // predating the tag.
73
+ event.exceptionType = readBenzene(node, 'exception.type');
74
+ event.correlationId = readBenzene(node, 'correlation-id') ?? readBenzene(node, 'correlationId');
75
+ event.startedAt = readStartedAt(node);
76
+ event.durationMs = readDurationMs(node);
77
+ events.push(event);
78
+ }
79
+ const subsegments = node['subsegments'];
80
+ if (isArray(subsegments)) {
81
+ for (const sub of subsegments) {
82
+ if (isObject(sub)) {
83
+ // Keep the enclosing segment's name as the service: subsegments are the same service's internal
84
+ // spans, not a new service boundary (which X-Ray would model as its own segment).
85
+ walk(meshTraceId, sub, service, events);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * Read a `benzene.<name>` attribute from either the node's annotations (underscore key) or its metadata
92
+ * (dotted key, at the top level or one namespace deep).
93
+ */
94
+ function readBenzene(node, name) {
95
+ const underscoreKey = 'benzene_' + name.replace(/-/g, '_').replace(/\./g, '_');
96
+ const dottedKey = 'benzene.' + name;
97
+ const annotations = node['annotations'];
98
+ if (isObject(annotations)) {
99
+ const fromAnnotation = tryReadValue(annotations, underscoreKey);
100
+ if (fromAnnotation !== undefined) {
101
+ return fromAnnotation;
102
+ }
103
+ }
104
+ const metadata = node['metadata'];
105
+ if (isObject(metadata)) {
106
+ const fromMetadata = tryReadValue(metadata, dottedKey);
107
+ if (fromMetadata !== undefined) {
108
+ return fromMetadata;
109
+ }
110
+ // OTel->X-Ray nests span attributes one namespace deep (default "default").
111
+ for (const value of Object.values(metadata)) {
112
+ if (isObject(value)) {
113
+ const fromNamespaced = tryReadValue(value, dottedKey);
114
+ if (fromNamespaced !== undefined) {
115
+ return fromNamespaced;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ return undefined;
121
+ }
122
+ /** Read a scalar property as text, coercing number/boolean kinds; `undefined` when absent or a non-scalar. */
123
+ function tryReadValue(obj, key) {
124
+ const value = obj[key];
125
+ if (typeof value === 'string') {
126
+ return value;
127
+ }
128
+ if (typeof value === 'number') {
129
+ return String(value);
130
+ }
131
+ if (value === true) {
132
+ return 'true';
133
+ }
134
+ if (value === false) {
135
+ return 'false';
136
+ }
137
+ return undefined;
138
+ }
139
+ /** X-Ray `start_time` is epoch seconds as a double (sub-second fraction preserved). */
140
+ function readStartedAt(node) {
141
+ const startSeconds = tryGetDouble(node, 'start_time');
142
+ return startSeconds === undefined ? 0 : Math.floor(startSeconds * 1000);
143
+ }
144
+ function readDurationMs(node) {
145
+ const start = tryGetDouble(node, 'start_time');
146
+ const end = tryGetDouble(node, 'end_time');
147
+ return start !== undefined && end !== undefined ? (end - start) * 1000 : 0;
148
+ }
149
+ function tryGetString(node, name) {
150
+ const value = node[name];
151
+ return typeof value === 'string' ? value : undefined;
152
+ }
153
+ function tryGetDouble(node, name) {
154
+ const value = node[name];
155
+ if (typeof value === 'number' && Number.isFinite(value)) {
156
+ return value;
157
+ }
158
+ // Some exporters serialise the epoch as a string; accept that too.
159
+ if (typeof value === 'string' && value.trim().length > 0) {
160
+ const parsed = Number(value);
161
+ if (Number.isFinite(parsed)) {
162
+ return parsed;
163
+ }
164
+ }
165
+ return undefined;
166
+ }
167
+ function isObject(value) {
168
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
169
+ }
170
+ function isArray(value) {
171
+ return Array.isArray(value);
172
+ }
@@ -0,0 +1,60 @@
1
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRayTraceSource. */
2
+ import { XRayClient } from '@aws-sdk/client-xray';
3
+ import { CorrelationView, IMeshTraceSource, MeshTimeRange, TraceSummary, TraceView } from '@benzenejs/mesh-collector';
4
+ import { XRayTraceSourceOptions } from './XRayTraceSourceOptions';
5
+ /**
6
+ * An {@link IMeshTraceSource} that answers `benzene:mesh:query:trace` and `benzene:mesh:query:correlation` from AWS X-Ray:
7
+ * it fetches a trace's segments with `BatchGetTraces` and maps the topic-bearing spans into a
8
+ * {@link TraceView} (see {@link XRaySegmentMapper}), and finds a business correlation id's flows with
9
+ * `GetTraceSummaries` filtered on the correlation-id annotation. The AWS realisation of the trace-backed
10
+ * fleet reader scoped in `work/otel-fleet-adapter-scope.md`, reusing the same `CompositeMeshFleetReadModel`,
11
+ * handlers, and UI as the Tempo/Jaeger adapters - no push collector required.
12
+ *
13
+ * Trace stats and service health are deliberately not sourced here (see {@link IMeshTraceSource}); X-Ray
14
+ * traces are sampled, so counts would be biased, and X-Ray has no heartbeat feed. Those compose from an
15
+ * `IMeshUsageSource` (CloudWatch) and the heartbeat plane in later increments.
16
+ *
17
+ * `IAmazonXRay` -> the `@aws-sdk/client-xray` `XRayClient` (`_xray.BatchGetTracesAsync(req)` ->
18
+ * `xray.send(new BatchGetTracesCommand(input))`); the v3 SDK uses PascalCase shape members, so the request
19
+ * and response shapes map almost 1:1 to the C#. C# two constructors (defaulted / explicit options) -> one
20
+ * constructor with a defaulted `options`. `CancellationToken` -> optional `AbortSignal` (threaded to `send`
21
+ * as `{ abortSignal }`); `DateTimeOffset` -> epoch-ms `number` (X-Ray's `StartTime`/`EndTime` are JS
22
+ * `Date`); `Task.WhenAll` -> `Promise.all`; `StringComparer.Ordinal` -> {@link ordinalCompare}.
23
+ */
24
+ export declare class XRayTraceSource implements IMeshTraceSource {
25
+ private readonly xray;
26
+ private readonly options;
27
+ /**
28
+ * Creates the source over an X-Ray client (region/credentials come from the client) with optional tuning.
29
+ * @param xray The `@aws-sdk/client-xray` client the source queries.
30
+ * @param options Correlation + recent-flows tuning (lookback windows, enrichment cap); defaults if omitted.
31
+ */
32
+ constructor(xray: XRayClient, options?: XRayTraceSourceOptions);
33
+ getTraceAsync(traceId: string, cancellationToken?: AbortSignal): Promise<TraceView | undefined>;
34
+ getCorrelationAsync(correlationId: string, range?: MeshTimeRange, cancellationToken?: AbortSignal): Promise<CorrelationView | undefined>;
35
+ getRecentFlowsAsync(limit?: number, range?: MeshTimeRange, cancellationToken?: AbortSignal): Promise<readonly TraceSummary[]>;
36
+ /**
37
+ * Enrich the chosen summaries with real span data by batching {@link BatchGetTracesMax} ids per
38
+ * `BatchGetTraces` call (parallel, per-batch fetch-isolation). A trace that maps to Benzene events becomes
39
+ * an enriched row (benzene.service names, ms start, real span count); a trace with no Benzene span or a
40
+ * failed batch keeps its summary-plane row. Skips all fetches when enrichment is off
41
+ * ({@link XRayTraceSourceOptions.recentFlowsServiceEnrichmentMax} = 0).
42
+ */
43
+ private enrichRecentFlows;
44
+ /**
45
+ * Fetch a <=5-id batch from X-Ray and map each returned trace's segments to events. A failed batch leaves
46
+ * its rows on the summary plane (fetch isolation), never the whole list.
47
+ */
48
+ private fetchBatch;
49
+ /**
50
+ * Fetch the given trace ids from X-Ray and map the returned segments to events under one mesh trace id (a
51
+ * single trace's lookup).
52
+ */
53
+ private fetchEvents;
54
+ /**
55
+ * Resolve a requested {@link MeshTimeRange} to X-Ray's `[start,end]`, falling back to `now - fallbackMs`
56
+ * .. `now` when no window was requested (today's behavior). X-Ray's `GetTraceSummaries` needs a bounded
57
+ * range either way.
58
+ */
59
+ private resolveWindow;
60
+ }
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.XRayTraceSource = void 0;
4
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRayTraceSource. */
5
+ const client_xray_1 = require("@aws-sdk/client-xray");
6
+ const mesh_collector_1 = require("@benzenejs/mesh-collector");
7
+ const XRaySegmentMapper_1 = require("./XRaySegmentMapper");
8
+ const XRayTraceSourceOptions_1 = require("./XRayTraceSourceOptions");
9
+ // X-Ray's BatchGetTraces accepts at most 5 trace ids per call.
10
+ const BatchGetTracesMax = 5;
11
+ /**
12
+ * An {@link IMeshTraceSource} that answers `benzene:mesh:query:trace` and `benzene:mesh:query:correlation` from AWS X-Ray:
13
+ * it fetches a trace's segments with `BatchGetTraces` and maps the topic-bearing spans into a
14
+ * {@link TraceView} (see {@link XRaySegmentMapper}), and finds a business correlation id's flows with
15
+ * `GetTraceSummaries` filtered on the correlation-id annotation. The AWS realisation of the trace-backed
16
+ * fleet reader scoped in `work/otel-fleet-adapter-scope.md`, reusing the same `CompositeMeshFleetReadModel`,
17
+ * handlers, and UI as the Tempo/Jaeger adapters - no push collector required.
18
+ *
19
+ * Trace stats and service health are deliberately not sourced here (see {@link IMeshTraceSource}); X-Ray
20
+ * traces are sampled, so counts would be biased, and X-Ray has no heartbeat feed. Those compose from an
21
+ * `IMeshUsageSource` (CloudWatch) and the heartbeat plane in later increments.
22
+ *
23
+ * `IAmazonXRay` -> the `@aws-sdk/client-xray` `XRayClient` (`_xray.BatchGetTracesAsync(req)` ->
24
+ * `xray.send(new BatchGetTracesCommand(input))`); the v3 SDK uses PascalCase shape members, so the request
25
+ * and response shapes map almost 1:1 to the C#. C# two constructors (defaulted / explicit options) -> one
26
+ * constructor with a defaulted `options`. `CancellationToken` -> optional `AbortSignal` (threaded to `send`
27
+ * as `{ abortSignal }`); `DateTimeOffset` -> epoch-ms `number` (X-Ray's `StartTime`/`EndTime` are JS
28
+ * `Date`); `Task.WhenAll` -> `Promise.all`; `StringComparer.Ordinal` -> {@link ordinalCompare}.
29
+ */
30
+ class XRayTraceSource {
31
+ xray;
32
+ options;
33
+ /**
34
+ * Creates the source over an X-Ray client (region/credentials come from the client) with optional tuning.
35
+ * @param xray The `@aws-sdk/client-xray` client the source queries.
36
+ * @param options Correlation + recent-flows tuning (lookback windows, enrichment cap); defaults if omitted.
37
+ */
38
+ constructor(xray, options = new XRayTraceSourceOptions_1.XRayTraceSourceOptions()) {
39
+ this.xray = xray;
40
+ this.options = options;
41
+ }
42
+ async getTraceAsync(traceId, cancellationToken) {
43
+ if (traceId.length === 0) {
44
+ return undefined;
45
+ }
46
+ const events = await this.fetchEvents([traceId], traceId, cancellationToken);
47
+ // Undefined (not empty) when X-Ray has no such trace, or a real trace that carried no Benzene
48
+ // topic-bearing span - so the query handler answers NotFound, not an empty waterfall.
49
+ if (events.length === 0) {
50
+ return undefined;
51
+ }
52
+ const view = new mesh_collector_1.TraceView();
53
+ view.traceId = traceId;
54
+ view.events = events;
55
+ return view;
56
+ }
57
+ async getCorrelationAsync(correlationId, range, cancellationToken) {
58
+ if (correlationId.length === 0) {
59
+ return undefined;
60
+ }
61
+ const window = this.resolveWindow(range, this.options.correlationLookbackMs);
62
+ // benzene.correlation-id lands in X-Ray as the underscore-sanitised annotation key; only annotations are
63
+ // filterable (see work/otel-fleet-adapter-scope.md §6b).
64
+ const filter = `annotation.benzene_correlation_id = "${escape(correlationId)}"`;
65
+ const traceIds = [];
66
+ let nextToken;
67
+ do {
68
+ const response = await this.xray.send(new client_xray_1.GetTraceSummariesCommand({
69
+ StartTime: new Date(window.start),
70
+ EndTime: new Date(window.end),
71
+ FilterExpression: filter,
72
+ NextToken: nextToken,
73
+ }), abortOptions(cancellationToken));
74
+ for (const summary of response.TraceSummaries ?? []) {
75
+ if (summary.Id !== undefined && summary.Id.length > 0) {
76
+ traceIds.push(summary.Id);
77
+ }
78
+ }
79
+ nextToken = response.NextToken !== undefined && response.NextToken.length > 0 ? response.NextToken : undefined;
80
+ } while (nextToken !== undefined);
81
+ if (traceIds.length === 0) {
82
+ return undefined;
83
+ }
84
+ const traces = [];
85
+ for (const batch of chunk(traceIds, BatchGetTracesMax)) {
86
+ const response = await this.xray.send(new client_xray_1.BatchGetTracesCommand({ TraceIds: batch }), abortOptions(cancellationToken));
87
+ for (const trace of response.Traces ?? []) {
88
+ const segments = trace.Segments;
89
+ if (segments === undefined || segments.length === 0 || trace.Id === undefined || trace.Id.length === 0) {
90
+ continue;
91
+ }
92
+ const events = XRaySegmentMapper_1.XRaySegmentMapper.map(trace.Id, segments.map((s) => s.Document));
93
+ if (events.length > 0) {
94
+ const view = new mesh_collector_1.TraceView();
95
+ view.traceId = trace.Id;
96
+ view.events = events;
97
+ traces.push(view);
98
+ }
99
+ }
100
+ }
101
+ if (traces.length === 0) {
102
+ return undefined;
103
+ }
104
+ // Earliest-first, the same ordering the in-memory collector's correlation view uses so the UI renders
105
+ // both identically.
106
+ traces.sort((a, b) => earliestStart(a) - earliestStart(b));
107
+ const result = new mesh_collector_1.CorrelationView();
108
+ result.correlationId = correlationId;
109
+ result.traces = traces;
110
+ return result;
111
+ }
112
+ async getRecentFlowsAsync(limit = 20, range, cancellationToken) {
113
+ if (limit <= 0) {
114
+ return [];
115
+ }
116
+ const window = this.resolveWindow(range, this.options.recentFlowsLookbackMs);
117
+ const summaries = [];
118
+ let nextToken;
119
+ do {
120
+ const response = await this.xray.send(new client_xray_1.GetTraceSummariesCommand({
121
+ StartTime: new Date(window.start),
122
+ EndTime: new Date(window.end),
123
+ NextToken: nextToken,
124
+ }), abortOptions(cancellationToken));
125
+ if (response.TraceSummaries !== undefined) {
126
+ summaries.push(...response.TraceSummaries);
127
+ }
128
+ nextToken = response.NextToken !== undefined && response.NextToken.length > 0 ? response.NextToken : undefined;
129
+ // Stop once we have comfortably more than the cap to sort a newest-first top-N from; the window, not
130
+ // exhaustive paging, bounds a "recent flows" list.
131
+ } while (nextToken !== undefined && summaries.length < limit * 4);
132
+ // Select the newest N by the trace-id epoch (second-granularity, but enough to pick the right ~20), then
133
+ // enrich those rows below. Ordering within a second is refined by the enriched millisecond start; the
134
+ // final sort applies a stable trace-id tiebreaker so both planes are deterministic.
135
+ const top = [...summaries]
136
+ .sort((a, b) => {
137
+ const byEpoch = parseTraceStart(b.Id) - parseTraceStart(a.Id);
138
+ return byEpoch !== 0 ? byEpoch : ordinalCompare(b.Id ?? '', a.Id ?? '');
139
+ })
140
+ .slice(0, limit);
141
+ const rows = await this.enrichRecentFlows(top, limit, cancellationToken);
142
+ return rows.sort((a, b) => {
143
+ const byStart = b.startedAt - a.startedAt;
144
+ return byStart !== 0 ? byStart : ordinalCompare(b.traceId, a.traceId);
145
+ });
146
+ }
147
+ /**
148
+ * Enrich the chosen summaries with real span data by batching {@link BatchGetTracesMax} ids per
149
+ * `BatchGetTraces` call (parallel, per-batch fetch-isolation). A trace that maps to Benzene events becomes
150
+ * an enriched row (benzene.service names, ms start, real span count); a trace with no Benzene span or a
151
+ * failed batch keeps its summary-plane row. Skips all fetches when enrichment is off
152
+ * ({@link XRayTraceSourceOptions.recentFlowsServiceEnrichmentMax} = 0).
153
+ */
154
+ async enrichRecentFlows(summaries, limit, cancellationToken) {
155
+ const enrichMax = Math.min(Math.max(this.options.recentFlowsServiceEnrichmentMax, 0), limit);
156
+ if (enrichMax === 0) {
157
+ return summaries.map(toSummaryPlaneRow);
158
+ }
159
+ // Which trace ids to enrich (the first enrichMax that have an id); the rest stay summary-plane.
160
+ const toEnrich = summaries
161
+ .filter((s) => s.Id !== undefined && s.Id.length > 0)
162
+ .slice(0, enrichMax)
163
+ .map((s) => s.Id);
164
+ const mapped = new Map();
165
+ const batches = await Promise.all(chunk(toEnrich, BatchGetTracesMax).map((ids) => this.fetchBatch(ids, cancellationToken)));
166
+ for (const batch of batches) {
167
+ for (const [id, events] of batch) {
168
+ mapped.set(id, events);
169
+ }
170
+ }
171
+ // Enriched row where the trace mapped to at least one Benzene event; summary-plane fallback otherwise.
172
+ return summaries.map((s) => {
173
+ const events = s.Id !== undefined ? mapped.get(s.Id) : undefined;
174
+ return events !== undefined && events.length > 0 ? enrichedRow(s, events) : toSummaryPlaneRow(s);
175
+ });
176
+ }
177
+ /**
178
+ * Fetch a <=5-id batch from X-Ray and map each returned trace's segments to events. A failed batch leaves
179
+ * its rows on the summary plane (fetch isolation), never the whole list.
180
+ */
181
+ async fetchBatch(ids, cancellationToken) {
182
+ const result = new Map();
183
+ try {
184
+ const response = await this.xray.send(new client_xray_1.BatchGetTracesCommand({ TraceIds: ids }), abortOptions(cancellationToken));
185
+ for (const trace of response.Traces ?? []) {
186
+ const segments = trace.Segments;
187
+ if (segments === undefined || segments.length === 0 || trace.Id === undefined || trace.Id.length === 0) {
188
+ continue;
189
+ }
190
+ result.set(trace.Id, XRaySegmentMapper_1.XRaySegmentMapper.map(trace.Id, segments.map((s) => s.Document)));
191
+ }
192
+ }
193
+ catch {
194
+ // Fetch isolation: a failed batch leaves its <=5 rows on the summary plane, never the whole list.
195
+ }
196
+ return result;
197
+ }
198
+ /**
199
+ * Fetch the given trace ids from X-Ray and map the returned segments to events under one mesh trace id (a
200
+ * single trace's lookup).
201
+ */
202
+ async fetchEvents(traceIds, meshTraceId, cancellationToken) {
203
+ const response = await this.xray.send(new client_xray_1.BatchGetTracesCommand({ TraceIds: traceIds }), abortOptions(cancellationToken));
204
+ // X-Ray returns an unknown id in UnprocessedTraceIds (not Traces), so an unknown trace yields no segments
205
+ // here -> an empty event list -> a null view upstream.
206
+ const documents = [];
207
+ for (const trace of response.Traces ?? []) {
208
+ const segments = trace.Segments;
209
+ if (segments !== undefined && segments.length > 0) {
210
+ for (const segment of segments) {
211
+ documents.push(segment.Document);
212
+ }
213
+ }
214
+ }
215
+ return XRaySegmentMapper_1.XRaySegmentMapper.map(meshTraceId, documents);
216
+ }
217
+ /**
218
+ * Resolve a requested {@link MeshTimeRange} to X-Ray's `[start,end]`, falling back to `now - fallbackMs`
219
+ * .. `now` when no window was requested (today's behavior). X-Ray's `GetTraceSummaries` needs a bounded
220
+ * range either way.
221
+ */
222
+ resolveWindow(range, fallbackMs) {
223
+ const resolved = mesh_collector_1.MeshTimeRangeResolver.resolve(range, Date.now());
224
+ if (resolved !== undefined) {
225
+ return { start: resolved.from, end: resolved.to };
226
+ }
227
+ const end = Date.now();
228
+ return { start: end - fallbackMs, end };
229
+ }
230
+ }
231
+ exports.XRayTraceSource = XRayTraceSource;
232
+ /**
233
+ * An enriched recent-flows row: real service names (benzene.service, from the mapped events), the earliest
234
+ * event's millisecond start, and the real span count. Keeps the summary's authoritative error flag
235
+ * (`HasError`/`HasFault`) and duration.
236
+ */
237
+ function enrichedRow(summary, events) {
238
+ const row = new mesh_collector_1.TraceSummary();
239
+ row.traceId = summary.Id ?? '';
240
+ // X-Ray Duration is in seconds; the mesh carries milliseconds.
241
+ row.durationMs = (summary.Duration ?? 0) * 1000;
242
+ row.failed = (summary.HasError ?? false) || (summary.HasFault ?? false);
243
+ row.services = [
244
+ ...new Set(events.map((e) => e.service).filter((name) => name !== undefined && name.length > 0)),
245
+ ].sort(ordinalCompare);
246
+ row.startedAt = Math.min(...events.map((e) => e.startedAt));
247
+ row.events = events.length;
248
+ // The flow's entry topic: the earliest mapped event's (map returns events in start order).
249
+ row.topic = events[0].topic;
250
+ // How the flow ended (mesh.md §4), from the mapped events' benzene.status. `failed` is X-Ray's own error
251
+ // flag here, so when it and the events disagree no event decides the flow and the status is omitted.
252
+ row.status = (0, mesh_collector_1.flowStatus)(events, row.failed);
253
+ return row;
254
+ }
255
+ function toSummaryPlaneRow(summary) {
256
+ const row = new mesh_collector_1.TraceSummary();
257
+ row.traceId = summary.Id ?? '';
258
+ // X-Ray Duration is in seconds; the mesh carries milliseconds.
259
+ row.durationMs = (summary.Duration ?? 0) * 1000;
260
+ row.failed = (summary.HasError ?? false) || (summary.HasFault ?? false);
261
+ row.services = [
262
+ ...new Set((summary.ServiceIds ?? [])
263
+ .map((s) => s.Name)
264
+ .filter((name) => name !== undefined && name.length > 0)),
265
+ ];
266
+ row.startedAt = parseTraceStart(summary.Id);
267
+ row.events = 0; // X-Ray summaries carry no span count; the drill-in trace has it.
268
+ // No `status`: a summary-plane row has no Benzene spans to read it from (mesh.md §4 - omitted when unknown).
269
+ return row;
270
+ }
271
+ /**
272
+ * An X-Ray trace id is `1-{8 hex epoch seconds}-{24 hex}`; the middle group is the trace's start time. Falls
273
+ * back to `0` (the epoch, the TS analog of C# `DateTimeOffset.MinValue` used only for ordering) for an
274
+ * unparseable id.
275
+ */
276
+ function parseTraceStart(traceId) {
277
+ if (traceId !== undefined) {
278
+ const parts = traceId.split('-');
279
+ if (parts.length >= 2 && /^[0-9a-fA-F]{1,15}$/.test(parts[1])) {
280
+ const epoch = parseInt(parts[1], 16);
281
+ if (!Number.isNaN(epoch)) {
282
+ return epoch * 1000;
283
+ }
284
+ }
285
+ }
286
+ return 0;
287
+ }
288
+ function earliestStart(trace) {
289
+ return trace.events.length === 0 ? Number.MAX_SAFE_INTEGER : Math.min(...trace.events.map((e) => e.startedAt));
290
+ }
291
+ // X-Ray filter-expression string literals are double-quoted; escape backslashes and quotes so a correlation
292
+ // id can't break out of the literal.
293
+ function escape(value) {
294
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
295
+ }
296
+ function chunk(items, size) {
297
+ const result = [];
298
+ for (let i = 0; i < items.length; i += size) {
299
+ result.push(items.slice(i, i + size));
300
+ }
301
+ return result;
302
+ }
303
+ function ordinalCompare(a, b) {
304
+ return a < b ? -1 : a > b ? 1 : 0;
305
+ }
306
+ /** Thread the optional `AbortSignal` to the SDK's `send` as its `HttpHandlerOptions`, or omit it. */
307
+ function abortOptions(cancellationToken) {
308
+ return cancellationToken !== undefined ? { abortSignal: cancellationToken } : undefined;
309
+ }
@@ -0,0 +1,42 @@
1
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRayTraceSourceOptions. */
2
+ /** Default `correlationLookbackMs`: 24 hours, in milliseconds (`TimeSpan` -> millisecond `number`). */
3
+ export declare const DefaultCorrelationLookbackMs: number;
4
+ /** Default `recentFlowsLookbackMs`: 1 hour, in milliseconds (`TimeSpan` -> millisecond `number`). */
5
+ export declare const DefaultRecentFlowsLookbackMs: number;
6
+ /** Default `recentFlowsServiceEnrichmentMax`: the fleet cap (20 rows). */
7
+ export declare const DefaultRecentFlowsServiceEnrichmentMax = 20;
8
+ /**
9
+ * Tuning for {@link XRayTraceSource}. Only correlation search and recent-flows enrichment need these - a
10
+ * trace lookup is by id (`BatchGetTraces`, no window). X-Ray's `GetTraceSummaries` requires a time range, so
11
+ * a correlation/recent-flows lookup searches a bounded window for matching traces.
12
+ *
13
+ * `TimeSpan` -> millisecond `number` (`correlationLookbackMs`/`recentFlowsLookbackMs`). The C# `init`
14
+ * properties -> plain mutable fields set after construction (`new XRayTraceSourceOptions()` then assign),
15
+ * since TypeScript object-initializer syntax has no `init`-only equivalent. Unlike the Tempo/Jaeger options
16
+ * there is no endpoint URL: the X-Ray SDK client resolves region/credentials from the ambient AWS
17
+ * environment, so the source takes the client, not a URL.
18
+ */
19
+ export declare class XRayTraceSourceOptions {
20
+ /**
21
+ * How far back a `benzene:mesh:query:correlation` search scans X-Ray for matching traces (ms). Default 24 hours -
22
+ * a business correlation id (a ticket/log id) is typically chased soon after the event, and X-Ray retains
23
+ * traces for 30 days so a longer window is available if you widen it.
24
+ */
25
+ correlationLookbackMs: number;
26
+ /**
27
+ * How far back the fleet view's recent-flows list scans X-Ray (ms). Default 1 hour - the fleet view wants
28
+ * the latest activity, not history; kept separate from {@link correlationLookbackMs} because "what's
29
+ * flowing now" is a much shorter horizon than "find the trace(s) for this ticket".
30
+ */
31
+ recentFlowsLookbackMs: number;
32
+ /**
33
+ * How many of the recent-flows rows to enrich with real span data via a bounded `BatchGetTraces` (X-Ray
34
+ * allows 5 ids/call, so 20 -> <=4 calls). Enrichment reads the pipeline-stamped `benzene.service` as each
35
+ * row's service names (instead of X-Ray's own `ServiceIds`, which on Lambda are infra/handler names like
36
+ * `ApiGatewayLambdaHandler`), the mapped events' real millisecond start time (instead of the
37
+ * second-granularity trace-id epoch), and the real span count. Default 20 (= the fleet cap); a row whose
38
+ * trace can't be fetched or that carries no Benzene span falls back to the summary plane per-row. Set to
39
+ * `0` to opt out entirely (pure-summary behavior: `ServiceIds` names, id-epoch ordering, `events = 0`).
40
+ */
41
+ recentFlowsServiceEnrichmentMax: number;
42
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /** Port of Benzene.Mesh.Fleet.Aws.XRay.XRayTraceSourceOptions. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.XRayTraceSourceOptions = exports.DefaultRecentFlowsServiceEnrichmentMax = exports.DefaultRecentFlowsLookbackMs = exports.DefaultCorrelationLookbackMs = void 0;
5
+ /** Default `correlationLookbackMs`: 24 hours, in milliseconds (`TimeSpan` -> millisecond `number`). */
6
+ exports.DefaultCorrelationLookbackMs = 24 * 60 * 60 * 1000;
7
+ /** Default `recentFlowsLookbackMs`: 1 hour, in milliseconds (`TimeSpan` -> millisecond `number`). */
8
+ exports.DefaultRecentFlowsLookbackMs = 60 * 60 * 1000;
9
+ /** Default `recentFlowsServiceEnrichmentMax`: the fleet cap (20 rows). */
10
+ exports.DefaultRecentFlowsServiceEnrichmentMax = 20;
11
+ /**
12
+ * Tuning for {@link XRayTraceSource}. Only correlation search and recent-flows enrichment need these - a
13
+ * trace lookup is by id (`BatchGetTraces`, no window). X-Ray's `GetTraceSummaries` requires a time range, so
14
+ * a correlation/recent-flows lookup searches a bounded window for matching traces.
15
+ *
16
+ * `TimeSpan` -> millisecond `number` (`correlationLookbackMs`/`recentFlowsLookbackMs`). The C# `init`
17
+ * properties -> plain mutable fields set after construction (`new XRayTraceSourceOptions()` then assign),
18
+ * since TypeScript object-initializer syntax has no `init`-only equivalent. Unlike the Tempo/Jaeger options
19
+ * there is no endpoint URL: the X-Ray SDK client resolves region/credentials from the ambient AWS
20
+ * environment, so the source takes the client, not a URL.
21
+ */
22
+ class XRayTraceSourceOptions {
23
+ /**
24
+ * How far back a `benzene:mesh:query:correlation` search scans X-Ray for matching traces (ms). Default 24 hours -
25
+ * a business correlation id (a ticket/log id) is typically chased soon after the event, and X-Ray retains
26
+ * traces for 30 days so a longer window is available if you widen it.
27
+ */
28
+ correlationLookbackMs = exports.DefaultCorrelationLookbackMs;
29
+ /**
30
+ * How far back the fleet view's recent-flows list scans X-Ray (ms). Default 1 hour - the fleet view wants
31
+ * the latest activity, not history; kept separate from {@link correlationLookbackMs} because "what's
32
+ * flowing now" is a much shorter horizon than "find the trace(s) for this ticket".
33
+ */
34
+ recentFlowsLookbackMs = exports.DefaultRecentFlowsLookbackMs;
35
+ /**
36
+ * How many of the recent-flows rows to enrich with real span data via a bounded `BatchGetTraces` (X-Ray
37
+ * allows 5 ids/call, so 20 -> <=4 calls). Enrichment reads the pipeline-stamped `benzene.service` as each
38
+ * row's service names (instead of X-Ray's own `ServiceIds`, which on Lambda are infra/handler names like
39
+ * `ApiGatewayLambdaHandler`), the mapped events' real millisecond start time (instead of the
40
+ * second-granularity trace-id epoch), and the real span count. Default 20 (= the fleet cap); a row whose
41
+ * trace can't be fetched or that carries no Benzene span falls back to the summary plane per-row. Set to
42
+ * `0` to opt out entirely (pure-summary behavior: `ServiceIds` names, id-epoch ordering, `events = 0`).
43
+ */
44
+ recentFlowsServiceEnrichmentMax = exports.DefaultRecentFlowsServiceEnrichmentMax;
45
+ }
46
+ exports.XRayTraceSourceOptions = XRayTraceSourceOptions;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Port of Benzene.Mesh.Fleet.Aws.XRay - the AWS realisation of the trace-backed fleet reader (alongside
3
+ * `@benzenejs/mesh-fleet-tempo` and `@benzenejs/mesh-fleet-jaeger`): it answers the mesh's `benzene:mesh:query:trace`,
4
+ * `benzene:mesh:query:correlation`, and the fleet view's recent-flows from **AWS X-Ray**, reusing the same
5
+ * `CompositeMeshFleetReadModel`, query handlers, and mesh UI - a backend on the `IMeshTraceSource` seam with
6
+ * zero upstream change.
7
+ *
8
+ * `XRayTraceSource` implements `IMeshTraceSource` over the X-Ray SDK (`BatchGetTraces` by id,
9
+ * `GetTraceSummaries` for correlation/recent-flows); `XRaySegmentMapper` maps X-Ray's segment-document JSON
10
+ * (annotations with underscore keys or metadata with dotted keys, possibly namespaced) into the mesh
11
+ * `MeshTraceEvent` shape. `addXRayFleetReadModel` wires the source + `CompositeMeshFleetReadModel`.
12
+ *
13
+ * Divergences from the C# original (see also each file's header):
14
+ * - `IAmazonXRay` -> the `@aws-sdk/client-xray` `XRayClient`. `_xray.BatchGetTracesAsync(req)` /
15
+ * `_xray.GetTraceSummariesAsync(req)` -> `xray.send(new BatchGetTracesCommand(input))` /
16
+ * `xray.send(new GetTraceSummariesCommand(input))`. The v3 SDK uses PascalCase shape members
17
+ * (`TraceIds`/`Traces`/`Segments`/`Document`/`TraceSummaries`/`NextToken`/`Duration`/`HasError`/…), so the
18
+ * request/response shapes map almost 1:1 to the C#; `StartTime`/`EndTime` are JS `Date`.
19
+ * - C# two constructors (`(IAmazonXRay)` defaulting options, `(IAmazonXRay, options)`) -> one constructor
20
+ * `(xray, options = new XRayTraceSourceOptions())`.
21
+ * - `System.Text.Json.JsonDocument` -> `JSON.parse` + `unknown` type guards; `DateTimeOffset` -> epoch-ms
22
+ * `number`; an unparseable trace-id epoch falls back to `0` (the epoch, the ordering analog of C#
23
+ * `DateTimeOffset.MinValue`). `CancellationToken` -> optional `AbortSignal` (threaded to `send` as
24
+ * `{ abortSignal }`); `Task.WhenAll` -> `Promise.all`; `StringComparer.Ordinal` -> an ordinal compare.
25
+ * - `addXRayFleetReadModel` constructs `new XRayClient({})` inside the `IMeshTraceSource` factory rather than
26
+ * resolving a container-registered `IAmazonXRay` (there is no ambient AWS client token in the TS DI), the
27
+ * same pattern as the `addSqs*` extensions.
28
+ * - Unlike the Tempo/Jaeger ports, `MeshTraceEvent.exceptionType` IS carried here: this port adds the
29
+ * `exceptionType` field to `@benzenejs/mesh-wire`'s `MeshTraceEvent` (it was missing from the snapshot), and
30
+ * the mapper reads `benzene.exception.type` into it (spec §3 "the failure's WHY").
31
+ */
32
+ export * from './XRayTraceSourceOptions';
33
+ export * from './XRaySegmentMapper';
34
+ export * from './XRayTraceSource';
35
+ export * from './Extensions';
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * Port of Benzene.Mesh.Fleet.Aws.XRay - the AWS realisation of the trace-backed fleet reader (alongside
19
+ * `@benzenejs/mesh-fleet-tempo` and `@benzenejs/mesh-fleet-jaeger`): it answers the mesh's `benzene:mesh:query:trace`,
20
+ * `benzene:mesh:query:correlation`, and the fleet view's recent-flows from **AWS X-Ray**, reusing the same
21
+ * `CompositeMeshFleetReadModel`, query handlers, and mesh UI - a backend on the `IMeshTraceSource` seam with
22
+ * zero upstream change.
23
+ *
24
+ * `XRayTraceSource` implements `IMeshTraceSource` over the X-Ray SDK (`BatchGetTraces` by id,
25
+ * `GetTraceSummaries` for correlation/recent-flows); `XRaySegmentMapper` maps X-Ray's segment-document JSON
26
+ * (annotations with underscore keys or metadata with dotted keys, possibly namespaced) into the mesh
27
+ * `MeshTraceEvent` shape. `addXRayFleetReadModel` wires the source + `CompositeMeshFleetReadModel`.
28
+ *
29
+ * Divergences from the C# original (see also each file's header):
30
+ * - `IAmazonXRay` -> the `@aws-sdk/client-xray` `XRayClient`. `_xray.BatchGetTracesAsync(req)` /
31
+ * `_xray.GetTraceSummariesAsync(req)` -> `xray.send(new BatchGetTracesCommand(input))` /
32
+ * `xray.send(new GetTraceSummariesCommand(input))`. The v3 SDK uses PascalCase shape members
33
+ * (`TraceIds`/`Traces`/`Segments`/`Document`/`TraceSummaries`/`NextToken`/`Duration`/`HasError`/…), so the
34
+ * request/response shapes map almost 1:1 to the C#; `StartTime`/`EndTime` are JS `Date`.
35
+ * - C# two constructors (`(IAmazonXRay)` defaulting options, `(IAmazonXRay, options)`) -> one constructor
36
+ * `(xray, options = new XRayTraceSourceOptions())`.
37
+ * - `System.Text.Json.JsonDocument` -> `JSON.parse` + `unknown` type guards; `DateTimeOffset` -> epoch-ms
38
+ * `number`; an unparseable trace-id epoch falls back to `0` (the epoch, the ordering analog of C#
39
+ * `DateTimeOffset.MinValue`). `CancellationToken` -> optional `AbortSignal` (threaded to `send` as
40
+ * `{ abortSignal }`); `Task.WhenAll` -> `Promise.all`; `StringComparer.Ordinal` -> an ordinal compare.
41
+ * - `addXRayFleetReadModel` constructs `new XRayClient({})` inside the `IMeshTraceSource` factory rather than
42
+ * resolving a container-registered `IAmazonXRay` (there is no ambient AWS client token in the TS DI), the
43
+ * same pattern as the `addSqs*` extensions.
44
+ * - Unlike the Tempo/Jaeger ports, `MeshTraceEvent.exceptionType` IS carried here: this port adds the
45
+ * `exceptionType` field to `@benzenejs/mesh-wire`'s `MeshTraceEvent` (it was missing from the snapshot), and
46
+ * the mapper reads `benzene.exception.type` into it (spec §3 "the failure's WHY").
47
+ */
48
+ __exportStar(require("./XRayTraceSourceOptions"), exports);
49
+ __exportStar(require("./XRaySegmentMapper"), exports);
50
+ __exportStar(require("./XRayTraceSource"), exports);
51
+ __exportStar(require("./Extensions"), exports);
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@benzenejs/mesh-fleet-aws-xray",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.2",
4
+ "description": "TypeScript port of Benzene.Mesh.Fleet.Aws.XRay",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "dependencies": {
10
+ "@aws-sdk/client-xray": "^3.700.0",
11
+ "@benzenejs/abstractions": "0.1.0-beta.2",
12
+ "@benzenejs/mesh-collector": "0.1.0-beta.2",
13
+ "@benzenejs/mesh-contracts": "0.1.0-beta.2",
14
+ "@benzenejs/mesh-wire": "0.1.0-beta.2"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ]
19
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.