Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -14,4 +14,6 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("org.apache.commons:commons-lang3:3.12.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.7.1")
testImplementation("org.mockito:mockito-core:3.9.0")
testImplementation("org.mockito:mockito-inline:3.9.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ public class ApiTraceGraph {
// set of outbound edges for apiNode
private final Map<Integer, Set<Integer>> apiNodeIdxToEdges;
private final Map<ByteBuffer, Integer> eventIdToIndexInTrace;
// map of api node to index in api nodes list, since we want to build edges between api nodes
private final Map<ApiNode<Event>, Integer> apiNodeToIndex;
// map of entry boundary event to api node. each api node has one entry boundary event
private final Map<Event, ApiNode<Event>> entryBoundaryToApiNode;

// map of head event id of api node to index in api node list, helps in building edges between api
// nodes
private final Map<ByteBuffer, Integer> apiNodeHeadEventIdToIndex;

// map of event id of entry api boundary event to api node. each api node has at most one entry
// boundary event
private final Map<ByteBuffer, ApiNode<Event>> entryApiBoundaryEventIdToApiNode;

private final Table<Integer, Integer, Edge> traceEdgeTable;
// set of exit boundary events of apiNode, with no outgoing edge to any apiNode
private final Set<Integer> apiExitBoundaryEventIdxWithNoOutgoingEdge;
Expand All @@ -64,8 +69,8 @@ public ApiTraceGraph(StructuredTrace trace) {
apiNodeIdxToEdges = Maps.newHashMap();
eventIdToIndexInTrace = Maps.newHashMap();

apiNodeToIndex = Maps.newHashMap();
entryBoundaryToApiNode = Maps.newHashMap();
apiNodeHeadEventIdToIndex = Maps.newHashMap();
entryApiBoundaryEventIdToApiNode = Maps.newHashMap();
traceEdgeTable = HashBasedTable.create();

apiExitBoundaryEventIdxWithNoOutgoingEdge = Sets.newHashSet();
Expand Down Expand Up @@ -109,11 +114,13 @@ public List<Event> getApiEntryBoundaryEventsWithIncomingEdge() {
}

public List<ApiNodeEventEdge> getOutboundEdgesForApiNode(ApiNode<Event> apiNode) {
int idx = apiNodeToIndex.get(apiNode);
int idx = apiNodeHeadEventIdToIndex.get(apiNode.getHeadEvent().getEventId());
if (!apiNodeIdxToEdges.containsKey(idx)) {
return Collections.emptyList();
}
return apiNodeIdxToEdges.get(apiNodeToIndex.get(apiNode)).stream()
return apiNodeIdxToEdges
.get(apiNodeHeadEventIdToIndex.get(apiNode.getHeadEvent().getEventId()))
.stream()
.map(apiNodeEventEdgeList::get)
.collect(Collectors.toList());
}
Expand Down Expand Up @@ -281,7 +288,7 @@ private void buildApiNodeEdges(StructuredTraceGraph graph) {
if (EnrichedSpanUtils.isEntryApiBoundary(exitBoundaryEventChild)) {
// get the api node exit boundary event is connecting to
ApiNode<Event> destinationApiNode =
entryBoundaryToApiNode.get(exitBoundaryEventChild);
entryApiBoundaryEventIdToApiNode.get(exitBoundaryEventChild.getEventId());

Optional<ApiNodeEventEdge> edgeBetweenApiNodes =
createEdgeBetweenApiNodes(
Expand All @@ -292,7 +299,9 @@ private void buildApiNodeEdges(StructuredTraceGraph graph) {
apiExitBoundaryEventIdxWithOutgoingEdge.add(edge.getSrcEventIndex());
apiEntryBoundaryEventIdxWithIncomingEdge.add(edge.getTgtEventIndex());
apiNodeIdxToEdges
.computeIfAbsent(apiNodeToIndex.get(apiNode), v -> Sets.newHashSet())
.computeIfAbsent(
apiNodeHeadEventIdToIndex.get(apiNode.getHeadEvent().getEventId()),
v -> Sets.newHashSet())
.add(apiNodeEventEdgeList.size() - 1);
});
} else {
Expand Down Expand Up @@ -322,8 +331,9 @@ private Optional<ApiNodeEventEdge> createEdgeBetweenApiNodes(
Event entryBoundaryEventOfDestinationApiNode) {
if (destinationApiNode != null) {
// get the indexes in apiNodes list to create an edge
Integer srcIndex = apiNodeToIndex.get(srcApiNode);
Integer targetIndex = apiNodeToIndex.get(destinationApiNode);
Integer srcIndex = apiNodeHeadEventIdToIndex.get(srcApiNode.getHeadEvent().getEventId());
Integer targetIndex =
apiNodeHeadEventIdToIndex.get(destinationApiNode.getHeadEvent().getEventId());

// Get the actual edge from trace connecting exitBoundaryEvent and child
Integer srcIndexInTrace =
Expand Down Expand Up @@ -381,8 +391,10 @@ private void buildApiExitBoundaryEventWithNoOutgoingEdge() {
private void buildApiNodeToIndexMap() {
for (int i = 0; i < apiNodeList.size(); i++) {
ApiNode<Event> apiNode = apiNodeList.get(i);
apiNode.getEntryApiBoundaryEvent().ifPresent(e -> entryBoundaryToApiNode.put(e, apiNode));
apiNodeToIndex.put(apiNode, i);
apiNode
.getEntryApiBoundaryEvent()
.ifPresent(e -> entryApiBoundaryEventIdToApiNode.put(e.getEventId(), apiNode));
apiNodeHeadEventIdToIndex.put(apiNode.getHeadEvent().getEventId(), i);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.hypertrace.traceenricher.trace.util;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.hypertrace.core.datamodel.StructuredTrace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiTraceGraphBuilder {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit tests?

private static final Logger LOG = LoggerFactory.getLogger(ApiTraceGraphBuilder.class);

private static ThreadLocal<ApiTraceGraph> cachedGraph = new ThreadLocal<>();
private static ThreadLocal<StructuredTrace> cachedTrace = new ThreadLocal<>();

public static ApiTraceGraph buildGraph(StructuredTrace trace) {
if (!GraphBuilderUtil.isSameStructuredTrace(cachedTrace.get(), trace)) {
Instant start = Instant.now();
ApiTraceGraph graph = new ApiTraceGraph(trace);
LOG.debug(
"Time taken in building ApiTraceGraph:{} for tenantId:{}",
Duration.between(start, Instant.now()).toMillis(),
TimeUnit.MILLISECONDS,
trace.getCustomerId());
cachedTrace.set(StructuredTrace.newBuilder(trace).build());
cachedGraph.set(graph);
return graph;
}
return cachedGraph.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.hypertrace.traceenricher.trace.util;

import org.hypertrace.core.datamodel.StructuredTrace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GraphBuilderUtil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add unit tests for this util?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

private static final Logger LOG = LoggerFactory.getLogger(GraphBuilderUtil.class);

/**
* optimistic method of comparing two trace for considering rebuilding of entire graph structure.
*/
static boolean isSameStructuredTrace(StructuredTrace cachedTrace, StructuredTrace trace) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the method name could be misleading, we are trying to check if the components in the Trace are same as previous or not. Trace might have changed due to event enrichment or trace enrichment.

if (cachedTrace == null || trace == null) {
LOG.debug("Cached and Input trace are not same. Reason: one of the input is null");
return false;
}
// is processed and cached are same trace?
if (!cachedTrace.getCustomerId().equals(trace.getCustomerId())
|| !cachedTrace.getTraceId().equals(trace.getTraceId())) {
LOG.debug(
"Cached and Input trace are not same. Reason: doesn't not match either traceId or tenantId");
return false;
}

// trace internally changed (full trace comparision is costly, so we are doing only with
// required fields)
if (cachedTrace.getEntityList().size() != trace.getEntityList().size()
|| cachedTrace.getEventList().size() != trace.getEventList().size()
|| cachedTrace.getEntityEdgeList().size() != trace.getEntityEdgeList().size()
|| cachedTrace.getEntityEventEdgeList().size() != trace.getEntityEventEdgeList().size()
|| cachedTrace.getEventEdgeList().size() != trace.getEventEdgeList().size()) {
LOG.debug(
"Cached and Input trace are not same. Reason: they are having different size either for event or entities");
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.hypertrace.traceenricher.trace.util;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.hypertrace.core.datamodel.StructuredTrace;
import org.hypertrace.core.datamodel.shared.StructuredTraceGraph;
import org.slf4j.Logger;
Expand All @@ -12,41 +15,18 @@ public class StructuredTraceGraphBuilder {
private static ThreadLocal<StructuredTrace> cachedTrace = new ThreadLocal<>();

public static StructuredTraceGraph buildGraph(StructuredTrace trace) {
// trace doesn't exist
if (cachedTrace.get() == null) {
LOG.debug("Building structured trace graph. Reason: no cached trace");
if (!GraphBuilderUtil.isSameStructuredTrace(cachedTrace.get(), trace)) {
Instant start = Instant.now();
StructuredTraceGraph graph = StructuredTraceGraph.createGraph(trace);
cachedTrace.set(StructuredTrace.newBuilder(trace).build());
cachedGraph.set(graph);
return graph;
}

// is processed and cached are same trace?
if (!cachedTrace.get().getCustomerId().equals(trace.getCustomerId())
|| !cachedTrace.get().getTraceId().equals(trace.getTraceId())) {
LOG.debug(
"Building structured trace graph. Reason: cached trace and current trace doesn't not match");
StructuredTraceGraph graph = StructuredTraceGraph.createGraph(trace);
cachedTrace.set(StructuredTrace.newBuilder(trace).build());
cachedGraph.set(graph);
return graph;
}

// trace internally changed
if (cachedTrace.get().getEntityList().size() != trace.getEntityList().size()
|| cachedTrace.get().getEventList().size() != trace.getEventList().size()
|| cachedTrace.get().getEntityEdgeList().size() != trace.getEntityEdgeList().size()
|| cachedTrace.get().getEntityEventEdgeList().size()
!= trace.getEntityEventEdgeList().size()
|| cachedTrace.get().getEventEdgeList().size() != trace.getEventEdgeList().size()) {
LOG.debug(
"Building structured trace graph. Reason: cached trace and current trace have different size");
StructuredTraceGraph graph = StructuredTraceGraph.createGraph(trace);
"Time taken in building StructuredTraceGraph:{} for tenantId:{}",
Duration.between(start, Instant.now()).toMillis(),
TimeUnit.MILLISECONDS,
trace.getCustomerId());
cachedTrace.set(StructuredTrace.newBuilder(trace).build());
cachedGraph.set(graph);
return graph;
}

return cachedGraph.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.hypertrace.traceenricher.trace.util;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.nio.ByteBuffer;
import java.util.List;
import org.hypertrace.core.datamodel.Edge;
import org.hypertrace.core.datamodel.Entity;
import org.hypertrace.core.datamodel.Event;
import org.hypertrace.core.datamodel.StructuredTrace;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;

public class ApiTraceGraphBuilderTest {

@Test
void testBuildGraph() {

Entity entity = mock(Entity.class);
Event parent = mock(Event.class);
Event child = mock(Event.class);
Edge eventEdge = mock(Edge.class);

StructuredTrace underTestTrace = mock(StructuredTrace.class);
when(underTestTrace.getCustomerId()).thenReturn("__defaultTenant");
when(underTestTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));
when(underTestTrace.getEntityList()).thenReturn(List.of(entity));
when(underTestTrace.getEventList()).thenReturn(List.of(parent, child));
when(underTestTrace.getEntityEdgeList()).thenReturn(List.of());
when(underTestTrace.getEntityEventEdgeList()).thenReturn(List.of());
when(underTestTrace.getEventEdgeList()).thenReturn(List.of(eventEdge));

// structure trace builder
try (MockedStatic<StructuredTrace> builderMockedStatic = mockStatic(StructuredTrace.class)) {
StructuredTrace.Builder builder = mock(StructuredTrace.Builder.class);
when(builder.build()).thenReturn(underTestTrace);

builderMockedStatic
.when(() -> StructuredTrace.newBuilder(underTestTrace))
.thenReturn(builder);

// make two calls, and check that first call create cache entries, and second call uses same
try (MockedConstruction<ApiTraceGraph> mockedConstruction =
mockConstruction(ApiTraceGraph.class)) {
// first call
ApiTraceGraph actual = ApiTraceGraphBuilder.buildGraph(underTestTrace);
Assertions.assertNotNull(actual);
Assertions.assertEquals(1, mockedConstruction.constructed().size());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this mockedConstruction.constructed().size() size representing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of times new ApiTraceGraph(trace) is called.


// second call
ApiTraceGraph second = ApiTraceGraphBuilder.buildGraph(underTestTrace);
Assertions.assertEquals(actual, second);
Assertions.assertEquals(1, mockedConstruction.constructed().size());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, its also important to test that the cached graph is not returned when a different trace is passed?

Copy link
Copy Markdown
Contributor Author

@kotharironak kotharironak May 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first call is testing that we are going to if block where the cache is prepared. And GraphBuilderUtil.isSameStructuredTrace is independently tested in GraphBuilderUtilTest.

}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.hypertrace.traceenricher.trace.util;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.nio.ByteBuffer;
import java.util.List;
import org.hypertrace.core.datamodel.Edge;
import org.hypertrace.core.datamodel.Entity;
import org.hypertrace.core.datamodel.Event;
import org.hypertrace.core.datamodel.StructuredTrace;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class GraphBuilderUtilTest {

@Test
public void testSameTraceForNullInputs() {
StructuredTrace cachedTrace = mock(StructuredTrace.class);
StructuredTrace underTestTrace = mock(StructuredTrace.class);

boolean result = GraphBuilderUtil.isSameStructuredTrace(null, null);
Assertions.assertFalse(result);

result = GraphBuilderUtil.isSameStructuredTrace(null, underTestTrace);
Assertions.assertFalse(result);

result = GraphBuilderUtil.isSameStructuredTrace(cachedTrace, null);
Assertions.assertFalse(result);
}

@Test
public void testSameTraceForTenantAndTraceCondition() {
// different tenant id
StructuredTrace cachedTrace = mock(StructuredTrace.class);
when(cachedTrace.getCustomerId()).thenReturn("__defaultTenant");
when(cachedTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));

StructuredTrace underTestTrace = mock(StructuredTrace.class);
when(underTestTrace.getCustomerId()).thenReturn("__defaultTenantUnderTest");
when(underTestTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));

boolean result = GraphBuilderUtil.isSameStructuredTrace(cachedTrace, underTestTrace);
Assertions.assertFalse(result);

// different trace ids
cachedTrace = mock(StructuredTrace.class);
when(cachedTrace.getCustomerId()).thenReturn("__defaultTenant");
when(cachedTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));

underTestTrace = mock(StructuredTrace.class);
when(underTestTrace.getCustomerId()).thenReturn("__defaultTenant");
when(underTestTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428511f".getBytes()));

result = GraphBuilderUtil.isSameStructuredTrace(cachedTrace, underTestTrace);
Assertions.assertFalse(result);
}

@Test
public void testSameTraceForSizeCondition() {
Entity entity = mock(Entity.class);
Event parent = mock(Event.class);
Event child = mock(Event.class);
Edge eventEdge = mock(Edge.class);

// same size
StructuredTrace cachedTrace = mock(StructuredTrace.class);
when(cachedTrace.getCustomerId()).thenReturn("__defaultTenant");
when(cachedTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));
when(cachedTrace.getEntityList()).thenReturn(List.of(entity));
when(cachedTrace.getEventList()).thenReturn(List.of(parent, child));
when(cachedTrace.getEntityEdgeList()).thenReturn(List.of());
when(cachedTrace.getEntityEventEdgeList()).thenReturn(List.of());
when(cachedTrace.getEventEdgeList()).thenReturn(List.of(eventEdge));

StructuredTrace underTestTrace = mock(StructuredTrace.class);
when(underTestTrace.getCustomerId()).thenReturn("__defaultTenant");
when(underTestTrace.getTraceId()).thenReturn(ByteBuffer.wrap("2ebbc19b6428510f".getBytes()));
when(underTestTrace.getEntityList()).thenReturn(List.of(entity));
when(underTestTrace.getEventList()).thenReturn(List.of(parent, child));
when(underTestTrace.getEntityEdgeList()).thenReturn(List.of());
when(underTestTrace.getEntityEventEdgeList()).thenReturn(List.of());
when(underTestTrace.getEventEdgeList()).thenReturn(List.of(eventEdge));

boolean result = GraphBuilderUtil.isSameStructuredTrace(cachedTrace, underTestTrace);
Assertions.assertTrue(result);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test the negative case as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remains are covered with the other two tests, and this I have created as an end to cover up all. But, if you want, I can add one to the list item.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it can be taken up subsequently

}
}
Loading