Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe('calculateTraceDagEV', () => {
it('calculates TraceGraph', () => {
const traceDag = calculateTraceDagEV(transformedTrace.asOtelTrace());
const { vertices: nodes } = traceDag;
expect(nodes.length).toBe(9);
assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 224);
expect(nodes.length).toBe(10);
assertData(nodes, 'service1', 'op1', 1, 0, 390, 39, 209);
// accumulate data (count,times)
assertData(nodes, 'service1', 'op2', 2, 1, 70, 7, 70);
// self-time is substracted from child
Expand All @@ -36,6 +36,22 @@ describe('calculateTraceDagEV', () => {
// fork-join self-times are calculated correctly (self-time drange)
assertData(nodes, 'service1', 'op6', 1, 0, 10, 1, 1);
assertData(nodes, 'service1', 'op7', 2, 0, 17, 1.7, 17);
// DAG span with multiple references (CHILD_OF to span-1 + FOLLOWS_FROM to span-6)
assertData(nodes, 'service1', 'op8', 1, 0, 15, 1.5, 15);
});

it('includes additional DAG edges from span links', () => {
const traceDag = calculateTraceDagEV(transformedTrace.asOtelTrace());
const { edges } = traceDag;
// span-8 (op8) has a FOLLOWS_FROM to span-6 (op4), creating an additional edge
// These should be rendered as non-blocking (dashed) edges
const additionalEdges = edges.filter(e => {
// Find edges pointing to the op8 node that are not from the parent (op1 node)
const toStr = String(e.to);
const fromStr = String(e.from);
return toStr.includes('op8') && fromStr.includes('op4');
});
expect(additionalEdges.length).toBe(1);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,36 @@
"logs": [],
"processID": "p1",
"warnings": null
},
{
"traceID": "trace-123",
"spanID": "span-8",
"flags": 1,
"operationName": "op8",
"startTime": 1542666453350000,
"duration": 15000,
"references": [
{
"refType": "CHILD_OF",
"traceID": "trace-123",
"spanID": "span-1"
},
{
"refType": "FOLLOWS_FROM",
"traceID": "trace-123",
"spanID": "span-6"
}
],
"tags": [
{
"key": "span.kind",
"type": "string",
"value": "internal"
}
],
"logs": [],
"processID": "p1",
"warnings": null
}
],
"processes": {
Expand Down
1 change: 1 addition & 0 deletions packages/jaeger-ui/src/model/trace-dag/DenseTrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function convSpans(spans: ReadonlyArray<IOtelSpan>) {
attributes,
children: new Set<string>(),
skipToChild: false,
links: span.links || [],
};
const parent = parentSpanID && map.get(parentSpanID);
if (!parent) {
Expand Down
181 changes: 181 additions & 0 deletions packages/jaeger-ui/src/model/trace-dag/convPlexus.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Copyright (c) 2026 The Jaeger Authors.
// SPDX-License-Identifier: Apache-2.0

import convPlexus from './convPlexus';
import TraceDag from './TraceDag';

describe('convPlexus', () => {
it('generates parent-child edges', () => {
const dag = new TraceDag();
dag.addNode('root', null, { operation: 'op1', service: 'svc1', members: [] });
dag.addNode('child1', 'root', { operation: 'op2', service: 'svc1', members: [] });
dag.addNode('child2', 'root', { operation: 'op3', service: 'svc2', members: [] });

const { edges, vertices } = convPlexus(dag.nodesMap);
expect(vertices).toHaveLength(3);
expect(edges).toHaveLength(2);
expect(edges).toEqual(
expect.arrayContaining([
expect.objectContaining({ from: 'root', to: 'child1' }),
expect.objectContaining({ from: 'root', to: 'child2' }),
])
);
});

it('generates additional edges from span links', () => {
const dag = new TraceDag();
const traceID = 'trace-abc';

const mockSpanA = { traceID, spanID: 'spanA', links: [] };
const mockSpanB = { traceID, spanID: 'spanB', links: [] };
const mockSpanC = {
traceID,
spanID: 'spanC',
links: [{ traceID, spanID: 'spanA', attributes: [] }],
};

dag.addNode('nodeA', null, {
operation: 'opA',
service: 'svc1',
members: [{ id: 'spanA', span: mockSpanA, links: [] }],
});
dag.addNode('nodeB', 'nodeA', {
operation: 'opB',
service: 'svc1',
members: [{ id: 'spanB', span: mockSpanB, links: [] }],
});
dag.addNode('nodeC', 'nodeB', {
operation: 'opC',
service: 'svc2',
members: [{ id: 'spanC', span: mockSpanC, links: [{ traceID, spanID: 'spanA', attributes: [] }] }],
});

const { edges, vertices } = convPlexus(dag.nodesMap);
expect(vertices).toHaveLength(3);
// parent-child: nodeA->nodeB, nodeB->nodeC
// additional: nodeA->nodeC (from link)
expect(edges).toHaveLength(3);
expect(edges).toEqual(
expect.arrayContaining([
expect.objectContaining({ from: 'nodeA', to: 'nodeB' }),
expect.objectContaining({ from: 'nodeB', to: 'nodeC' }),
expect.objectContaining({ from: 'nodeA', to: 'nodeC' }),
])
);
});

it('does not create duplicate edges', () => {
const dag = new TraceDag();
const traceID = 'trace-abc';

const mockSpanA = { traceID, spanID: 'spanA', links: [] };
// spanB has a link to spanA, but nodeA is already NodeB's parent
const mockSpanB = {
traceID,
spanID: 'spanB',
links: [{ traceID, spanID: 'spanA', attributes: [] }],
};

dag.addNode('nodeA', null, {
operation: 'opA',
service: 'svc1',
members: [{ id: 'spanA', span: mockSpanA, links: [] }],
});
dag.addNode('nodeB', 'nodeA', {
operation: 'opB',
service: 'svc1',
members: [{ id: 'spanB', span: mockSpanB, links: [{ traceID, spanID: 'spanA', attributes: [] }] }],
});

const { edges } = convPlexus(dag.nodesMap);
// Only one edge: nodeA->nodeB (link is same as parent-child, no duplicate)
expect(edges).toHaveLength(1);
expect(edges[0]).toEqual(expect.objectContaining({ from: 'nodeA', to: 'nodeB' }));
});

it('ignores links to spans in different traces', () => {
const dag = new TraceDag();

const mockSpanA = { traceID: 'trace-1', spanID: 'spanA', links: [] };
const mockSpanB = {
traceID: 'trace-1',
spanID: 'spanB',
links: [{ traceID: 'trace-OTHER', spanID: 'spanX', attributes: [] }],
};

dag.addNode('nodeA', null, {
operation: 'opA',
service: 'svc1',
members: [{ id: 'spanA', span: mockSpanA, links: [] }],
});
dag.addNode('nodeB', 'nodeA', {
operation: 'opB',
service: 'svc1',
members: [
{
id: 'spanB',
span: mockSpanB,
links: [{ traceID: 'trace-OTHER', spanID: 'spanX', attributes: [] }],
},
],
});

const { edges } = convPlexus(dag.nodesMap);
// Only parent-child edge: nodeA->nodeB (cross-trace link is ignored)
expect(edges).toHaveLength(1);
});

it('handles nodes with no members gracefully', () => {
const dag = new TraceDag();
dag.addNode('root', null, { operation: 'op1', service: 'svc1' });
dag.addNode('child', 'root', { operation: 'op2', service: 'svc2' });

const { edges, vertices } = convPlexus(dag.nodesMap);
expect(vertices).toHaveLength(2);
expect(edges).toHaveLength(1);
});

it('ignores self-referencing links', () => {
const dag = new TraceDag();
const traceID = 'trace-abc';

// spanA links to itself — should not create an edge to own node
const mockSpanA = {
traceID,
spanID: 'spanA',
links: [{ traceID, spanID: 'spanA', attributes: [] }],
};

dag.addNode('nodeA', null, {
operation: 'opA',
service: 'svc1',
members: [{ id: 'spanA', span: mockSpanA, links: [{ traceID, spanID: 'spanA', attributes: [] }] }],
});

const { edges } = convPlexus(dag.nodesMap);
// No edges — root has no parent, and self-link is ignored
expect(edges).toHaveLength(0);
});

it('ignores links to unknown spans not in the DAG', () => {
const dag = new TraceDag();
const traceID = 'trace-abc';

const mockSpanA = {
traceID,
spanID: 'spanA',
links: [{ traceID, spanID: 'spanNONEXISTENT', attributes: [] }],
};

dag.addNode('nodeA', null, {
operation: 'opA',
service: 'svc1',
members: [
{ id: 'spanA', span: mockSpanA, links: [{ traceID, spanID: 'spanNONEXISTENT', attributes: [] }] },
],
});

const { edges } = convPlexus(dag.nodesMap);
expect(edges).toHaveLength(0);
});
});
61 changes: 54 additions & 7 deletions packages/jaeger-ui/src/model/trace-dag/convPlexus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,74 @@

import { TEdge } from '@jaegertracing/plexus/lib/types';

import { NodeID } from './types';
import { NodeID, TDenseSpanMembers } from './types';
import TDagNode from './types/TDagNode';
import TDagPlexusVertex from './types/TDagPlexusVertex';

export default function convPlexus<T extends { [k: string]: unknown }>(nodesMap: Map<NodeID, TDagNode<T>>) {
const vertices: TDagPlexusVertex<T>[] = [];
const edges: TEdge[] = [];
const nodes = [...nodesMap.values()];

// Build a mapping from spanID to nodeID for resolving links
const spanIdToNodeId = new Map<string, NodeID>();
for (let i = 0; i < nodes.length; i++) {
const dagNode = nodes[i];
const nodeData = dagNode as unknown as TDagNode<TDenseSpanMembers>;
if (nodeData.members) {
nodeData.members.forEach(member => {
spanIdToNodeId.set(member.id, dagNode.id);
});
}
}

// Track existing edges to avoid duplicates
const edgeSet = new Set<string>();

for (let i = 0; i < nodes.length; i++) {
const dagNode = nodes[i];
vertices.push({
key: dagNode.id,
data: dagNode,
});
if (!dagNode.parentID) {
continue;

// Add parent-child edge
if (dagNode.parentID) {
const edgeKey = `${dagNode.parentID}->${dagNode.id}`;
if (!edgeSet.has(edgeKey)) {
edges.push({
from: dagNode.parentID,
to: dagNode.id,
});
edgeSet.add(edgeKey);
}
}

// Add additional edges from span links (non-parent references)
const nodeData = dagNode as unknown as TDagNode<TDenseSpanMembers>;
if (nodeData.members) {
nodeData.members.forEach(member => {
if (member.links && member.links.length > 0) {
member.links.forEach(link => {
// Only consider links within the same trace
if (link.traceID === member.span.traceID) {
const linkedNodeId = spanIdToNodeId.get(link.spanID);
if (linkedNodeId && linkedNodeId !== dagNode.id) {
// Edge goes from the linked (referenced) node to the current node
const edgeKey = `${linkedNodeId}->${dagNode.id}`;
if (!edgeSet.has(edgeKey)) {
edges.push({
from: linkedNodeId,
to: dagNode.id,
});
edgeSet.add(edgeKey);
}
}
}
});
}
});
}
edges.push({
from: dagNode.parentID,
to: dagNode.id,
});
}
return { edges, vertices };
}
3 changes: 2 additions & 1 deletion packages/jaeger-ui/src/model/trace-dag/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2018-2020 The Jaeger Authors.
// SPDX-License-Identifier: Apache-2.0

import { IOtelSpan } from '../../../types/otel';
import { ILink, IOtelSpan } from '../../../types/otel';
import { TNil } from '../../../types';

export type NodeID = string;
Expand All @@ -15,6 +15,7 @@ export type TDenseSpan = {
parentID: string | TNil;
skipToChild: boolean;
children: Set<string>;
links: ILink[];
};

export type TDenseSpanMembers = {
Expand Down
Loading