@contrail/cli 3.5.0-alpha.11f30e6 → 3.5.0-alpha.9d881a5
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/README.md +1 -1
- package/dist/api/dash0/spans.d.ts +1 -6
- package/dist/api/dash0/spans.js +6 -24
- package/dist/commands/event-workflows/assess-impact.js +5 -9
- package/dist/helpers/event-workflow-impact/subtree.js +2 -9
- package/dist/helpers/event-workflow-impact/subtree.spec.js +11 -43
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ $ npm install -g @contrail/cli
|
|
|
17
17
|
$ contrail COMMAND
|
|
18
18
|
running command...
|
|
19
19
|
$ contrail (--version)
|
|
20
|
-
@contrail/cli/3.5.0-alpha.
|
|
20
|
+
@contrail/cli/3.5.0-alpha.9d881a5 linux-x64 node-v24.14.1
|
|
21
21
|
$ contrail --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ contrail COMMAND
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { Dash0Client } from './client';
|
|
2
2
|
import { AttributeFilter, FlatSpan, OtlpAttribute, OtlpResourceSpans, OtlpSpan, TimeReferenceRange } from './types';
|
|
3
3
|
export declare function paginateSpans(client: Dash0Client, filter: AttributeFilter[], timeRange: TimeReferenceRange, limit?: number): AsyncGenerator<OtlpResourceSpans, void, void>;
|
|
4
|
-
export
|
|
5
|
-
traceId: string;
|
|
6
|
-
anchorNs: bigint;
|
|
7
|
-
}
|
|
8
|
-
export declare function sampleTraceIdsForAction(client: Dash0Client, actionIdentifier: string, timeRange: TimeReferenceRange, maxTraces: number): Promise<SampledTrace[]>;
|
|
9
|
-
export declare function bracketWindowAround(anchorNs: bigint, halfWindowMin?: number): TimeReferenceRange;
|
|
4
|
+
export declare function sampleTraceIdsForAction(client: Dash0Client, actionIdentifier: string, timeRange: TimeReferenceRange, maxTraces: number): Promise<string[]>;
|
|
10
5
|
export declare function fetchTraceSpans(client: Dash0Client, traceIdHex: string, timeRange: TimeReferenceRange): Promise<FlatSpan[]>;
|
|
11
6
|
export declare function toFlat(sp: OtlpSpan, resourceAttrs: Record<string, string | number | boolean>): FlatSpan;
|
|
12
7
|
export declare function flattenAttrs(attrs: OtlpAttribute[]): Record<string, string | number | boolean>;
|
package/dist/api/dash0/spans.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.b64ToHex = exports.flattenAttrs = exports.toFlat = exports.fetchTraceSpans = exports.
|
|
3
|
+
exports.b64ToHex = exports.flattenAttrs = exports.toFlat = exports.fetchTraceSpans = exports.sampleTraceIdsForAction = exports.paginateSpans = void 0;
|
|
4
4
|
const PAGE_LIMIT = 50;
|
|
5
5
|
const MAX_PAGES_PER_QUERY = 200;
|
|
6
6
|
async function* paginateSpans(client, filter, timeRange, limit = PAGE_LIMIT) {
|
|
@@ -21,8 +21,8 @@ async function* paginateSpans(client, filter, timeRange, limit = PAGE_LIMIT) {
|
|
|
21
21
|
}
|
|
22
22
|
exports.paginateSpans = paginateSpans;
|
|
23
23
|
async function sampleTraceIdsForAction(client, actionIdentifier, timeRange, maxTraces) {
|
|
24
|
-
const seen = new
|
|
25
|
-
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for await (const rs of paginateSpans(client, [
|
|
26
26
|
{
|
|
27
27
|
key: 'contrail.event_workflow.action.identifier',
|
|
28
28
|
operator: 'is',
|
|
@@ -31,33 +31,15 @@ async function sampleTraceIdsForAction(client, actionIdentifier, timeRange, maxT
|
|
|
31
31
|
], timeRange)) {
|
|
32
32
|
for (const ss of rs.scopeSpans ?? []) {
|
|
33
33
|
for (const sp of ss.spans ?? []) {
|
|
34
|
-
|
|
35
|
-
const startNs = BigInt(sp.startTimeUnixNano);
|
|
36
|
-
const existing = seen.get(traceId);
|
|
37
|
-
if (existing === undefined || startNs < existing) {
|
|
38
|
-
seen.set(traceId, startNs);
|
|
39
|
-
}
|
|
34
|
+
seen.add(b64ToHex(sp.traceId));
|
|
40
35
|
if (seen.size >= maxTraces)
|
|
41
|
-
|
|
36
|
+
return [...seen];
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
|
-
return [...seen
|
|
40
|
+
return [...seen];
|
|
46
41
|
}
|
|
47
42
|
exports.sampleTraceIdsForAction = sampleTraceIdsForAction;
|
|
48
|
-
// Build a Dash0 absolute time range bracketing a trace anchor. ±halfWindowMin
|
|
49
|
-
// around the anchor in ISO RFC3339; resilient to clock drift between sample
|
|
50
|
-
// and fetch and wide enough to capture the full trace.
|
|
51
|
-
function bracketWindowAround(anchorNs, halfWindowMin = 5) {
|
|
52
|
-
const halfMs = BigInt(halfWindowMin) * 60000n;
|
|
53
|
-
const fromMs = Number((anchorNs - halfMs * 1000000n) / 1000000n);
|
|
54
|
-
const toMs = Number((anchorNs + halfMs * 1000000n) / 1000000n);
|
|
55
|
-
return {
|
|
56
|
-
from: new Date(fromMs).toISOString(),
|
|
57
|
-
to: new Date(toMs).toISOString(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
exports.bracketWindowAround = bracketWindowAround;
|
|
61
43
|
async function fetchTraceSpans(client, traceIdHex, timeRange) {
|
|
62
44
|
const out = [];
|
|
63
45
|
for await (const rs of paginateSpans(client, [{ key: 'otel.trace.id', operator: 'is', value: traceIdHex }], timeRange)) {
|
|
@@ -60,24 +60,20 @@ class AssessImpact extends basecommand_1.BaseCommand {
|
|
|
60
60
|
this.log(`Samples: target ${samples}`);
|
|
61
61
|
this.log('');
|
|
62
62
|
this.log('Sampling trace IDs...');
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
63
|
+
const traceIds = await (0, spans_1.sampleTraceIdsForAction)(client, identifier, timeRange, samples);
|
|
64
|
+
if (traceIds.length === 0) {
|
|
65
65
|
this.warn(`No spans found for action.identifier="${identifier}" in the last ${lookback}. ` +
|
|
66
66
|
'Verify the identifier and dataset.');
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
this.log(`Got ${
|
|
69
|
+
this.log(`Got ${traceIds.length} trace(s). Fetching subtrees...`);
|
|
70
70
|
const perTraceDescendants = [];
|
|
71
71
|
const actionDurationsNs = [];
|
|
72
72
|
let dropped = 0;
|
|
73
73
|
let totalOrphans = 0;
|
|
74
74
|
let preciseRootCount = 0;
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
// time. Avoids sliding-window edge effects where a trace at the boundary
|
|
78
|
-
// of the lookback window has its earliest spans excluded.
|
|
79
|
-
const traceWindow = (0, spans_1.bracketWindowAround)(anchorNs);
|
|
80
|
-
const spans = await (0, spans_1.fetchTraceSpans)(client, traceId, traceWindow);
|
|
75
|
+
for (const traceId of traceIds) {
|
|
76
|
+
const spans = await (0, spans_1.fetchTraceSpans)(client, traceId, timeRange);
|
|
81
77
|
const result = (0, subtree_1.extractActionSubtree)(spans, identifier);
|
|
82
78
|
if (!result) {
|
|
83
79
|
dropped++;
|
|
@@ -83,17 +83,10 @@ function extractActionSubtree(spans, identifier) {
|
|
|
83
83
|
queue.push({ spanId: c.spanId, depth: depth + 1, serverAncestors: childServerCount });
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
// An orphan is a span whose parent was never observed AND whose time range
|
|
87
|
-
// falls inside the action span's time range. Orphans outside that range are
|
|
88
|
-
// unrelated trace spans (siblings of the action's lineage) and shouldn't
|
|
89
|
-
// taint the propagation-gap warning.
|
|
90
86
|
let orphanCount = 0;
|
|
91
87
|
for (const s of spans) {
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
if (s.startNs < actionSpan.startNs || s.startNs > actionSpan.endNs)
|
|
95
|
-
continue;
|
|
96
|
-
orphanCount++;
|
|
88
|
+
if (s.parentSpanId && !byId.has(s.parentSpanId))
|
|
89
|
+
orphanCount++;
|
|
97
90
|
}
|
|
98
91
|
return { descendants, actionSpan, preciseRoot, orphanCount };
|
|
99
92
|
}
|
|
@@ -88,48 +88,16 @@ describe('extractActionSubtree', () => {
|
|
|
88
88
|
expect(ids).toEqual(['http1', 'nestedDdb']);
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
s({
|
|
103
|
-
spanId: 'innerOrphan',
|
|
104
|
-
parentSpanId: 'missing',
|
|
105
|
-
startNs: 5000n,
|
|
106
|
-
endNs: 6000n,
|
|
107
|
-
}),
|
|
108
|
-
];
|
|
109
|
-
const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
|
|
110
|
-
expect(result.orphanCount).toBe(1);
|
|
111
|
-
});
|
|
112
|
-
it('ignores orphans whose time falls outside the action span', () => {
|
|
113
|
-
const spans = [
|
|
114
|
-
s({
|
|
115
|
-
spanId: 'consumer',
|
|
116
|
-
parentSpanId: '',
|
|
117
|
-
startNs: 1000n,
|
|
118
|
-
endNs: 9000n,
|
|
119
|
-
attrs: { 'contrail.event_workflow.action.identifier': IDENT },
|
|
120
|
-
}),
|
|
121
|
-
// Sibling-of-action lineage span. Real parent is unobserved (window
|
|
122
|
-
// edge effect) but its time is outside the action's range — noise,
|
|
123
|
-
// not a real propagation gap.
|
|
124
|
-
s({
|
|
125
|
-
spanId: 'unrelated',
|
|
126
|
-
parentSpanId: 'missing',
|
|
127
|
-
startNs: 50000n,
|
|
128
|
-
endNs: 60000n,
|
|
129
|
-
}),
|
|
130
|
-
];
|
|
131
|
-
const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
|
|
132
|
-
expect(result.orphanCount).toBe(0);
|
|
133
|
-
});
|
|
91
|
+
it('counts orphans (parent_id pointing outside the trace)', () => {
|
|
92
|
+
const spans = [
|
|
93
|
+
s({
|
|
94
|
+
spanId: 'consumer',
|
|
95
|
+
parentSpanId: '',
|
|
96
|
+
attrs: { 'contrail.event_workflow.action.identifier': IDENT },
|
|
97
|
+
}),
|
|
98
|
+
s({ spanId: 'unattached', parentSpanId: 'missing-parent' }),
|
|
99
|
+
];
|
|
100
|
+
const result = (0, subtree_1.extractActionSubtree)(spans, IDENT);
|
|
101
|
+
expect(result.orphanCount).toBe(1);
|
|
134
102
|
});
|
|
135
103
|
});
|
package/oclif.manifest.json
CHANGED