@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.
- package/dist/Extensions.d.ts +27 -0
- package/dist/Extensions.js +39 -0
- package/dist/XRaySegmentMapper.d.ts +27 -0
- package/dist/XRaySegmentMapper.js +172 -0
- package/dist/XRayTraceSource.d.ts +60 -0
- package/dist/XRayTraceSource.js +309 -0
- package/dist/XRayTraceSourceOptions.d.ts +42 -0
- package/dist/XRayTraceSourceOptions.js +46 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51 -0
- package/package.json +17 -4
- package/README.md +0 -3
|
@@ -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;
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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