From f0515625f41f9852d855fca3a1f7f3e96d5a7c0e Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 17 Mar 2026 10:33:43 +0100 Subject: [PATCH 01/47] Rework CollectorApi to accept OTLP data. --- ladybug-backend-jaxrs/pom.xml | 19 +++++++++- .../ladybug/web/jaxrs/api/CollectorApi.java | 37 +++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/ladybug-backend-jaxrs/pom.xml b/ladybug-backend-jaxrs/pom.xml index 359284d97..ecb32cee2 100644 --- a/ladybug-backend-jaxrs/pom.xml +++ b/ladybug-backend-jaxrs/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.wearefrank @@ -16,6 +16,21 @@ Ladybug API protected by JAX-RS authorization + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + + + com.google.protobuf + protobuf-java-util + 4.34.0 + + + com.google.protobuf + protobuf-java + 4.34.0 + org.wearefrank ladybug-frontend-servlet diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java index 408df91f9..3d88c1595 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java @@ -15,13 +15,15 @@ */ package org.wearefrank.ladybug.web.jaxrs.api; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.Setter; -import org.wearefrank.ladybug.Span; import org.springframework.beans.factory.annotation.Autowired; - import org.wearefrank.ladybug.web.common.CollectorApiImpl; import org.wearefrank.ladybug.web.common.Constants; @@ -31,15 +33,26 @@ public class CollectorApi extends ApiBase { private @Setter CollectorApiImpl delegate; @POST - public Response collectSpans(Span[] trace) { - delegate.processSpans(trace); - return Response.ok().build(); - } + @Consumes({"application/x-protobuf", "application/json"}) + public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException { + ExportTraceServiceRequest request; + if (contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType.startsWith("application/json")) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); + } else { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).entity("Unsupported Content-Type: " + contentType).build(); + } + + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { + for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { + delegate.processSpans(scopeSpans.getSpansList()); + } + } - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response collectSpansJson(Span[] trace) { - delegate.processSpans(trace); return Response.ok().build(); } -} +} \ No newline at end of file From a72eb5f2128c830e7e065cce57ec12a726897c18 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 24 Mar 2026 10:18:33 +0100 Subject: [PATCH 02/47] Process OTel spans not ladbug spans. --- ladybug-common/pom.xml | 11 ++++ .../ladybug/web/common/CollectorApiImpl.java | 58 ++++++++++++++----- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index 2f856d252..2dffa6565 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -130,6 +130,17 @@ io.opentelemetry opentelemetry-exporter-otlp + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + compile + + + commons-codec + commons-codec + 1.17.0 + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java index 65a0612d4..6bcc5dbff 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java @@ -1,5 +1,5 @@ /* - Copyright 2025 WeAreFrank! + Copyright 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,16 +15,20 @@ */ package org.wearefrank.ladybug.web.common; +import com.google.protobuf.ByteString; import lombok.Setter; +import org.apache.commons.codec.binary.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.wearefrank.ladybug.Span; +import io.opentelemetry.proto.trace.v1.Span; import org.wearefrank.ladybug.TestTool; import java.lang.invoke.MethodHandles; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; @Component public class CollectorApiImpl { @@ -33,29 +37,53 @@ public class CollectorApiImpl { @Autowired private @Setter TestTool testTool; - public void processSpans(Span[] trace) { + public void processSpans(ArrayList trace) { + System.out.println("CALLED PROCESS SPANS"); ArrayList parentIds = new ArrayList<>(); for (Span span: trace) { - if (span.getParentId() != null && !parentIds.contains(span.getParentId())) { - parentIds.add(span.getParentId()); + String parentId = byteStringToHex(span.getParentSpanId()); + if (!parentId.isEmpty() && !parentIds.contains(parentId)) { + parentIds.add(parentId); } } + ArrayList endpoints = new ArrayList<>(); - for (int i = trace.length - 1; i >= 0; i--) { - if (trace[i].getParentId() == null) { - testTool.startpoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); - endpoints.add(trace[i].getName()); + for (Span span: trace) { + System.out.println(byteStringToHex(span.getTraceId())); + String parentId = byteStringToHex(span.getParentSpanId()); + + if (parentId.isEmpty()) { + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); + endpoints.add(span.getName()); } else { - if (parentIds.contains(trace[i].getId())) { - testTool.startpoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); - endpoints.add(trace[i].getName()); + if (parentIds.contains(parentId)) { + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); + endpoints.add(span.getName()); } else { - testTool.infopoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); + testTool.infopoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); } } } for (int i = endpoints.size() - 1; i >= 0; i--) { - testTool.endpoint(trace[0].getTraceId(), null, endpoints.get(i), "Endpoint"); + testTool.endpoint(byteStringToHex(trace.get(0).getTraceId()), null, endpoints.get(i), "Endpoint"); } } -} + + public HashMap toHashMap(Span span) { + HashMap map = new HashMap<>(); + + span.getAllFields().forEach((descriptor, value) -> { + if (value instanceof ByteString) { + map.put(descriptor.getName(), byteStringToHex((ByteString) value)); + } else { + map.put(descriptor.getName(), value.toString()); + } + }); + + return map; + } + + public String byteStringToHex(ByteString byteString) { + return Hex.encodeHexString(byteString.toByteArray()); + } +} \ No newline at end of file From c2fbf1e15e43c87b1aec690218f1e8dddf8693e8 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 30 Mar 2026 10:14:06 +0200 Subject: [PATCH 03/47] Sort spans in traces and then order traces in tree. --- ladybug-backend-jaxrs/pom.xml | 9 ++ .../ladybug/web/jaxrs/api/CollectorApi.java | 82 ++++++++++++++++++- ladybug-common/pom.xml | 5 ++ .../ladybug/web/common/CollectorApiImpl.java | 5 +- 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/ladybug-backend-jaxrs/pom.xml b/ladybug-backend-jaxrs/pom.xml index ecb32cee2..b6d66e2b2 100644 --- a/ladybug-backend-jaxrs/pom.xml +++ b/ladybug-backend-jaxrs/pom.xml @@ -31,6 +31,10 @@ protobuf-java 4.34.0 + + org.apache.commons + commons-dbcp2 + org.wearefrank ladybug-frontend-servlet @@ -65,5 +69,10 @@ org.projectlombok lombok + + com.github.ben-manes.caffeine + caffeine + 3.2.3 + \ No newline at end of file diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java index 3d88c1595..2c71c956c 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java @@ -20,22 +20,36 @@ import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; +import io.opentelemetry.proto.trace.v1.Span; +import jakarta.annotation.PostConstruct; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; +import org.wearefrank.ladybug.SpanBuffer; import org.wearefrank.ladybug.web.common.CollectorApiImpl; import org.wearefrank.ladybug.web.common.Constants; +import java.util.*; @Path("/" + Constants.LADYBUG_API_PATH + "/collector") public class CollectorApi extends ApiBase { + private SpanBuffer spanBuffer; + @Autowired private @Setter CollectorApiImpl delegate; + @PostConstruct + public void init() { + spanBuffer = new SpanBuffer(delegate); + } + @POST @Consumes({"application/x-protobuf", "application/json"}) - public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException { + public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) + throws InvalidProtocolBufferException { + ExportTraceServiceRequest request; + if (contentType.startsWith("application/x-protobuf")) { request = ExportTraceServiceRequest.parseFrom(data); } else if (contentType.startsWith("application/json")) { @@ -44,15 +58,77 @@ public Response receiveTrace(@HeaderParam("Content-Type") String contentType, by JsonFormat.parser().merge(json, builder); request = builder.build(); } else { - return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).entity("Unsupported Content-Type: " + contentType).build(); + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) + .entity("Unsupported Content-Type: " + contentType) + .build(); } + ArrayList unorderedSpans = new ArrayList<>(); + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { - delegate.processSpans(scopeSpans.getSpansList()); + for (Span span : scopeSpans.getSpansList()) { + unorderedSpans.add(span); + } } } + orderInTraces(unorderedSpans); + return Response.ok().build(); } + + public void orderInTraces(ArrayList unorderedSpans) { + List roots = new ArrayList<>(); + + for (Span span : unorderedSpans) { + if (isRoot(span, unorderedSpans)) { + roots.add(span); + } + } + + roots.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); + + for (Span root : roots) { + ArrayList trace = new ArrayList<>(); + traverse(root, unorderedSpans, trace); + spanBuffer.addTrace(trace); + } + } + + private void traverse(Span parent, ArrayList unorderedSpans, ArrayList trace) { + trace.add(parent); + + String parentId = delegate.byteStringToHex(parent.getSpanId()); + + List children = new ArrayList<>(); + for (Span span : unorderedSpans) { + String childParentId = delegate.byteStringToHex(span.getParentSpanId()); + if (childParentId.equals(parentId)) { + children.add(span); + } + } + + children.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); + + for (Span child : children) { + traverse(child, unorderedSpans, trace); + } + } + + private boolean isRoot(Span span, List unordered) { + String parentId = delegate.byteStringToHex(span.getParentSpanId()); + + if (parentId.isEmpty()) { + return true; + } + + for (Span s : unordered) { + if (delegate.byteStringToHex(s.getSpanId()).equals(parentId)) { + return false; + } + } + + return true; + } } \ No newline at end of file diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index 2dffa6565..f66a9ed3d 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -98,6 +98,11 @@ org.apache.commons commons-dbcp2 + + com.github.ben-manes.caffeine + caffeine + 3.2.3 + jakarta.transaction jakarta.transaction-api diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java index 6bcc5dbff..ab33b8d9d 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java @@ -28,7 +28,6 @@ import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; @Component public class CollectorApiImpl { @@ -38,7 +37,6 @@ public class CollectorApiImpl { private @Setter TestTool testTool; public void processSpans(ArrayList trace) { - System.out.println("CALLED PROCESS SPANS"); ArrayList parentIds = new ArrayList<>(); for (Span span: trace) { String parentId = byteStringToHex(span.getParentSpanId()); @@ -49,7 +47,7 @@ public void processSpans(ArrayList trace) { ArrayList endpoints = new ArrayList<>(); for (Span span: trace) { - System.out.println(byteStringToHex(span.getTraceId())); + String parentId = byteStringToHex(span.getParentSpanId()); if (parentId.isEmpty()) { @@ -58,6 +56,7 @@ public void processSpans(ArrayList trace) { } else { if (parentIds.contains(parentId)) { testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); + testTool.infopoint(byteStringToHex(span.getTraceId()), null, span.getName(), span.getKind()); endpoints.add(span.getName()); } else { testTool.infopoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); From e9794791d2d79ba405095a50fa66906754f26cd2 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 30 Mar 2026 10:14:29 +0200 Subject: [PATCH 04/47] Add caching. --- .../org/wearefrank/ladybug/SpanBuffer.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java new file mode 100644 index 000000000..28c18955b --- /dev/null +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java @@ -0,0 +1,57 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import io.opentelemetry.proto.trace.v1.Span; +import org.springframework.stereotype.Component; +import org.wearefrank.ladybug.web.common.CollectorApiImpl; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +@Component +public class SpanBuffer { + private final Cache> cache; + + private CollectorApiImpl delegate; + + public SpanBuffer(CollectorApiImpl delegate) { + this.delegate = delegate; + this.cache = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .removalListener((String traceId, ArrayList trace, RemovalCause cause) -> { + if (trace != null && cause == RemovalCause.EXPIRED) { + delegate.processSpans(new ArrayList<>(trace)); + } + }) + .build(); + } + + public void addTrace(ArrayList trace) { + String traceId = this.delegate.byteStringToHex(trace.get(0).getTraceId()); + + cache.asMap().compute(traceId, (key, existing) -> { + if (existing == null) { + existing = new ArrayList<>(); + } + existing.addAll(trace); + return existing; + }); + } +} From db5f31c59c6ba249a467ccf4a1f177d52a89aea2 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Thu, 2 Apr 2026 13:25:53 +0200 Subject: [PATCH 05/47] Add class for correct DFS processing. --- .../org/wearefrank/ladybug/SpanGraph.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java new file mode 100644 index 000000000..d8e951b34 --- /dev/null +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java @@ -0,0 +1,54 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug; + +import java.util.*; + +class SpanGraph { + private Map> adjList; + + public SpanGraph(String root) { + adjList = new HashMap<>(); + adjList.put(root, new ArrayList<>()); + } + + public void addEdge(String parent, String child) { + adjList.putIfAbsent(parent, new ArrayList<>()); + adjList.putIfAbsent(parent, new ArrayList<>()); + adjList.get(parent).add(child); + adjList.get(parent).add(child); + } + + public void dfs(String root) { + Set visited = new HashSet<>(); + dfsRecursive(root, visited); + } + + private void dfsRecursive(String node, Set visited) { + System.out.println("startpoint for: " + node); + visited.add(node); + + if (adjList.containsKey(node)) { + for (String neighbor : adjList.get(node)) { + if (!visited.contains(neighbor)) { + dfsRecursive(neighbor, visited); + } + } + } + + System.out.println("3 endpoint for: " + node); + } +} \ No newline at end of file From 20aa65986b0949678eb96e49452f71f147f8ef08 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Thu, 2 Apr 2026 14:28:32 +0200 Subject: [PATCH 06/47] Remove sorting before expiration from cache. --- .../ladybug/web/jaxrs/api/CollectorApi.java | 61 +------------------ .../org/wearefrank/ladybug/SpanBuffer.java | 23 +++---- 2 files changed, 13 insertions(+), 71 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java index 2c71c956c..8a761a64d 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java @@ -29,7 +29,6 @@ import org.wearefrank.ladybug.SpanBuffer; import org.wearefrank.ladybug.web.common.CollectorApiImpl; import org.wearefrank.ladybug.web.common.Constants; -import java.util.*; @Path("/" + Constants.LADYBUG_API_PATH + "/collector") public class CollectorApi extends ApiBase { @@ -63,72 +62,14 @@ public Response receiveTrace(@HeaderParam("Content-Type") String contentType, by .build(); } - ArrayList unorderedSpans = new ArrayList<>(); - for (ResourceSpans resourceSpans : request.getResourceSpansList()) { for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { for (Span span : scopeSpans.getSpansList()) { - unorderedSpans.add(span); + spanBuffer.addSpan(span); } } } - orderInTraces(unorderedSpans); - return Response.ok().build(); } - - public void orderInTraces(ArrayList unorderedSpans) { - List roots = new ArrayList<>(); - - for (Span span : unorderedSpans) { - if (isRoot(span, unorderedSpans)) { - roots.add(span); - } - } - - roots.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); - - for (Span root : roots) { - ArrayList trace = new ArrayList<>(); - traverse(root, unorderedSpans, trace); - spanBuffer.addTrace(trace); - } - } - - private void traverse(Span parent, ArrayList unorderedSpans, ArrayList trace) { - trace.add(parent); - - String parentId = delegate.byteStringToHex(parent.getSpanId()); - - List children = new ArrayList<>(); - for (Span span : unorderedSpans) { - String childParentId = delegate.byteStringToHex(span.getParentSpanId()); - if (childParentId.equals(parentId)) { - children.add(span); - } - } - - children.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); - - for (Span child : children) { - traverse(child, unorderedSpans, trace); - } - } - - private boolean isRoot(Span span, List unordered) { - String parentId = delegate.byteStringToHex(span.getParentSpanId()); - - if (parentId.isEmpty()) { - return true; - } - - for (Span s : unordered) { - if (delegate.byteStringToHex(s.getSpanId()).equals(parentId)) { - return false; - } - } - - return true; - } } \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java index 28c18955b..0de0d8c9e 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java @@ -34,24 +34,25 @@ public class SpanBuffer { public SpanBuffer(CollectorApiImpl delegate) { this.delegate = delegate; this.cache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .removalListener((String traceId, ArrayList trace, RemovalCause cause) -> { - if (trace != null && cause == RemovalCause.EXPIRED) { - delegate.processSpans(new ArrayList<>(trace)); + .expireAfterWrite(1, TimeUnit.SECONDS) + .removalListener((String traceId, ArrayList spans, RemovalCause cause) -> { + if (spans != null && cause == RemovalCause.EXPIRED) { + ArrayList spansCopy = new ArrayList<>(spans); + delegate.processSpans(spansCopy); } }) .build(); } - public void addTrace(ArrayList trace) { - String traceId = this.delegate.byteStringToHex(trace.get(0).getTraceId()); + public void addSpan(Span span) { + String traceId = this.delegate.byteStringToHex(span.getTraceId()); cache.asMap().compute(traceId, (key, existing) -> { - if (existing == null) { - existing = new ArrayList<>(); - } - existing.addAll(trace); - return existing; + ArrayList updated = + existing == null ? new ArrayList<>() : new ArrayList<>(existing); + + updated.add(span); + return updated; }); } } From e5a868a95fcbb8337c917610c7ed09cf1a7594c0 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Thu, 2 Apr 2026 14:29:19 +0200 Subject: [PATCH 07/47] New class for DFS creation of checkpoints. --- .../org/wearefrank/ladybug/SpanGraph.java | 55 ++++++++++----- .../ladybug/web/common/CollectorApiImpl.java | 69 ++++++++----------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java index d8e951b34..0eab7f8e4 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java @@ -15,40 +15,61 @@ */ package org.wearefrank.ladybug; +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.trace.v1.Span; +import org.apache.commons.codec.binary.Hex; + import java.util.*; -class SpanGraph { - private Map> adjList; +public class SpanGraph { + private Map> spans; + TestTool testTool; - public SpanGraph(String root) { - adjList = new HashMap<>(); - adjList.put(root, new ArrayList<>()); + public SpanGraph(TestTool testTool) { + spans = new HashMap<>(); + this.testTool = testTool; } - public void addEdge(String parent, String child) { - adjList.putIfAbsent(parent, new ArrayList<>()); - adjList.putIfAbsent(parent, new ArrayList<>()); - adjList.get(parent).add(child); - adjList.get(parent).add(child); + public void addEdge(String parent, Span child) { + spans.putIfAbsent(parent, new ArrayList<>()); + spans.get(parent).add(child); } - public void dfs(String root) { - Set visited = new HashSet<>(); + public void dfs(Span root) { + Set visited = new HashSet<>(); dfsRecursive(root, visited); } - private void dfsRecursive(String node, Set visited) { - System.out.println("startpoint for: " + node); + private void dfsRecursive(Span node, Set visited) { + testTool.startpoint(byteStringToHex(node.getTraceId()), null, node.getName(), toHashMap(node).toString()); visited.add(node); - if (adjList.containsKey(node)) { - for (String neighbor : adjList.get(node)) { + if (spans.containsKey(byteStringToHex(node.getSpanId()))) { + for (Span neighbor : spans.get(byteStringToHex(node.getSpanId()))) { if (!visited.contains(neighbor)) { dfsRecursive(neighbor, visited); } } } - System.out.println("3 endpoint for: " + node); + testTool.endpoint(byteStringToHex(node.getTraceId()), null, node.getName(), "Endpoint"); + } + + public String byteStringToHex(ByteString byteString) { + return Hex.encodeHexString(byteString.toByteArray()); + } + + public HashMap toHashMap(Span span) { + HashMap map = new HashMap<>(); + + span.getAllFields().forEach((descriptor, value) -> { + if (value instanceof ByteString) { + map.put(descriptor.getName(), byteStringToHex((ByteString) value)); + } else { + map.put(descriptor.getName(), value.toString()); + } + }); + + return map; } } \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java index ab33b8d9d..f91a69c76 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java @@ -23,11 +23,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import io.opentelemetry.proto.trace.v1.Span; +import org.wearefrank.ladybug.SpanGraph; import org.wearefrank.ladybug.TestTool; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; @Component public class CollectorApiImpl { @@ -36,53 +36,44 @@ public class CollectorApiImpl { @Autowired private @Setter TestTool testTool; - public void processSpans(ArrayList trace) { - ArrayList parentIds = new ArrayList<>(); - for (Span span: trace) { - String parentId = byteStringToHex(span.getParentSpanId()); - if (!parentId.isEmpty() && !parentIds.contains(parentId)) { - parentIds.add(parentId); + public void processSpans(ArrayList spans) { + SpanGraph spanGraph = new SpanGraph(testTool); + + Span rootSpan = findRoot(spans); + + if (rootSpan != null) { + for (Span span: spans) { + spanGraph.addEdge(byteStringToHex(span.getParentSpanId()), span); } + spanGraph.dfs(rootSpan); } + } + + public String byteStringToHex(ByteString byteString) { + return Hex.encodeHexString(byteString.toByteArray()); + } - ArrayList endpoints = new ArrayList<>(); - for (Span span: trace) { + private Span findRoot(ArrayList unorderedTrace) { + ArrayList spanIds = new ArrayList<>(); - String parentId = byteStringToHex(span.getParentSpanId()); + for (Span span : unorderedTrace) { + spanIds.add(byteStringToHex(span.getSpanId())); + } + for (Span span : unorderedTrace) { + String parentId = byteStringToHex(span.getParentSpanId()); if (parentId.isEmpty()) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); - endpoints.add(span.getName()); - } else { - if (parentIds.contains(parentId)) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); - testTool.infopoint(byteStringToHex(span.getTraceId()), null, span.getName(), span.getKind()); - endpoints.add(span.getName()); - } else { - testTool.infopoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span).toString()); - } + return span; } } - for (int i = endpoints.size() - 1; i >= 0; i--) { - testTool.endpoint(byteStringToHex(trace.get(0).getTraceId()), null, endpoints.get(i), "Endpoint"); - } - } - public HashMap toHashMap(Span span) { - HashMap map = new HashMap<>(); - - span.getAllFields().forEach((descriptor, value) -> { - if (value instanceof ByteString) { - map.put(descriptor.getName(), byteStringToHex((ByteString) value)); - } else { - map.put(descriptor.getName(), value.toString()); + for (Span span : unorderedTrace) { + String parentId = byteStringToHex(span.getParentSpanId()); + if (!spanIds.contains(parentId)) { + return span; } - }); - - return map; - } + } - public String byteStringToHex(ByteString byteString) { - return Hex.encodeHexString(byteString.toByteArray()); + return null; } } \ No newline at end of file From d56ed61aed5cf1961e089b2dde37334651577b12 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Fri, 10 Apr 2026 14:16:49 +0200 Subject: [PATCH 08/47] Orphan span handling + refactoring. --- .../org/wearefrank/ladybug/SpanBuffer.java | 4 +-- .../{SpanGraph.java => TraceTree.java} | 30 +++++++++++-------- .../ladybug/web/common/CollectorApiImpl.java | 28 ++++++++++++++--- 3 files changed, 44 insertions(+), 18 deletions(-) rename ladybug-common/src/main/java/org/wearefrank/ladybug/{SpanGraph.java => TraceTree.java} (64%) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java index 0de0d8c9e..1b13fd210 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java @@ -34,7 +34,7 @@ public class SpanBuffer { public SpanBuffer(CollectorApiImpl delegate) { this.delegate = delegate; this.cache = Caffeine.newBuilder() - .expireAfterWrite(1, TimeUnit.SECONDS) + .expireAfterWrite(30, TimeUnit.SECONDS) .removalListener((String traceId, ArrayList spans, RemovalCause cause) -> { if (spans != null && cause == RemovalCause.EXPIRED) { ArrayList spansCopy = new ArrayList<>(spans); @@ -55,4 +55,4 @@ public void addSpan(Span span) { return updated; }); } -} +} \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java similarity index 64% rename from ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java rename to ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java index 0eab7f8e4..d515f2b9b 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanGraph.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java @@ -16,16 +16,17 @@ package org.wearefrank.ladybug; import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.Span; import org.apache.commons.codec.binary.Hex; import java.util.*; -public class SpanGraph { - private Map> spans; +public class TraceTree { + private HashMap> spans; TestTool testTool; - public SpanGraph(TestTool testTool) { + public TraceTree(TestTool testTool) { spans = new HashMap<>(); this.testTool = testTool; } @@ -36,23 +37,28 @@ public void addEdge(String parent, Span child) { } public void dfs(Span root) { - Set visited = new HashSet<>(); + HashSet visited = new HashSet<>(); dfsRecursive(root, visited); } - private void dfsRecursive(Span node, Set visited) { - testTool.startpoint(byteStringToHex(node.getTraceId()), null, node.getName(), toHashMap(node).toString()); - visited.add(node); + private void dfsRecursive(Span span, HashSet visited) { + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span)); - if (spans.containsKey(byteStringToHex(node.getSpanId()))) { - for (Span neighbor : spans.get(byteStringToHex(node.getSpanId()))) { - if (!visited.contains(neighbor)) { - dfsRecursive(neighbor, visited); + for (KeyValue keyValue : span.getAttributesList()) { + testTool.infopoint(byteStringToHex(span.getTraceId()), null, keyValue.getKey(), keyValue.getValue()); + } + + visited.add(span); + + if (spans.containsKey(byteStringToHex(span.getSpanId()))) { + for (Span sibling : spans.get(byteStringToHex(span.getSpanId()))) { + if (!visited.contains(sibling)) { + dfsRecursive(sibling, visited); } } } - testTool.endpoint(byteStringToHex(node.getTraceId()), null, node.getName(), "Endpoint"); + testTool.endpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "Endpoint"); } public String byteStringToHex(ByteString byteString) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java index f91a69c76..bcddc5867 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java @@ -23,7 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import io.opentelemetry.proto.trace.v1.Span; -import org.wearefrank.ladybug.SpanGraph; +import org.wearefrank.ladybug.TraceTree; import org.wearefrank.ladybug.TestTool; import java.lang.invoke.MethodHandles; @@ -37,15 +37,35 @@ public class CollectorApiImpl { private @Setter TestTool testTool; public void processSpans(ArrayList spans) { - SpanGraph spanGraph = new SpanGraph(testTool); + TraceTree traceTree = new TraceTree(testTool); + ArrayList spanIds = new ArrayList<>(); + + for (Span span : spans) { + spanIds.add(byteStringToHex(span.getSpanId())); + } Span rootSpan = findRoot(spans); if (rootSpan != null) { for (Span span: spans) { - spanGraph.addEdge(byteStringToHex(span.getParentSpanId()), span); + String parentId = byteStringToHex(span.getParentSpanId()); + + if (!parentId.isEmpty() && spanIds.contains(parentId)) { + traceTree.addEdge(parentId, span); + } else { + ArrayList timeSortedSpans = new ArrayList<>(spans); + timeSortedSpans.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); + + for (int i = 0; i < timeSortedSpans.size(); i++) { + if (byteStringToHex(span.getSpanId()).equals(byteStringToHex(timeSortedSpans.get(i).getSpanId()))) { + if (i > 0) { + traceTree.addEdge(byteStringToHex(timeSortedSpans.get(i - 1).getSpanId()), span); + } + } + } + } } - spanGraph.dfs(rootSpan); + traceTree.dfs(rootSpan); } } From 4bb7c67b1c36d01be50ce54097c673f5d16e6bb7 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Fri, 10 Apr 2026 15:28:43 +0200 Subject: [PATCH 09/47] Only actual value in infopoint. --- .../src/main/java/org/wearefrank/ladybug/TraceTree.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java index d515f2b9b..504ba78e6 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java @@ -16,11 +16,16 @@ package org.wearefrank.ladybug; import com.google.protobuf.ByteString; +import com.google.protobuf.Parser; +import com.google.protobuf.TextFormat; +import io.opentelemetry.proto.common.v1.AnyValue; import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.Span; import org.apache.commons.codec.binary.Hex; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TraceTree { private HashMap> spans; @@ -45,7 +50,9 @@ private void dfsRecursive(Span span, HashSet visited) { testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span)); for (KeyValue keyValue : span.getAttributesList()) { - testTool.infopoint(byteStringToHex(span.getTraceId()), null, keyValue.getKey(), keyValue.getValue()); + AnyValue anyValue = keyValue.getValue(); + String value = String.valueOf(anyValue.getField(anyValue.getDescriptorForType().findFieldByNumber(anyValue.getValueCase().getNumber()))); + testTool.infopoint(byteStringToHex(span.getTraceId()), null, keyValue.getKey(), value); } visited.add(span); From 6c7a61123694d06a7999ab88c233c56041d1373d Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Fri, 10 Apr 2026 15:44:05 +0200 Subject: [PATCH 10/47] Added unit tests for TraceTree.java. --- ladybug-common/pom.xml | 12 +++ .../org/wearefrank/ladybug/TraceTree.java | 4 - .../ladybug/test/junit/TestTraceTree.java | 94 +++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index f66a9ed3d..fafe75153 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -146,6 +146,18 @@ commons-codec 1.17.0 + + org.mockito + mockito-core + 5.11.0 + test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java index 504ba78e6..abb314527 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java @@ -16,16 +16,12 @@ package org.wearefrank.ladybug; import com.google.protobuf.ByteString; -import com.google.protobuf.Parser; -import com.google.protobuf.TextFormat; import io.opentelemetry.proto.common.v1.AnyValue; import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.Span; import org.apache.commons.codec.binary.Hex; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class TraceTree { private HashMap> spans; diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java new file mode 100644 index 000000000..043570850 --- /dev/null +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java @@ -0,0 +1,94 @@ +package org.wearefrank.ladybug.test.junit; + +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.Span; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.wearefrank.ladybug.TestTool; +import org.wearefrank.ladybug.TraceTree; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class TestTraceTree { + private TestTool testTool; + private TraceTree traceTree; + + @BeforeEach + void setUp() { + testTool = Mockito.mock(TestTool.class); + traceTree = new TraceTree(testTool); + } + + @Test + void testDfsOnlyRoot() { + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("root") + .addAttributes(KeyValue.newBuilder() + .setKey("http.method") + .setValue(AnyValue.newBuilder().setStringValue("GET").build()) + .build()) + .build(); + + traceTree.dfs(span); + + verify(testTool).startpoint(anyString(), isNull(), eq("root"), any()); + verify(testTool).infopoint(anyString(), isNull(), eq("http.method"), anyString()); + verify(testTool).endpoint(anyString(), isNull(), eq("root"), eq("Endpoint")); + } + + @Test + void testDfsWithChildSpan() { + Span parent = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("parent")) + .setName("parent") + .build(); + + Span child = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("child")) + .setParentSpanId(ByteString.copyFromUtf8("parent")) + .setName("child") + .build(); + + traceTree.addEdge(traceTree.byteStringToHex(parent.getSpanId()), child); + + traceTree.dfs(parent); + + verify(testTool).startpoint(anyString(), isNull(), eq("parent"), any()); + verify(testTool).startpoint(anyString(), isNull(), eq("child"), any()); + verify(testTool).endpoint(anyString(), isNull(), eq("child"), eq("Endpoint")); + verify(testTool).endpoint(anyString(), isNull(), eq("parent"), eq("Endpoint")); + } + + @Test + void testByteStringToHex() { + ByteString bytes = ByteString.copyFromUtf8("abc"); + String hex = traceTree.byteStringToHex(bytes); + + assert hex != null; + assert !hex.isEmpty(); + assert hex.equals("616263"); + } + + @Test + void testToHashMap() { + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("testSpan") + .build(); + + var map = traceTree.toHashMap(span); + + assert map.containsKey("trace_id"); + assert map.containsKey("span_id"); + assert map.containsKey("name"); + } +} \ No newline at end of file From e3d7ae8e4391cf67eddebab348095f02971248ae Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 14 Apr 2026 14:28:03 +0200 Subject: [PATCH 11/47] Fix. --- .../src/main/java/org/wearefrank/ladybug/TraceTree.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java index abb314527..99ab61acf 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java @@ -43,7 +43,7 @@ public void dfs(Span root) { } private void dfsRecursive(Span span, HashSet visited) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), toHashMap(span)); + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), String.valueOf(toHashMap(span))); for (KeyValue keyValue : span.getAttributesList()) { AnyValue anyValue = keyValue.getValue(); From 49836c6ebc92056361ea512a677b431c6c3f394f Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 4 May 2026 13:55:42 +0200 Subject: [PATCH 12/47] Quick fix for OTel export. --- .../wearefrank/ladybug/web/springmvc/api/CollectorApi.java | 6 +++--- .../src/main/java/org/wearefrank/ladybug/TestTool.java | 6 +++++- .../java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java | 4 ++-- .../src/main/resources/springTestToolTestWebapp.xml | 5 +++++ 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java index a2a252f77..a8b005d96 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java @@ -1,5 +1,5 @@ /* - Copyright 2025 WeAreFrank! + Copyright 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,13 +36,13 @@ public class CollectorApi { @PostMapping public ResponseEntity collectSpans(Span[] trace) { - delegate.processSpans(trace); + //delegate.processSpans(trace); return ResponseEntity.ok().build(); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity collectSpansJson(Span[] trace) { - delegate.processSpans(trace); + //delegate.processSpans(trace); return ResponseEntity.ok().build(); } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 444eedadf..7276f857d 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -102,7 +102,7 @@ public class TestTool { private @Setter @Getter @Inject @Autowired Views views; private @Setter @Getter int reportsInProgressThreshold = 300000; boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() - private @Qualifier("openTelemetryEndpoint") String openTelemetryEndpoint; + private String openTelemetryEndpoint; private Tracer tracer; @PostConstruct @@ -125,6 +125,10 @@ public Logger getSecurityLog() { return securityLog; } + public void setOpenTelemetryEndpoint(String openTelemetryEndpoint) { + this.openTelemetryEndpoint = openTelemetryEndpoint; + } + public void setConfigName(String configName) { this.configName = configName; } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java index 5a02b1ff9..2aef74b61 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java @@ -1,5 +1,5 @@ /* - Copyright 2024, 2025 WeAreFrank! + Copyright 2024, 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ public static Tracer getOpenTelemetryTracer(String openTelemetryEndpoint) { ContextPropagators.create( TextMapPropagator.composite( W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) - .buildAndRegisterGlobal(); + .build(); return openTelemetry.getTracer(Report.class.getName(), "0.1.0"); } return null; diff --git a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml index 5aaf203fa..30a7e9d64 100644 --- a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml +++ b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml @@ -32,8 +32,13 @@ + + + + + From 8b2767b73f54d19a9b6eab75242b5cb2bd0b36fb Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 5 May 2026 12:53:04 +0200 Subject: [PATCH 13/47] Update reports. --- .../ladybug/web/jaxrs/api/CollectorApi.java | 45 ++++- .../java/org/wearefrank/ladybug/TestTool.java | 11 +- .../org/wearefrank/ladybug/TraceTree.java | 2 +- .../wearefrank/ladybug/storage/Storage.java | 2 + .../ladybug/storage/file/Storage.java | 10 + .../ProofOfMigrationErrorsView.java | 4 +- .../ProofOfMigrationView.java | 4 +- .../ladybug/test/junit/TestTraceTree.java | 188 +++++++++--------- .../resources/springTestToolTestWebapp.xml | 2 +- ladybug-test-webapp/src/main/webapp/index.jsp | 7 +- 10 files changed, 164 insertions(+), 111 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java index 8a761a64d..f1e8911d7 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java @@ -22,14 +22,22 @@ import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; import jakarta.ws.rs.*; +import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; -import org.wearefrank.ladybug.SpanBuffer; +import org.wearefrank.ladybug.*; +import org.wearefrank.ladybug.storage.CrudStorage; +import org.wearefrank.ladybug.storage.StorageException; import org.wearefrank.ladybug.web.common.CollectorApiImpl; import org.wearefrank.ladybug.web.common.Constants; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + @Path("/" + Constants.LADYBUG_API_PATH + "/collector") public class CollectorApi extends ApiBase { private SpanBuffer spanBuffer; @@ -37,6 +45,9 @@ public class CollectorApi extends ApiBase { @Autowired private @Setter CollectorApiImpl delegate; + private @Setter @Inject + @Autowired TestTool testTool; + @PostConstruct public void init() { spanBuffer = new SpanBuffer(delegate); @@ -44,9 +55,7 @@ public void init() { @POST @Consumes({"application/x-protobuf", "application/json"}) - public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) - throws InvalidProtocolBufferException { - + public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException, StorageException { ExportTraceServiceRequest request; if (contentType.startsWith("application/x-protobuf")) { @@ -66,6 +75,34 @@ public Response receiveTrace(@HeaderParam("Content-Type") String contentType, by for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { for (Span span : scopeSpans.getSpansList()) { spanBuffer.addSpan(span); + + CrudStorage storage = (CrudStorage) testTool.getDebugStorage(); + + for (Integer storageId : storage.getStorageIds()) { + if (storage.getReport(storageId).getCorrelationId().equals(delegate.byteStringToHex(span.getTraceId()))) { + Report report = storage.getReport(storageId); + List checkpoints = report.getCheckpoints(); + + List newList = new ArrayList<>(); + + for (int i = 0; i < checkpoints.size(); i++) { + Checkpoint checkpoint = checkpoints.get(i); + newList.add(checkpoint); + + if (Objects.equals(checkpoint.getMessage(), delegate.byteStringToHex(span.getParentSpanId()))) { + Checkpoint start = new Checkpoint(report, checkpoints.get(i).getThreadName(), null, span.getName(), CheckpointType.STARTPOINT.toInt(), checkpoints.get(i).getLevel() + 1); + Checkpoint end = new Checkpoint(report, checkpoints.get(i).getThreadName(), null, span.getName(), CheckpointType.ENDPOINT.toInt(), checkpoints.get(i).getLevel() + 1); + + newList.add(start); + newList.add(end); + } + } + + report.setCheckpoints(newList); + + storage.update(report); + } + } } } } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 7276f857d..5d684e4fc 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -70,7 +70,7 @@ public class TestTool { private Map reportsInProgressByCorrelationId = new HashMap(); private long numberOfReportsInProgress = 0; private Map originalReports = new HashMap(); - private @Setter @Getter @Inject @Autowired LogStorage debugStorage; + private @Setter @Getter @Inject @Autowired Storage debugStorage; private @Setter @Getter @Inject @Autowired CrudStorage testStorage; private MessageEncoder messageEncoder = new MessageEncoderImpl(); private MessageCapturer messageCapturer = new MessageCapturerImpl(); @@ -335,7 +335,7 @@ public Tracer getOpenTelemetryTracer() { return tracer; } - private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { return checkpoint(correlationId, childThreadId, sourceClassName, name, @@ -476,7 +476,12 @@ protected void closeReportIfFinished(Report report) { numberOfReportsInProgress--; } if (report.isReportFilterMatching()) { - debugStorage.storeWithoutException(report); + try { + debugStorage.store(report); + } catch (StorageException e) { + + } + } } } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java index 99ab61acf..734b52aa4 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java @@ -43,7 +43,7 @@ public void dfs(Span root) { } private void dfsRecursive(Span span, HashSet visited) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), String.valueOf(toHashMap(span))); + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), String.valueOf(byteStringToHex(span.getSpanId()))); for (KeyValue keyValue : span.getAttributesList()) { AnyValue anyValue = keyValue.getValue(); diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java index 5240d2d67..474f7a609 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java @@ -64,6 +64,8 @@ public List> getMetadata(int maxNumberOfRecords, List metad public void close(); + public void store(Report report) throws StorageException; + public int getFilterType(String column); public List getFilterValues(String column) throws StorageException; diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java index 6d12ec34b..a55c58e49 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java @@ -172,6 +172,11 @@ public void close() { writer.close(); } + @Override + public void store(Report report) throws StorageException { + storeWithoutException(report); + } + @Override public int getFilterType(String column) { return FILTER_RESET; @@ -186,4 +191,9 @@ public List getFilterValues(String column) throws StorageException { public String getUserHelp(String column) { return SearchUtil.getUserHelp(); } + + @Override + public boolean isCrudStorage() { + return LogStorage.super.isCrudStorage(); + } } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java index 5bda89349..dbe4fce3b 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java @@ -22,7 +22,7 @@ import jakarta.inject.Inject; import lombok.Setter; import org.wearefrank.ladybug.filter.View; -import org.wearefrank.ladybug.storage.LogStorage; +import org.wearefrank.ladybug.storage.Storage; //@Dependent disabled for Quarkus for now because of the use of JdbcTemplate public class ProofOfMigrationErrorsView extends View { @@ -38,7 +38,7 @@ public String getName() { } @Override - public LogStorage getDebugStorage() { + public Storage getDebugStorage() { return proofOfMigrationErrorsStorage; } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java index 0b022157b..1e239aff8 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java @@ -22,7 +22,7 @@ import jakarta.inject.Inject; import lombok.Setter; import org.wearefrank.ladybug.filter.View; -import org.wearefrank.ladybug.storage.LogStorage; +import org.wearefrank.ladybug.storage.Storage; // @Dependent disabled for Quarkus for now because of the use of JdbcTemplate public class ProofOfMigrationView extends View { @@ -38,7 +38,7 @@ public String getName() { } @Override - public LogStorage getDebugStorage() { + public Storage getDebugStorage() { return proofOfMigrationStorage; } diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java index 043570850..a356ee9f1 100644 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java @@ -1,94 +1,94 @@ -package org.wearefrank.ladybug.test.junit; - -import com.google.protobuf.ByteString; -import io.opentelemetry.proto.common.v1.AnyValue; -import io.opentelemetry.proto.common.v1.KeyValue; -import io.opentelemetry.proto.trace.v1.Span; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import org.wearefrank.ladybug.TestTool; -import org.wearefrank.ladybug.TraceTree; - -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -class TestTraceTree { - private TestTool testTool; - private TraceTree traceTree; - - @BeforeEach - void setUp() { - testTool = Mockito.mock(TestTool.class); - traceTree = new TraceTree(testTool); - } - - @Test - void testDfsOnlyRoot() { - Span span = Span.newBuilder() - .setTraceId(ByteString.copyFromUtf8("trace1")) - .setSpanId(ByteString.copyFromUtf8("span1")) - .setName("root") - .addAttributes(KeyValue.newBuilder() - .setKey("http.method") - .setValue(AnyValue.newBuilder().setStringValue("GET").build()) - .build()) - .build(); - - traceTree.dfs(span); - - verify(testTool).startpoint(anyString(), isNull(), eq("root"), any()); - verify(testTool).infopoint(anyString(), isNull(), eq("http.method"), anyString()); - verify(testTool).endpoint(anyString(), isNull(), eq("root"), eq("Endpoint")); - } - - @Test - void testDfsWithChildSpan() { - Span parent = Span.newBuilder() - .setTraceId(ByteString.copyFromUtf8("trace1")) - .setSpanId(ByteString.copyFromUtf8("parent")) - .setName("parent") - .build(); - - Span child = Span.newBuilder() - .setTraceId(ByteString.copyFromUtf8("trace1")) - .setSpanId(ByteString.copyFromUtf8("child")) - .setParentSpanId(ByteString.copyFromUtf8("parent")) - .setName("child") - .build(); - - traceTree.addEdge(traceTree.byteStringToHex(parent.getSpanId()), child); - - traceTree.dfs(parent); - - verify(testTool).startpoint(anyString(), isNull(), eq("parent"), any()); - verify(testTool).startpoint(anyString(), isNull(), eq("child"), any()); - verify(testTool).endpoint(anyString(), isNull(), eq("child"), eq("Endpoint")); - verify(testTool).endpoint(anyString(), isNull(), eq("parent"), eq("Endpoint")); - } - - @Test - void testByteStringToHex() { - ByteString bytes = ByteString.copyFromUtf8("abc"); - String hex = traceTree.byteStringToHex(bytes); - - assert hex != null; - assert !hex.isEmpty(); - assert hex.equals("616263"); - } - - @Test - void testToHashMap() { - Span span = Span.newBuilder() - .setTraceId(ByteString.copyFromUtf8("trace1")) - .setSpanId(ByteString.copyFromUtf8("span1")) - .setName("testSpan") - .build(); - - var map = traceTree.toHashMap(span); - - assert map.containsKey("trace_id"); - assert map.containsKey("span_id"); - assert map.containsKey("name"); - } -} \ No newline at end of file +//package org.wearefrank.ladybug.test.junit; +// +//import com.google.protobuf.ByteString; +//import io.opentelemetry.proto.common.v1.AnyValue; +//import io.opentelemetry.proto.common.v1.KeyValue; +//import io.opentelemetry.proto.trace.v1.Span; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.Test; +//import org.mockito.Mockito; +//import org.wearefrank.ladybug.TestTool; +//import org.wearefrank.ladybug.TraceTree; +// +//import static org.mockito.ArgumentMatchers.*; +//import static org.mockito.Mockito.*; +// +//class TestTraceTree { +// private TestTool testTool; +// private TraceTree traceTree; +// +// @BeforeEach +// void setUp() { +// testTool = Mockito.mock(TestTool.class); +// traceTree = new TraceTree(testTool); +// } +// +// @Test +// void testDfsOnlyRoot() { +// Span span = Span.newBuilder() +// .setTraceId(ByteString.copyFromUtf8("trace1")) +// .setSpanId(ByteString.copyFromUtf8("span1")) +// .setName("root") +// .addAttributes(KeyValue.newBuilder() +// .setKey("http.method") +// .setValue(AnyValue.newBuilder().setStringValue("GET").build()) +// .build()) +// .build(); +// +// traceTree.dfs(span); +// +// verify(testTool).startpoint(anyString(), isNull(), eq("root"), any()); +// verify(testTool).infopoint(anyString(), isNull(), eq("http.method"), anyString()); +// verify(testTool).endpoint(anyString(), isNull(), eq("root"), eq("Endpoint")); +// } +// +// @Test +// void testDfsWithChildSpan() { +// Span parent = Span.newBuilder() +// .setTraceId(ByteString.copyFromUtf8("trace1")) +// .setSpanId(ByteString.copyFromUtf8("parent")) +// .setName("parent") +// .build(); +// +// Span child = Span.newBuilder() +// .setTraceId(ByteString.copyFromUtf8("trace1")) +// .setSpanId(ByteString.copyFromUtf8("child")) +// .setParentSpanId(ByteString.copyFromUtf8("parent")) +// .setName("child") +// .build(); +// +// traceTree.addEdge(traceTree.byteStringToHex(parent.getSpanId()), child); +// +// traceTree.dfs(parent); +// +// verify(testTool).startpoint(anyString(), isNull(), eq("parent"), any()); +// verify(testTool).startpoint(anyString(), isNull(), eq("child"), any()); +// verify(testTool).endpoint(anyString(), isNull(), eq("child"), eq("Endpoint")); +// verify(testTool).endpoint(anyString(), isNull(), eq("parent"), eq("Endpoint")); +// } +// +// @Test +// void testByteStringToHex() { +// ByteString bytes = ByteString.copyFromUtf8("abc"); +// String hex = traceTree.byteStringToHex(bytes); +// +// assert hex != null; +// assert !hex.isEmpty(); +// assert hex.equals("616263"); +// } +// +// @Test +// void testToHashMap() { +// Span span = Span.newBuilder() +// .setTraceId(ByteString.copyFromUtf8("trace1")) +// .setSpanId(ByteString.copyFromUtf8("span1")) +// .setName("testSpan") +// .build(); +// +// var map = traceTree.toHashMap(span); +// +// assert map.containsKey("trace_id"); +// assert map.containsKey("span_id"); +// assert map.containsKey("name"); +// } +//} \ No newline at end of file diff --git a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml index 30a7e9d64..5c924de8c 100644 --- a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml +++ b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml @@ -276,7 +276,7 @@ diff --git a/ladybug-test-webapp/src/main/webapp/index.jsp b/ladybug-test-webapp/src/main/webapp/index.jsp index e9268c697..47959b886 100644 --- a/ladybug-test-webapp/src/main/webapp/index.jsp +++ b/ladybug-test-webapp/src/main/webapp/index.jsp @@ -2,7 +2,6 @@ <%@ page import="org.wearefrank.ladybug.TestTool"%> <%@ page import="org.wearefrank.ladybug.MessageEncoderImpl"%> <%@ page import="org.wearefrank.ladybug.storage.CrudStorage"%> -<%@ page import="org.wearefrank.ladybug.storage.LogStorage"%> <%@ page import="org.wearefrank.ladybug.storage.Storage"%> <%@ page import="org.wearefrank.ladybug.test.webapp.test.webapp.ComplexReports"%> <%@ page import="org.springframework.web.context.WebApplicationContext"%> @@ -164,7 +163,7 @@ } // Other actions if ("true".equals(request.getParameter("clearDebugStorage"))) { - LogStorage debugStorage = (LogStorage)webApplicationContext.getBean("debugStorage"); + Storage debugStorage = (Storage)webApplicationContext.getBean("debugStorage"); debugStorage.clear(); } if ("true".equals(request.getParameter("clearDatabaseStorage"))) { @@ -172,10 +171,10 @@ databaseStorage.clear(); } if (request.getParameter("changeDebugStorage") != null) { - testTool.setDebugStorage((LogStorage)testTool.getStorage(request.getParameter("changeDebugStorage"))); + testTool.setDebugStorage((Storage)testTool.getStorage(request.getParameter("changeDebugStorage"))); } if (request.getParameter("resetDebugStorage") != null) { - testTool.setDebugStorage((LogStorage) webApplicationContext.getBean("debugStorage")); + testTool.setDebugStorage((Storage) webApplicationContext.getBean("debugStorage")); } if (request.getParameter("removeReportsInProgress") != null) { while (testTool.getNumberOfReportsInProgress() > 0) { From cef18da8c203aff30ce84e83d41233cece80a88a Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 18 May 2026 17:04:11 +0200 Subject: [PATCH 14/47] Refactor API's and the impl. --- .../web/jaxrs/ApiAuthorizationFilter.java | 2 +- .../ladybug/web/jaxrs/api/CollectorApi.java | 112 ------------------ .../ladybug/web/jaxrs/api/TracingApi.java | 63 ++++++++++ .../web/springmvc/api/CollectorApi.java | 49 -------- .../ladybug/web/springmvc/api/TracingApi.java | 64 ++++++++++ .../ladybug/web/common/CollectorApiImpl.java | 99 ---------------- .../ladybug/web/common/TracingApiImpl.java | 49 ++++++++ 7 files changed, 177 insertions(+), 261 deletions(-) delete mode 100644 ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java create mode 100644 ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java delete mode 100644 ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java create mode 100644 ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java delete mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java create mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java index 4278bec04..9ea39158c 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java @@ -129,7 +129,7 @@ public void setTesterRoles(List testerRoles) { public void setWebServiceRoles(List webServiceRoles) { if (constructorDone) log.info("Set web service roles"); - addConfigurationPart("POST/" + Constants.LADYBUG_API_PATH + "/collector/.*$", webServiceRoles); + addConfigurationPart("POST/" + Constants.LADYBUG_API_PATH + "/traces/.*$", webServiceRoles); } public void setLadybugApiRoles(Map> ladybugApiRoles) { diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java deleted file mode 100644 index f1e8911d7..000000000 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2021-2026 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug.web.jaxrs.api; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; -import io.opentelemetry.proto.trace.v1.ResourceSpans; -import io.opentelemetry.proto.trace.v1.ScopeSpans; -import io.opentelemetry.proto.trace.v1.Span; -import jakarta.annotation.PostConstruct; -import jakarta.inject.Inject; -import jakarta.ws.rs.*; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.Response; -import lombok.Setter; -import org.springframework.beans.factory.annotation.Autowired; -import org.wearefrank.ladybug.*; -import org.wearefrank.ladybug.storage.CrudStorage; -import org.wearefrank.ladybug.storage.StorageException; -import org.wearefrank.ladybug.web.common.CollectorApiImpl; -import org.wearefrank.ladybug.web.common.Constants; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -@Path("/" + Constants.LADYBUG_API_PATH + "/collector") -public class CollectorApi extends ApiBase { - private SpanBuffer spanBuffer; - - @Autowired - private @Setter CollectorApiImpl delegate; - - private @Setter @Inject - @Autowired TestTool testTool; - - @PostConstruct - public void init() { - spanBuffer = new SpanBuffer(delegate); - } - - @POST - @Consumes({"application/x-protobuf", "application/json"}) - public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException, StorageException { - ExportTraceServiceRequest request; - - if (contentType.startsWith("application/x-protobuf")) { - request = ExportTraceServiceRequest.parseFrom(data); - } else if (contentType.startsWith("application/json")) { - String json = new String(data); - ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); - JsonFormat.parser().merge(json, builder); - request = builder.build(); - } else { - return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) - .entity("Unsupported Content-Type: " + contentType) - .build(); - } - - for (ResourceSpans resourceSpans : request.getResourceSpansList()) { - for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { - for (Span span : scopeSpans.getSpansList()) { - spanBuffer.addSpan(span); - - CrudStorage storage = (CrudStorage) testTool.getDebugStorage(); - - for (Integer storageId : storage.getStorageIds()) { - if (storage.getReport(storageId).getCorrelationId().equals(delegate.byteStringToHex(span.getTraceId()))) { - Report report = storage.getReport(storageId); - List checkpoints = report.getCheckpoints(); - - List newList = new ArrayList<>(); - - for (int i = 0; i < checkpoints.size(); i++) { - Checkpoint checkpoint = checkpoints.get(i); - newList.add(checkpoint); - - if (Objects.equals(checkpoint.getMessage(), delegate.byteStringToHex(span.getParentSpanId()))) { - Checkpoint start = new Checkpoint(report, checkpoints.get(i).getThreadName(), null, span.getName(), CheckpointType.STARTPOINT.toInt(), checkpoints.get(i).getLevel() + 1); - Checkpoint end = new Checkpoint(report, checkpoints.get(i).getThreadName(), null, span.getName(), CheckpointType.ENDPOINT.toInt(), checkpoints.get(i).getLevel() + 1); - - newList.add(start); - newList.add(end); - } - } - - report.setCheckpoints(newList); - - storage.update(report); - } - } - } - } - } - - return Response.ok().build(); - } -} \ No newline at end of file diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java new file mode 100644 index 000000000..19a7f700a --- /dev/null +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -0,0 +1,63 @@ +/* + Copyright 2021-2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug.web.jaxrs.api; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; +import jakarta.ws.rs.*; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.wearefrank.ladybug.storage.StorageException; +import org.wearefrank.ladybug.web.common.TracingApiImpl; +import org.wearefrank.ladybug.web.common.Constants; + +@Path("/" + Constants.LADYBUG_API_PATH + "/traces") +public class TracingApi extends ApiBase { + @Autowired + private @Setter TracingApiImpl delegate; + + @POST + @Consumes({"application/x-protobuf", "application/json"}) + public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException, StorageException { + ExportTraceServiceRequest request; + + if (contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType.startsWith("application/json")) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); + } else { + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) + .entity("Unsupported Content-Type: " + contentType) + .build(); + } + + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { + for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { + delegate.processSpans(scopeSpans.getSpansList()); + } + } + + return Response.ok().build(); + } +} \ No newline at end of file diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java deleted file mode 100644 index a8b005d96..000000000 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2026 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug.web.springmvc.api; - -import jakarta.annotation.security.RolesAllowed; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.wearefrank.ladybug.Span; -import org.wearefrank.ladybug.web.common.CollectorApiImpl; - -import lombok.Setter; - -@RestController -@RequestMapping("/collector") -@RolesAllowed("IbisWebService") -public class CollectorApi { - @Autowired - private @Setter CollectorApiImpl delegate; - - @PostMapping - public ResponseEntity collectSpans(Span[] trace) { - //delegate.processSpans(trace); - return ResponseEntity.ok().build(); - } - - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity collectSpansJson(Span[] trace) { - //delegate.processSpans(trace); - return ResponseEntity.ok().build(); - } - -} \ No newline at end of file diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java new file mode 100644 index 000000000..311840585 --- /dev/null +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -0,0 +1,64 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug.web.springmvc.api; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; +import jakarta.annotation.security.RolesAllowed; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.wearefrank.ladybug.storage.StorageException; +import org.wearefrank.ladybug.web.common.TracingApiImpl; + +import lombok.Setter; + +@RestController +@RequestMapping("/traces") +@RolesAllowed("IbisWebService") +public class TracingApi { + @Autowired + private @Setter TracingApiImpl delegate; + + @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, "application/x-protobuf"}) + public ResponseEntity receiveTrace(@RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType, @RequestBody byte[] data) throws InvalidProtocolBufferException, StorageException { + ExportTraceServiceRequest request; + + if (contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType.startsWith("application/json")) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); + } else { + return ResponseEntity.badRequest().build(); + } + + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { + for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { + delegate.processSpans(scopeSpans.getSpansList()); + } + } + + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java deleted file mode 100644 index bcddc5867..000000000 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - Copyright 2026 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug.web.common; - -import com.google.protobuf.ByteString; -import lombok.Setter; -import org.apache.commons.codec.binary.Hex; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import io.opentelemetry.proto.trace.v1.Span; -import org.wearefrank.ladybug.TraceTree; -import org.wearefrank.ladybug.TestTool; - -import java.lang.invoke.MethodHandles; -import java.util.*; - -@Component -public class CollectorApiImpl { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - @Autowired - private @Setter TestTool testTool; - - public void processSpans(ArrayList spans) { - TraceTree traceTree = new TraceTree(testTool); - ArrayList spanIds = new ArrayList<>(); - - for (Span span : spans) { - spanIds.add(byteStringToHex(span.getSpanId())); - } - - Span rootSpan = findRoot(spans); - - if (rootSpan != null) { - for (Span span: spans) { - String parentId = byteStringToHex(span.getParentSpanId()); - - if (!parentId.isEmpty() && spanIds.contains(parentId)) { - traceTree.addEdge(parentId, span); - } else { - ArrayList timeSortedSpans = new ArrayList<>(spans); - timeSortedSpans.sort(Comparator.comparingLong(Span::getStartTimeUnixNano)); - - for (int i = 0; i < timeSortedSpans.size(); i++) { - if (byteStringToHex(span.getSpanId()).equals(byteStringToHex(timeSortedSpans.get(i).getSpanId()))) { - if (i > 0) { - traceTree.addEdge(byteStringToHex(timeSortedSpans.get(i - 1).getSpanId()), span); - } - } - } - } - } - traceTree.dfs(rootSpan); - } - } - - public String byteStringToHex(ByteString byteString) { - return Hex.encodeHexString(byteString.toByteArray()); - } - - private Span findRoot(ArrayList unorderedTrace) { - ArrayList spanIds = new ArrayList<>(); - - for (Span span : unorderedTrace) { - spanIds.add(byteStringToHex(span.getSpanId())); - } - - for (Span span : unorderedTrace) { - String parentId = byteStringToHex(span.getParentSpanId()); - if (parentId.isEmpty()) { - return span; - } - } - - for (Span span : unorderedTrace) { - String parentId = byteStringToHex(span.getParentSpanId()); - if (!spanIds.contains(parentId)) { - return span; - } - } - - return null; - } -} \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java new file mode 100644 index 000000000..7b02a7430 --- /dev/null +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -0,0 +1,49 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug.web.common; + +import com.google.protobuf.ByteString; +import lombok.Setter; +import org.apache.commons.codec.binary.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import io.opentelemetry.proto.trace.v1.Span; +import org.wearefrank.ladybug.TestTool; +import org.wearefrank.ladybug.storage.StorageException; + +import java.lang.invoke.MethodHandles; +import java.util.*; + +@Component +public class TracingApiImpl { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Autowired + private @Setter TestTool testTool; + + public void processSpans(List spans) throws StorageException { + for (Span span : spans) { + testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "test", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); + testTool.endpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "Endpoint", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); + } + } + + public String byteStringToHex(ByteString byteString) { + return Hex.encodeHexString(byteString.toByteArray()); + } +} \ No newline at end of file From c359e2f13aa04b81f3868dd3923c19759aeb1d2c Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 18 May 2026 17:51:50 +0200 Subject: [PATCH 15/47] Delete unnecessary classes. --- .../org/wearefrank/ladybug/SpanBuffer.java | 58 ------------- .../org/wearefrank/ladybug/TraceTree.java | 84 ------------------- 2 files changed, 142 deletions(-) delete mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java delete mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java deleted file mode 100644 index 1b13fd210..000000000 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - Copyright 2026 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.RemovalCause; -import io.opentelemetry.proto.trace.v1.Span; -import org.springframework.stereotype.Component; -import org.wearefrank.ladybug.web.common.CollectorApiImpl; - -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -@Component -public class SpanBuffer { - private final Cache> cache; - - private CollectorApiImpl delegate; - - public SpanBuffer(CollectorApiImpl delegate) { - this.delegate = delegate; - this.cache = Caffeine.newBuilder() - .expireAfterWrite(30, TimeUnit.SECONDS) - .removalListener((String traceId, ArrayList spans, RemovalCause cause) -> { - if (spans != null && cause == RemovalCause.EXPIRED) { - ArrayList spansCopy = new ArrayList<>(spans); - delegate.processSpans(spansCopy); - } - }) - .build(); - } - - public void addSpan(Span span) { - String traceId = this.delegate.byteStringToHex(span.getTraceId()); - - cache.asMap().compute(traceId, (key, existing) -> { - ArrayList updated = - existing == null ? new ArrayList<>() : new ArrayList<>(existing); - - updated.add(span); - return updated; - }); - } -} \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java deleted file mode 100644 index 734b52aa4..000000000 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TraceTree.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2026 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug; - -import com.google.protobuf.ByteString; -import io.opentelemetry.proto.common.v1.AnyValue; -import io.opentelemetry.proto.common.v1.KeyValue; -import io.opentelemetry.proto.trace.v1.Span; -import org.apache.commons.codec.binary.Hex; - -import java.util.*; - -public class TraceTree { - private HashMap> spans; - TestTool testTool; - - public TraceTree(TestTool testTool) { - spans = new HashMap<>(); - this.testTool = testTool; - } - - public void addEdge(String parent, Span child) { - spans.putIfAbsent(parent, new ArrayList<>()); - spans.get(parent).add(child); - } - - public void dfs(Span root) { - HashSet visited = new HashSet<>(); - dfsRecursive(root, visited); - } - - private void dfsRecursive(Span span, HashSet visited) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), String.valueOf(byteStringToHex(span.getSpanId()))); - - for (KeyValue keyValue : span.getAttributesList()) { - AnyValue anyValue = keyValue.getValue(); - String value = String.valueOf(anyValue.getField(anyValue.getDescriptorForType().findFieldByNumber(anyValue.getValueCase().getNumber()))); - testTool.infopoint(byteStringToHex(span.getTraceId()), null, keyValue.getKey(), value); - } - - visited.add(span); - - if (spans.containsKey(byteStringToHex(span.getSpanId()))) { - for (Span sibling : spans.get(byteStringToHex(span.getSpanId()))) { - if (!visited.contains(sibling)) { - dfsRecursive(sibling, visited); - } - } - } - - testTool.endpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "Endpoint"); - } - - public String byteStringToHex(ByteString byteString) { - return Hex.encodeHexString(byteString.toByteArray()); - } - - public HashMap toHashMap(Span span) { - HashMap map = new HashMap<>(); - - span.getAllFields().forEach((descriptor, value) -> { - if (value instanceof ByteString) { - map.put(descriptor.getName(), byteStringToHex((ByteString) value)); - } else { - map.put(descriptor.getName(), value.toString()); - } - }); - - return map; - } -} \ No newline at end of file From 834c46219f454b09ddb25c26bc681ee2c3cf0e9d Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 18 May 2026 17:52:22 +0200 Subject: [PATCH 16/47] Delete unnecessary classes. --- .../ladybug/test/junit/TestTraceTree.java | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java deleted file mode 100644 index a356ee9f1..000000000 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTraceTree.java +++ /dev/null @@ -1,94 +0,0 @@ -//package org.wearefrank.ladybug.test.junit; -// -//import com.google.protobuf.ByteString; -//import io.opentelemetry.proto.common.v1.AnyValue; -//import io.opentelemetry.proto.common.v1.KeyValue; -//import io.opentelemetry.proto.trace.v1.Span; -//import org.junit.jupiter.api.BeforeEach; -//import org.junit.jupiter.api.Test; -//import org.mockito.Mockito; -//import org.wearefrank.ladybug.TestTool; -//import org.wearefrank.ladybug.TraceTree; -// -//import static org.mockito.ArgumentMatchers.*; -//import static org.mockito.Mockito.*; -// -//class TestTraceTree { -// private TestTool testTool; -// private TraceTree traceTree; -// -// @BeforeEach -// void setUp() { -// testTool = Mockito.mock(TestTool.class); -// traceTree = new TraceTree(testTool); -// } -// -// @Test -// void testDfsOnlyRoot() { -// Span span = Span.newBuilder() -// .setTraceId(ByteString.copyFromUtf8("trace1")) -// .setSpanId(ByteString.copyFromUtf8("span1")) -// .setName("root") -// .addAttributes(KeyValue.newBuilder() -// .setKey("http.method") -// .setValue(AnyValue.newBuilder().setStringValue("GET").build()) -// .build()) -// .build(); -// -// traceTree.dfs(span); -// -// verify(testTool).startpoint(anyString(), isNull(), eq("root"), any()); -// verify(testTool).infopoint(anyString(), isNull(), eq("http.method"), anyString()); -// verify(testTool).endpoint(anyString(), isNull(), eq("root"), eq("Endpoint")); -// } -// -// @Test -// void testDfsWithChildSpan() { -// Span parent = Span.newBuilder() -// .setTraceId(ByteString.copyFromUtf8("trace1")) -// .setSpanId(ByteString.copyFromUtf8("parent")) -// .setName("parent") -// .build(); -// -// Span child = Span.newBuilder() -// .setTraceId(ByteString.copyFromUtf8("trace1")) -// .setSpanId(ByteString.copyFromUtf8("child")) -// .setParentSpanId(ByteString.copyFromUtf8("parent")) -// .setName("child") -// .build(); -// -// traceTree.addEdge(traceTree.byteStringToHex(parent.getSpanId()), child); -// -// traceTree.dfs(parent); -// -// verify(testTool).startpoint(anyString(), isNull(), eq("parent"), any()); -// verify(testTool).startpoint(anyString(), isNull(), eq("child"), any()); -// verify(testTool).endpoint(anyString(), isNull(), eq("child"), eq("Endpoint")); -// verify(testTool).endpoint(anyString(), isNull(), eq("parent"), eq("Endpoint")); -// } -// -// @Test -// void testByteStringToHex() { -// ByteString bytes = ByteString.copyFromUtf8("abc"); -// String hex = traceTree.byteStringToHex(bytes); -// -// assert hex != null; -// assert !hex.isEmpty(); -// assert hex.equals("616263"); -// } -// -// @Test -// void testToHashMap() { -// Span span = Span.newBuilder() -// .setTraceId(ByteString.copyFromUtf8("trace1")) -// .setSpanId(ByteString.copyFromUtf8("span1")) -// .setName("testSpan") -// .build(); -// -// var map = traceTree.toHashMap(span); -// -// assert map.containsKey("trace_id"); -// assert map.containsKey("span_id"); -// assert map.containsKey("name"); -// } -//} \ No newline at end of file From 8173af99651341f9c6b204da88135b6a4942a4d8 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 18 May 2026 19:04:24 +0200 Subject: [PATCH 17/47] Dependencies. --- ladybug-backend-jaxrs/pom.xml | 12 ++++-------- ladybug-backend-springmvc/pom.xml | 16 ++++++++++++++++ .../java/org/wearefrank/ladybug/TestTool.java | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ladybug-backend-jaxrs/pom.xml b/ladybug-backend-jaxrs/pom.xml index b6d66e2b2..786136821 100644 --- a/ladybug-backend-jaxrs/pom.xml +++ b/ladybug-backend-jaxrs/pom.xml @@ -21,11 +21,6 @@ opentelemetry-proto 1.9.0-alpha - - com.google.protobuf - protobuf-java-util - 4.34.0 - com.google.protobuf protobuf-java @@ -70,9 +65,10 @@ lombok - com.github.ben-manes.caffeine - caffeine - 3.2.3 + com.google.protobuf + protobuf-java-util + 4.34.0 + compile \ No newline at end of file diff --git a/ladybug-backend-springmvc/pom.xml b/ladybug-backend-springmvc/pom.xml index 8ed3e0e33..044c05a36 100644 --- a/ladybug-backend-springmvc/pom.xml +++ b/ladybug-backend-springmvc/pom.xml @@ -54,5 +54,21 @@ org.projectlombok lombok + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + + + com.google.protobuf + protobuf-java + 4.34.0 + + + com.google.protobuf + protobuf-java-util + 4.34.0 + compile + \ No newline at end of file diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 5d684e4fc..f24a1eb3a 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -29,7 +29,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import io.opentelemetry.api.trace.Tracer; import jakarta.annotation.PostConstruct; @@ -104,6 +103,7 @@ public class TestTool { boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() private String openTelemetryEndpoint; private Tracer tracer; + boolean updatingFromStorage = false; @PostConstruct public void init() { From 9685edc9b3f586830f8d184681be0ff519041c7b Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 18 May 2026 19:07:21 +0200 Subject: [PATCH 18/47] Logic for updating instead of storing. --- .../org/wearefrank/ladybug/Checkpoint.java | 6 +- .../java/org/wearefrank/ladybug/Report.java | 108 +++++++++++++++++- .../java/org/wearefrank/ladybug/TestTool.java | 84 ++++++++++++-- 3 files changed, 181 insertions(+), 17 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java index 74198571d..72a81b1d1 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java @@ -1,5 +1,5 @@ /* - Copyright 2019-2025 WeAreFrank!, 2018 Nationale-Nederlanden + Copyright 2019-2026 WeAreFrank!, 2018 Nationale-Nederlanden Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -34,6 +34,8 @@ import javax.json.bind.annotation.JsonbTransient; import javax.xml.xpath.XPathExpressionException; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,6 +83,8 @@ public class Checkpoint implements Serializable, Cloneable { private transient ByteArrayOutputStream messageCapturerOutputStream; private transient Map variablesPatternMap; private transient Span span = null; + private @Setter @Getter String parentId; + private @Setter @Getter String id; public Checkpoint() { // Only for Java XML encoding/decoding! Use other constructor instead. diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 3cbba3924..fa518e1ce 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -273,9 +273,62 @@ protected void init() { threadsActiveCount++; } + public void restoreRuntimeState() { + if (threads == null) { + threads = new ArrayList<>(); + } + if (threadsWithThreadCreatepoint == null) { + threadsWithThreadCreatepoint = new ArrayList<>(); + } + if (threadCheckpointIndex == null) { + threadCheckpointIndex = new HashMap<>(); + } + if (threadFirstLevel == null) { + threadFirstLevel = new HashMap<>(); + } + if (threadLevel == null) { + threadLevel = new HashMap<>(); + } + if (threadParent == null) { + threadParent = new HashMap<>(); + } + if (truncatedMessageMap == null) { + truncatedMessageMap = new RefCompareMap<>(); + } + if (streamingMessageListeners == null) { + streamingMessageListeners = new HashMap<>(); + } + if (streamingMessageResults == null) { + streamingMessageResults = new HashMap<>(); + } + + mainThread = Thread.currentThread().getName(); + + if (!threads.contains(mainThread)) { + threads.add(mainThread); + } + + int level = 0; + + if (!checkpoints.isEmpty()) { + level = checkpoints.get(checkpoints.size() - 1).getLevel(); + } + + threadCheckpointIndex.put(mainThread, checkpoints.size()); + threadFirstLevel.put(mainThread, level); + threadLevel.put(mainThread, level); + + threadsActiveCount = 1; + + reportFilterMatching = true; + logReportFilterMatching = true; + logMaxCheckpoints = true; + logMaxMemoryUsage = true; + } + protected T checkpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent) { if (checkpointType == CheckpointType.THREAD_CREATEPOINT.toInt()) { String parentThreadName = Thread.currentThread().getName(); if (!threads.contains(parentThreadName)) { @@ -318,7 +371,7 @@ protected T checkpoint(String childThreadId, String sourceClassName, String } } message = addCheckpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, findParent); return message; } @@ -375,7 +428,7 @@ private void removeThreadCreatepoint(int index, String childThreadId) { private T addCheckpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent) { String threadName = Thread.currentThread().getName(); Integer index = threadCheckpointIndex.get(threadName); Integer level = threadLevel.get(threadName); @@ -441,7 +494,7 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin } } else { message = addCheckpoint(threadName, sourceClassName, name, message, messageContext, stubableCode, - stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level); + stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level, id, parentId, findParent); } Integer newLevel = level + levelChangeNextCheckpoint; threadLevel.put(threadName, newLevel); @@ -457,9 +510,54 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin @SneakyThrows private T addCheckpoint(String threadName, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, Integer index, Integer level) { + Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String parentId, String id, boolean findParent) { + + if (findParent && parentId != null) { + Checkpoint parentCheckpoint = null; + + for (Checkpoint checkpoint : checkpoints) { + if (Objects.equals(checkpoint.getId(), parentId)) { + parentCheckpoint = checkpoint; + break; + } + } + + if (parentCheckpoint != null) { + level = parentCheckpoint.getLevel() + 1; + + if (checkpointType == CheckpointType.STARTPOINT.toInt()) { + index = parentCheckpoint.getIndex() + 1; + } else if (checkpointType == CheckpointType.ENDPOINT.toInt()) { + Checkpoint matchingStartpoint = null; + + for (Checkpoint checkpoint : checkpoints) { + if (Objects.equals(checkpoint.getId(), id) && checkpoint.getType() == CheckpointType.STARTPOINT.toInt()) { + matchingStartpoint = checkpoint; + break; + } + } + + if (matchingStartpoint != null) { + level = matchingStartpoint.getLevel(); + index = matchingStartpoint.getIndex() + 1; + + while (index < checkpoints.size() && checkpoints.get(index).getLevel() > matchingStartpoint.getLevel()) { + index++; + } + } + } + } + } + Checkpoint checkpoint = new Checkpoint(this, threadName, sourceClassName, name, checkpointType, level); + + if (checkpointType == CheckpointType.STARTPOINT.toInt()){ + checkpoint.setId(id); + checkpoint.setParentId(parentId); + } + checkpoint.setMessageContext(messageContext); + if (testTool.getOpenTelemetryTracer() != null) { SpanBuilder checkpointSpanBuilder = testTool.getOpenTelemetryTracer().spanBuilder("checkpoint - " + name); for (Checkpoint checkpointInList: checkpoints) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index f24a1eb3a..b6d3108ec 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -44,6 +44,7 @@ import org.wearefrank.ladybug.storage.LogStorage; import org.wearefrank.ladybug.storage.Storage; import org.wearefrank.ladybug.storage.StorageException; +import org.wearefrank.ladybug.storage.database.DatabaseCrudStorage; import org.wearefrank.ladybug.storage.memory.MemoryLogStorage; import org.wearefrank.ladybug.transform.MessageTransformer; import org.wearefrank.ladybug.util.OpenTelemetryUtil; @@ -335,25 +336,65 @@ public Tracer getOpenTelemetryTracer() { return tracer; } + public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + return checkpoint(correlationId, childThreadId, sourceClassName, name, + message, null, stubableCode, stubableCodeThrowsException, + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null); + } + + public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + return checkpoint(correlationId, childThreadId, sourceClassName, name, + message, messageContext, stubableCode, stubableCodeThrowsException, + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null); + } + public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId) { return checkpoint(correlationId, childThreadId, sourceClassName, name, message, null, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId); } private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId) { boolean executeStubableCode = true; + if (reportGeneratorEnabled) { Report report; // Blocking for all threads for all reports synchronized(reportsInProgress) { report = getReportInProgress(correlationId); if (report == null) { - report = createReport(correlationId, name, checkpointType); + try { + for (Integer storageId : debugStorage.getStorageIds()) { + if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { + report = debugStorage.getReport(storageId); + report.restoreRuntimeState(); + report.setClosed(false); + report.setTestTool(this); + + reportsInProgress.add(0, report); + reportsInProgressByCorrelationId.put(correlationId, report); + numberOfReportsInProgress++; + + updatingFromStorage = true; + + break; + } + } + } catch (StorageException e) { + log.error("Failed to find report in storage", e); + } + + if (report == null) { + report = createReport(correlationId, name, checkpointType); + } } } if (devMode) randomSleep(); @@ -382,7 +423,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour executeStubableCode = false; message = report.checkpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, matchingStubStrategies, checkpointType, - levelChangeNextCheckpoint); + levelChangeNextCheckpoint, id, parentId, updatingFromStorage); closeReportIfFinished(report); } report = null; @@ -476,12 +517,21 @@ protected void closeReportIfFinished(Report report) { numberOfReportsInProgress--; } if (report.isReportFilterMatching()) { - try { - debugStorage.store(report); - } catch (StorageException e) { - + if (updatingFromStorage) { + try { + ((DatabaseCrudStorage) debugStorage).update(report); + updatingFromStorage = false; + } catch (StorageException e) { + log.error("Failed to store report", e); + } + + } else { + try { + debugStorage.store(report); + } catch (StorageException e) { + + } } - } } } @@ -555,6 +605,11 @@ public T startpoint(String correlationId, String sourceClassName, String nam CheckpointType.STARTPOINT.toInt(), 1); } + public T startpoint(String correlationId, String sourceClassName, String name, T message, String parentId, String id) { + return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, + CheckpointType.STARTPOINT.toInt(), 1, id, parentId); + } + /** * Parameter throwsException determines the type of exception thrown. E.g. when set to (IOException)null the * compiler will report this method to throw an IOException which needs to be handled. When set to null the compiler @@ -581,6 +636,11 @@ public T endpoint(String correlationId, String sourceClassName, String name, return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, CheckpointType.ENDPOINT.toInt(), -1); } + + public T endpoint(String correlationId, String sourceClassName, String name, T message, String parentId, String id) { + return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, + CheckpointType.ENDPOINT.toInt(), -1, id, parentId); + } public T endpoint(String correlationId, String sourceClassName, String name, T message, Map messageContext) { return checkpoint(correlationId, null, sourceClassName, name, message, messageContext, null, null, null, @@ -1119,7 +1179,9 @@ public String rerun(String correlationId, Report report, SecurityContext securit } else { errorMessage = debugger.rerun(correlationId, report, securityContext, reportRunner); } - } finally { + } catch (StorageException e) { + throw new RuntimeException(e); + } finally { if (reportGeneratorEnabled) { // Verify that originalReport has been removed from originalReports by checkpoint() Report originalReport; From 9396cba8b847a205efda19ca051cebd2ee925465 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 19 May 2026 09:34:27 +0200 Subject: [PATCH 19/47] Copyrights. --- .../src/main/java/org/wearefrank/ladybug/TestTool.java | 4 +--- .../src/main/java/org/wearefrank/ladybug/storage/Storage.java | 2 +- .../java/org/wearefrank/ladybug/storage/file/Storage.java | 2 +- .../storage/proofofmigration/ProofOfMigrationErrorsView.java | 2 +- .../storage/proofofmigration/ProofOfMigrationView.java | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index b6d3108ec..376162b41 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -1179,9 +1179,7 @@ public String rerun(String correlationId, Report report, SecurityContext securit } else { errorMessage = debugger.rerun(correlationId, report, securityContext, reportRunner); } - } catch (StorageException e) { - throw new RuntimeException(e); - } finally { + } finally { if (reportGeneratorEnabled) { // Verify that originalReport has been removed from originalReports by checkpoint() Report originalReport; diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java index 474f7a609..7aac1eac2 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/Storage.java @@ -1,5 +1,5 @@ /* - Copyright 2020-2022, 2024-2025 WeAreFrank!, 2018 Nationale-Nederlanden + Copyright 2020-2022, 2024-2026 WeAreFrank!, 2018 Nationale-Nederlanden Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java index a55c58e49..cd271814b 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/file/Storage.java @@ -1,5 +1,5 @@ /* - Copyright 2020-2022, 2024-2025 WeAreFrank!, 2018 Nationale-Nederlanden + Copyright 2020-2022, 2024-2026 WeAreFrank!, 2018 Nationale-Nederlanden Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java index dbe4fce3b..7689f9063 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationErrorsView.java @@ -1,5 +1,5 @@ /* - Copyright 2022, 2024, 2025 WeAreFrank! + Copyright 2022, 2024, 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java index 1e239aff8..de98a04ef 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/storage/proofofmigration/ProofOfMigrationView.java @@ -1,5 +1,5 @@ /* - Copyright 2022-2025 WeAreFrank! + Copyright 2022-2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 9436816617d31bb6d6f3a37f71ca9db7858b0f6f Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 19 May 2026 10:46:09 +0200 Subject: [PATCH 20/47] Fix. --- .../main/java/org/wearefrank/ladybug/Report.java | 3 ++- .../main/java/org/wearefrank/ladybug/TestTool.java | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index fa518e1ce..2f09068c1 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -510,7 +510,7 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin @SneakyThrows private T addCheckpoint(String threadName, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String parentId, String id, boolean findParent) { + Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String id, String parentId, boolean findParent) { if (findParent && parentId != null) { Checkpoint parentCheckpoint = null; @@ -523,6 +523,7 @@ private T addCheckpoint(String threadName, String sourceClassName, String n } if (parentCheckpoint != null) { + System.out.println("PARENTCHECKPOINT IS NOT NULL"); level = parentCheckpoint.getLevel() + 1; if (checkpointType == CheckpointType.STARTPOINT.toInt()) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 376162b41..79bfab3cb 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -518,13 +518,14 @@ protected void closeReportIfFinished(Report report) { } if (report.isReportFilterMatching()) { if (updatingFromStorage) { - try { - ((DatabaseCrudStorage) debugStorage).update(report); - updatingFromStorage = false; - } catch (StorageException e) { - log.error("Failed to store report", e); + if (debugStorage.isCrudStorage()) { + try { + ((CrudStorage) debugStorage).update(report); + } catch (StorageException e) { + log.error("Failed to store report", e); + } } - + updatingFromStorage = false; } else { try { debugStorage.store(report); From 956e0ddf15fae67de66cf4e899388f85989433fc Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 19 May 2026 13:56:53 +0200 Subject: [PATCH 21/47] Fix. --- .../java/org/wearefrank/ladybug/Checkpoint.java | 16 ++++++++++++++-- .../main/java/org/wearefrank/ladybug/Report.java | 9 +++++++++ .../java/org/wearefrank/ladybug/TestTool.java | 7 +++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java index 72a81b1d1..d7b1c6f15 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java @@ -83,8 +83,8 @@ public class Checkpoint implements Serializable, Cloneable { private transient ByteArrayOutputStream messageCapturerOutputStream; private transient Map variablesPatternMap; private transient Span span = null; - private @Setter @Getter String parentId; - private @Setter @Getter String id; + private String parentId; + private String id; public Checkpoint() { // Only for Java XML encoding/decoding! Use other constructor instead. @@ -99,6 +99,18 @@ public Checkpoint(Report report, String threadName, String sourceClassName, Stri this.level = level; } + @Transient + public String getId() { return this.id; } + + @Transient + public void setId(String id) { this.id = id; } + + @Transient + public String getParentId() { return this.parentId; } + + @Transient + public void setParentId(String parentId) { this.parentId = parentId; } + // JsonIgnore is used so that Jackson will not get into an infinite loop trying to reference report, // which already contains checkpoint. @JsonIgnore diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 2f09068c1..7ba106d21 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -134,6 +134,15 @@ public class Report implements Serializable { private transient boolean logMaxMemoryUsage = true; private transient Map> streamingMessageListeners = new HashMap<>(); private transient Map streamingMessageResults = new HashMap<>(); + private boolean beingUpdated; + + @Transient + //@JsonIgnore + public boolean isBeingUpdated() { return beingUpdated; } + + @Transient + //@JsonIgnore + public void setBeingUpdated(boolean beingUpdated) { this.beingUpdated = beingUpdated; } @Transient @JsonIgnore diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 79bfab3cb..1909b3e26 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -383,7 +383,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour reportsInProgressByCorrelationId.put(correlationId, report); numberOfReportsInProgress++; - updatingFromStorage = true; + report.setBeingUpdated(true); break; } @@ -423,7 +423,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour executeStubableCode = false; message = report.checkpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, matchingStubStrategies, checkpointType, - levelChangeNextCheckpoint, id, parentId, updatingFromStorage); + levelChangeNextCheckpoint, id, parentId, report.isBeingUpdated()); closeReportIfFinished(report); } report = null; @@ -517,7 +517,7 @@ protected void closeReportIfFinished(Report report) { numberOfReportsInProgress--; } if (report.isReportFilterMatching()) { - if (updatingFromStorage) { + if (report.isBeingUpdated()) { if (debugStorage.isCrudStorage()) { try { ((CrudStorage) debugStorage).update(report); @@ -525,7 +525,6 @@ protected void closeReportIfFinished(Report report) { log.error("Failed to store report", e); } } - updatingFromStorage = false; } else { try { debugStorage.store(report); From 6b3325e6e78e2e19c1f710b2c475a69ec67f9b78 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 20 May 2026 10:15:53 +0200 Subject: [PATCH 22/47] Fix. --- .../ladybug/test/junit/util/TestExport.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java index ad624d07d..cd0838aaa 100644 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java @@ -22,10 +22,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.zip.GZIPInputStream; import org.junit.Test; @@ -51,11 +48,11 @@ public void testExport() throws IllegalAccessException, IllegalArgumentException // Find all bean properties and change default values to test that transient properties are not added to the // XMLEncoder xml (properties with default values will never be added to the xml by XMLEncoder) Report report = new Report(); - Map setMethods = new HashMap<>(); + Map setMethods = new TreeMap<>(); getBeanProperties(report.getClass(), "set", setMethods); - Map getMethods = new HashMap<>(); + Map getMethods = new TreeMap<>(); getBeanProperties(report.getClass(), "get", getMethods); - Map isMethods = new HashMap<>(); + Map isMethods = new TreeMap<>(); getBeanProperties(report.getClass(), "is", isMethods); for (String name : setMethods.keySet()) { Method method = setMethods.get(name); From dd7b9d92ed767e5b06d8701dbe9fd4fa3d5d8539 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 20 May 2026 11:04:46 +0200 Subject: [PATCH 23/47] Fix. --- .../java/org/wearefrank/ladybug/Report.java | 5 +-- .../java/org/wearefrank/ladybug/TestTool.java | 39 ++++++++++--------- .../ladybug/web/common/TracingApiImpl.java | 2 + .../resources/springTestToolTestWebapp.xml | 6 +-- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 7ba106d21..4ef7e1740 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -137,11 +137,11 @@ public class Report implements Serializable { private boolean beingUpdated; @Transient - //@JsonIgnore + @JsonIgnore public boolean isBeingUpdated() { return beingUpdated; } @Transient - //@JsonIgnore + @JsonIgnore public void setBeingUpdated(boolean beingUpdated) { this.beingUpdated = beingUpdated; } @Transient @@ -532,7 +532,6 @@ private T addCheckpoint(String threadName, String sourceClassName, String n } if (parentCheckpoint != null) { - System.out.println("PARENTCHECKPOINT IS NOT NULL"); level = parentCheckpoint.getLevel() + 1; if (checkpointType == CheckpointType.STARTPOINT.toInt()) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 1909b3e26..489a365ea 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -104,7 +104,7 @@ public class TestTool { boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() private String openTelemetryEndpoint; private Tracer tracer; - boolean updatingFromStorage = false; + private @Setter boolean updateFromStorage = false; @PostConstruct public void init() { @@ -371,25 +371,28 @@ private T checkpoint(String correlationId, String childThreadId, String sour synchronized(reportsInProgress) { report = getReportInProgress(correlationId); if (report == null) { - try { - for (Integer storageId : debugStorage.getStorageIds()) { - if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { - report = debugStorage.getReport(storageId); - report.restoreRuntimeState(); - report.setClosed(false); - report.setTestTool(this); - - reportsInProgress.add(0, report); - reportsInProgressByCorrelationId.put(correlationId, report); - numberOfReportsInProgress++; - - report.setBeingUpdated(true); - - break; + if (updateFromStorage && debugStorage.isCrudStorage()) { + try { + for (Integer storageId : debugStorage.getStorageIds()) { + if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { + report = debugStorage.getReport(storageId); + synchronized (report) { + report.restoreRuntimeState(); + report.setClosed(false); + report.setTestTool(this); + + reportsInProgress.add(0, report); + reportsInProgressByCorrelationId.put(correlationId, report); + numberOfReportsInProgress++; + + report.setBeingUpdated(true); + } + break; + } } + } catch (StorageException e) { + log.error("Failed to find report in storage", e); } - } catch (StorageException e) { - log.error("Failed to find report in storage", e); } if (report == null) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 7b02a7430..3711a32cc 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -37,6 +37,8 @@ public class TracingApiImpl { private @Setter TestTool testTool; public void processSpans(List spans) throws StorageException { + testTool.setUpdateFromStorage(true); + for (Span span : spans) { testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "test", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); testTool.endpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "Endpoint", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); diff --git a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml index 5c924de8c..feb8a1053 100644 --- a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml +++ b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml @@ -159,7 +159,7 @@ - + @@ -175,7 +175,7 @@ - + @@ -251,7 +251,7 @@ - + From 68facee08a4d6f4655f8e4e5e7421fbdf65610cb Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 20 May 2026 12:49:38 +0200 Subject: [PATCH 24/47] Orphan spans handling. --- .../java/org/wearefrank/ladybug/Report.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 4ef7e1740..32aff26b7 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -646,6 +646,12 @@ private T addCheckpoint(String threadName, String sourceClassName, String n // Add checkpoint to the list after stubable code has been executed. Otherwise when a report in progress is // opened it might give the impression that the stubable code is already executed checkpoints.add(index, checkpoint); + + if (checkpointType == CheckpointType.STARTPOINT.toInt() + && checkpoint.getId() != null) { + moveChildrenBelowParent(checkpoint); + } + for (int i = threads.indexOf(threadName); i < threads.size(); i++) { String key = threads.get(i); Integer value = threadCheckpointIndex.get(key); @@ -671,6 +677,37 @@ private T addCheckpoint(String threadName, String sourceClassName, String n return message; } + private void moveChildrenBelowParent(Checkpoint parentCheckpoint) { + int parentIndex = checkpoints.indexOf(parentCheckpoint); + int insertIndex = parentIndex + 1; + + List children = new ArrayList<>(); + + for (Checkpoint cp : new ArrayList<>(checkpoints)) { + if (Objects.equals(cp.getParentId(), parentCheckpoint.getId())) { + children.add(cp); + } + } + + for (Checkpoint child : children) { + checkpoints.remove(child); + + child.setLevel(parentCheckpoint.getLevel() + 1); + + checkpoints.add(insertIndex, child); + insertIndex++; + + moveChildrenBelowParent(child); + + insertIndex = checkpoints.indexOf(child) + 1; + + while (insertIndex < checkpoints.size() + && checkpoints.get(insertIndex).getLevel() > child.getLevel()) { + insertIndex++; + } + } + } + public String getThreadInfo() { return "\nmainThread: " + mainThread + "\nmainThreadFinishedTime: " + mainThreadFinishedTime From 6f1e0ac6ace943bf8651ceb78ad25b28beeae254 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 25 May 2026 14:04:33 +0200 Subject: [PATCH 25/47] Fix. --- .../java/org/wearefrank/ladybug/Report.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 32aff26b7..5a2d7eb26 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -679,6 +679,11 @@ private T addCheckpoint(String threadName, String sourceClassName, String n private void moveChildrenBelowParent(Checkpoint parentCheckpoint) { int parentIndex = checkpoints.indexOf(parentCheckpoint); + + if (parentIndex < 0) { + return; + } + int insertIndex = parentIndex + 1; List children = new ArrayList<>(); @@ -690,16 +695,25 @@ private void moveChildrenBelowParent(Checkpoint parentCheckpoint) { } for (Checkpoint child : children) { + checkpoints.remove(child); child.setLevel(parentCheckpoint.getLevel() + 1); + insertIndex = Math.min(insertIndex, checkpoints.size()); + checkpoints.add(insertIndex, child); - insertIndex++; moveChildrenBelowParent(child); - insertIndex = checkpoints.indexOf(child) + 1; + int childIndex = checkpoints.indexOf(child); + + if (childIndex < 0) { + insertIndex = checkpoints.size(); + continue; + } + + insertIndex = childIndex + 1; while (insertIndex < checkpoints.size() && checkpoints.get(insertIndex).getLevel() > child.getLevel()) { From 86d90be38fa851f123781fdbd61484475d081bc5 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 25 May 2026 21:08:19 +0200 Subject: [PATCH 26/47] Complete refactor of logic. --- .../org/wearefrank/ladybug/Checkpoint.java | 7 + .../java/org/wearefrank/ladybug/Report.java | 196 ++++++++++++------ .../java/org/wearefrank/ladybug/TestTool.java | 35 +++- .../ladybug/web/common/TracingApiImpl.java | 28 ++- 4 files changed, 189 insertions(+), 77 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java index d7b1c6f15..3a21ecf43 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java @@ -85,6 +85,7 @@ public class Checkpoint implements Serializable, Cloneable { private transient Span span = null; private String parentId; private String id; + private long startTime; public Checkpoint() { // Only for Java XML encoding/decoding! Use other constructor instead. @@ -111,6 +112,12 @@ public Checkpoint(Report report, String threadName, String sourceClassName, Stri @Transient public void setParentId(String parentId) { this.parentId = parentId; } + @Transient + public long getStartTime() { return this.startTime; } + + @Transient + public void setStartTime(long startTime) { this.startTime = startTime; } + // JsonIgnore is used so that Jackson will not get into an infinite loop trying to reference report, // which already contains checkpoint. @JsonIgnore diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 5a2d7eb26..17e9aa033 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -18,18 +18,7 @@ import java.beans.Transient; import java.io.Serializable; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Scanner; -import java.util.Set; +import java.util.*; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; @@ -337,7 +326,7 @@ public void restoreRuntimeState() { protected T checkpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent, long startTime) { if (checkpointType == CheckpointType.THREAD_CREATEPOINT.toInt()) { String parentThreadName = Thread.currentThread().getName(); if (!threads.contains(parentThreadName)) { @@ -379,8 +368,13 @@ protected T checkpoint(String childThreadId, String sourceClassName, String } } } + + if (checkpointType == CheckpointType.STARTPOINT.toInt() && parentId != null && parentId.isEmpty()) { + setName(name); + } + message = addCheckpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, findParent); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, findParent, startTime); return message; } @@ -437,7 +431,7 @@ private void removeThreadCreatepoint(int index, String childThreadId) { private T addCheckpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent, long startTime) { String threadName = Thread.currentThread().getName(); Integer index = threadCheckpointIndex.get(threadName); Integer level = threadLevel.get(threadName); @@ -503,7 +497,7 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin } } else { message = addCheckpoint(threadName, sourceClassName, name, message, messageContext, stubableCode, - stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level, id, parentId, findParent); + stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level, id, parentId, findParent, startTime); } Integer newLevel = level + levelChangeNextCheckpoint; threadLevel.put(threadName, newLevel); @@ -519,52 +513,116 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin @SneakyThrows private T addCheckpoint(String threadName, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String id, String parentId, boolean findParent) { - + Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String id, String parentId, boolean findParent, long startTime) { if (findParent && parentId != null) { - Checkpoint parentCheckpoint = null; - - for (Checkpoint checkpoint : checkpoints) { - if (Objects.equals(checkpoint.getId(), parentId)) { - parentCheckpoint = checkpoint; - break; + if (parentId.isEmpty()) { + if (checkpointType == CheckpointType.STARTPOINT.toInt()) { + level = 0; + index = 0; + } else if (checkpointType == CheckpointType.ENDPOINT.toInt()) { + level = 0; + index = checkpoints.size(); } - } + } else if (!parentId.isEmpty()) { + if (checkpointType == CheckpointType.STARTPOINT.toInt()) { + Checkpoint parentCheckpoint = null; - if (parentCheckpoint != null) { - level = parentCheckpoint.getLevel() + 1; + for (Checkpoint checkpoint : checkpoints) { + if (Objects.equals(checkpoint.getId(), parentId) && checkpoint.getType() == CheckpointType.STARTPOINT.toInt()) { + parentCheckpoint = checkpoint; + break; + } + } - if (checkpointType == CheckpointType.STARTPOINT.toInt()) { - index = parentCheckpoint.getIndex() + 1; + if (parentCheckpoint != null) { + level = parentCheckpoint.getLevel() + 1; + + if (checkpointType == CheckpointType.STARTPOINT.toInt()) { + int parentIndex = checkpoints.indexOf(parentCheckpoint); + + index = parentIndex + 1; + + if (startTime == -1) { + while (index < checkpoints.size() + && checkpoints.get(index).getLevel() > parentCheckpoint.getLevel()) { + index++; + } + } else if (parentCheckpoint.getStartTime() != -1){ + int childLevel = parentCheckpoint.getLevel() + 1; + int i = index; + + while (i < checkpoints.size()) { + Checkpoint current = checkpoints.get(i); + + if (current.getLevel() == parentCheckpoint.getLevel() + 1 + && current.getType() == CheckpointType.ENDPOINT.toInt()) { + break; + } + + if (current.getLevel() == childLevel + && current.getType() == CheckpointType.STARTPOINT.toInt()) { + if (current.getStartTime() > startTime) { + break; + } + i++; + while (i < checkpoints.size()) { + Checkpoint inner = checkpoints.get(i); + if (inner.getLevel() == childLevel + 1 + && inner.getType() == CheckpointType.ENDPOINT.toInt()) { + i++; + break; + } + i++; + } + } else { + i++; + } + } + index = i; + } + } + } else { + level = 0; + index = checkpoints.size(); + } } else if (checkpointType == CheckpointType.ENDPOINT.toInt()) { Checkpoint matchingStartpoint = null; for (Checkpoint checkpoint : checkpoints) { - if (Objects.equals(checkpoint.getId(), id) && checkpoint.getType() == CheckpointType.STARTPOINT.toInt()) { + if (Objects.equals(checkpoint.getId(), id)) { matchingStartpoint = checkpoint; break; } } if (matchingStartpoint != null) { - level = matchingStartpoint.getLevel(); - index = matchingStartpoint.getIndex() + 1; + level = matchingStartpoint.getLevel() + 1; + + int parentLevel = matchingStartpoint.getLevel(); + + index = checkpoints.indexOf(matchingStartpoint) + 1; - while (index < checkpoints.size() && checkpoints.get(index).getLevel() > matchingStartpoint.getLevel()) { + while (index < checkpoints.size() && checkpoints.get(index).getLevel() > parentLevel) { index++; } } } + } } Checkpoint checkpoint = new Checkpoint(this, threadName, sourceClassName, name, checkpointType, level); - if (checkpointType == CheckpointType.STARTPOINT.toInt()){ + if (id != null) { checkpoint.setId(id); + } + + if (parentId != null && !parentId.isEmpty()) { checkpoint.setParentId(parentId); } + checkpoint.setStartTime(startTime); + checkpoint.setMessageContext(messageContext); if (testTool.getOpenTelemetryTracer() != null) { @@ -647,10 +705,7 @@ private T addCheckpoint(String threadName, String sourceClassName, String n // opened it might give the impression that the stubable code is already executed checkpoints.add(index, checkpoint); - if (checkpointType == CheckpointType.STARTPOINT.toInt() - && checkpoint.getId() != null) { - moveChildrenBelowParent(checkpoint); - } + reparentOrphans(checkpoint); for (int i = threads.indexOf(threadName); i < threads.size(); i++) { String key = threads.get(i); @@ -677,49 +732,64 @@ private T addCheckpoint(String threadName, String sourceClassName, String n return message; } - private void moveChildrenBelowParent(Checkpoint parentCheckpoint) { - int parentIndex = checkpoints.indexOf(parentCheckpoint); - - if (parentIndex < 0) { + private void reparentOrphans(Checkpoint parentCheckpoint) { + if (parentCheckpoint.getId() == null) { return; } - int insertIndex = parentIndex + 1; + List orphans = new ArrayList<>(); - List children = new ArrayList<>(); + for (Checkpoint checkpoint : checkpoints) { + if (checkpoint == parentCheckpoint) { + continue; + } - for (Checkpoint cp : new ArrayList<>(checkpoints)) { - if (Objects.equals(cp.getParentId(), parentCheckpoint.getId())) { - children.add(cp); + if (Objects.equals(parentCheckpoint.getId(), checkpoint.getParentId()) + && checkpoint.getLevel() == 0) { + orphans.add(checkpoint); } } - for (Checkpoint child : children) { + for (Checkpoint orphan : orphans) { + moveSubtree(parentCheckpoint, orphan); + } + } - checkpoints.remove(child); + private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { + int orphanIndex = checkpoints.indexOf(orphanRoot); - child.setLevel(parentCheckpoint.getLevel() + 1); + if (orphanIndex < 0) { + return; + } - insertIndex = Math.min(insertIndex, checkpoints.size()); + int subtreeEnd = orphanIndex + 1; - checkpoints.add(insertIndex, child); + while (subtreeEnd < checkpoints.size() + && checkpoints.get(subtreeEnd).getLevel() > orphanRoot.getLevel()) { + subtreeEnd++; + } - moveChildrenBelowParent(child); + List subtree = + new ArrayList<>(checkpoints.subList(orphanIndex, subtreeEnd)); - int childIndex = checkpoints.indexOf(child); + checkpoints.subList(orphanIndex, subtreeEnd).clear(); - if (childIndex < 0) { - insertIndex = checkpoints.size(); - continue; - } + int parentIndex = checkpoints.indexOf(newParent); + + int insertIndex = parentIndex + 1; - insertIndex = childIndex + 1; + while (insertIndex < checkpoints.size() + && checkpoints.get(insertIndex).getLevel() > newParent.getLevel()) { + insertIndex++; + } - while (insertIndex < checkpoints.size() - && checkpoints.get(insertIndex).getLevel() > child.getLevel()) { - insertIndex++; - } + int levelDelta = (newParent.getLevel() + 1) - orphanRoot.getLevel(); + + for (Checkpoint checkpoint : subtree) { + checkpoint.setLevel(checkpoint.getLevel() + levelDelta); } + + checkpoints.addAll(insertIndex, subtree); } public String getThreadInfo() { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 489a365ea..85233708f 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -104,7 +104,6 @@ public class TestTool { boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() private String openTelemetryEndpoint; private Tracer tracer; - private @Setter boolean updateFromStorage = false; @PostConstruct public void init() { @@ -341,7 +340,7 @@ public T checkpoint(String correlationId, String childThreadId, String sourc Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { return checkpoint(correlationId, childThreadId, sourceClassName, name, message, null, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null, -1); } public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, @@ -349,7 +348,7 @@ public T checkpoint(String correlationId, String childThreadId, String sourc Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { return checkpoint(correlationId, childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null, -1); } public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, @@ -357,12 +356,20 @@ public T checkpoint(String correlationId, String childThreadId, String sourc Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId) { return checkpoint(correlationId, childThreadId, sourceClassName, name, message, null, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, -1); + } + + public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { + return checkpoint(correlationId, childThreadId, sourceClassName, name, + message, null, stubableCode, stubableCodeThrowsException, + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, startTime); } private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { boolean executeStubableCode = true; if (reportGeneratorEnabled) { @@ -371,7 +378,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour synchronized(reportsInProgress) { report = getReportInProgress(correlationId); if (report == null) { - if (updateFromStorage && debugStorage.isCrudStorage()) { + if (debugStorage.isCrudStorage()) { try { for (Integer storageId : debugStorage.getStorageIds()) { if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { @@ -426,7 +433,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour executeStubableCode = false; message = report.checkpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, matchingStubStrategies, checkpointType, - levelChangeNextCheckpoint, id, parentId, report.isBeingUpdated()); + levelChangeNextCheckpoint, id, parentId, report.isBeingUpdated(), startTime); closeReportIfFinished(report); } report = null; @@ -608,11 +615,16 @@ public T startpoint(String correlationId, String sourceClassName, String nam CheckpointType.STARTPOINT.toInt(), 1); } - public T startpoint(String correlationId, String sourceClassName, String name, T message, String parentId, String id) { + public T startpoint(String correlationId, String sourceClassName, String name, T message, String id, String parentId) { return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, CheckpointType.STARTPOINT.toInt(), 1, id, parentId); } + public T startpoint(String correlationId, String sourceClassName, String name, T message, String id, String parentId, long startTime) { + return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, + CheckpointType.STARTPOINT.toInt(), 1, id, parentId, startTime); + } + /** * Parameter throwsException determines the type of exception thrown. E.g. when set to (IOException)null the * compiler will report this method to throw an IOException which needs to be handled. When set to null the compiler @@ -640,10 +652,15 @@ public T endpoint(String correlationId, String sourceClassName, String name, CheckpointType.ENDPOINT.toInt(), -1); } - public T endpoint(String correlationId, String sourceClassName, String name, T message, String parentId, String id) { + public T endpoint(String correlationId, String sourceClassName, String name, T message, String id, String parentId) { return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, CheckpointType.ENDPOINT.toInt(), -1, id, parentId); } + + public T endpoint(String correlationId, String sourceClassName, String name, T message, String id, String parentId, long startTime) { + return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, + CheckpointType.ENDPOINT.toInt(), -1, id, parentId, startTime); + } public T endpoint(String correlationId, String sourceClassName, String name, T message, Map messageContext) { return checkpoint(correlationId, null, sourceClassName, name, message, messageContext, null, null, null, diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 3711a32cc..2f317db82 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -36,15 +36,33 @@ public class TracingApiImpl { @Autowired private @Setter TestTool testTool; - public void processSpans(List spans) throws StorageException { - testTool.setUpdateFromStorage(true); - + public void processSpans(List spans) { for (Span span : spans) { - testTool.startpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "test", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); - testTool.endpoint(byteStringToHex(span.getTraceId()), null, span.getName(), "Endpoint", byteStringToHex(span.getParentSpanId()), byteStringToHex(span.getSpanId())); + String traceId = byteStringToHex(span.getTraceId()); + String spanId = byteStringToHex(span.getSpanId()); + String parentSpanId = span.getParentSpanId().isEmpty() ? "" : byteStringToHex(span.getParentSpanId()); + long startTime = span.getStartTimeUnixNano(); + + testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); + testTool.endpoint(traceId, null, span.getName(), "Endpoint", spanId, parentSpanId, startTime); + testTool.close(traceId); } } + public HashMap toHashMap(Span span) { + HashMap map = new HashMap<>(); + + span.getAllFields().forEach((descriptor, value) -> { + if (value instanceof ByteString) { + map.put(descriptor.getName(), byteStringToHex((ByteString) value)); + } else { + map.put(descriptor.getName(), value.toString()); + } + }); + + return map; + } + public String byteStringToHex(ByteString byteString) { return Hex.encodeHexString(byteString.toByteArray()); } From b22986a5a8be80e4468a4e725a522990c2108b54 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 25 May 2026 21:36:57 +0200 Subject: [PATCH 27/47] Fix. --- .../ladybug/web/common/TracingApiImpl.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 2f317db82..b0bc32a07 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -37,14 +37,23 @@ public class TracingApiImpl { private @Setter TestTool testTool; public void processSpans(List spans) { + Map> spansByTrace = new LinkedHashMap<>(); for (Span span : spans) { String traceId = byteStringToHex(span.getTraceId()); - String spanId = byteStringToHex(span.getSpanId()); - String parentSpanId = span.getParentSpanId().isEmpty() ? "" : byteStringToHex(span.getParentSpanId()); - long startTime = span.getStartTimeUnixNano(); + spansByTrace.computeIfAbsent(traceId, k -> new ArrayList<>()).add(span); + } + + for (Map.Entry> entry : spansByTrace.entrySet()) { + String traceId = entry.getKey(); + for (Span span : entry.getValue()) { + String spanId = byteStringToHex(span.getSpanId()); + String parentSpanId = span.getParentSpanId().isEmpty() + ? "" : byteStringToHex(span.getParentSpanId()); + long startTime = span.getStartTimeUnixNano(); - testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); - testTool.endpoint(traceId, null, span.getName(), "Endpoint", spanId, parentSpanId, startTime); + testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); + testTool.endpoint(traceId, null, span.getName(), null, spanId, parentSpanId, startTime); + } testTool.close(traceId); } } From 05eb209c4e2ca66513812c4f68ae5882481597c9 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 25 May 2026 22:19:57 +0200 Subject: [PATCH 28/47] Add cache back in. --- .../org/wearefrank/ladybug/SpanBuffer.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java new file mode 100644 index 000000000..ce2ff44d3 --- /dev/null +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/SpanBuffer.java @@ -0,0 +1,60 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.Scheduler; +import io.opentelemetry.proto.trace.v1.Span; +import org.springframework.stereotype.Component; +import org.wearefrank.ladybug.web.common.TracingApiImpl; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +@Component +public class SpanBuffer { + private final Cache> cache; + + private TracingApiImpl delegate; + + public SpanBuffer(TracingApiImpl delegate) { + this.delegate = delegate; + this.cache = Caffeine.newBuilder() + .expireAfterWrite(30, TimeUnit.SECONDS) + .scheduler(Scheduler.systemScheduler()) + .removalListener((String traceId, ArrayList spans, RemovalCause cause) -> { + if (spans != null && cause == RemovalCause.EXPIRED) { + ArrayList spansCopy = new ArrayList<>(spans); + delegate.processSpans(spansCopy); + } + }) + .build(); + } + + public void addSpan(Span span) { + String traceId = this.delegate.byteStringToHex(span.getTraceId()); + + cache.asMap().compute(traceId, (key, existing) -> { + ArrayList updated = + existing == null ? new ArrayList<>() : new ArrayList<>(existing); + + updated.add(span); + return updated; + }); + } +} \ No newline at end of file From 89b87d3f4c4eb9f07b77ff8eacb71205b58a9a3d Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 25 May 2026 22:35:24 +0200 Subject: [PATCH 29/47] Add cache back in. --- .../ladybug/web/jaxrs/api/TracingApi.java | 14 ++++++++++- .../ladybug/web/common/TracingApiImpl.java | 25 +++++++------------ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index 19a7f700a..d946566a6 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -20,20 +20,30 @@ import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; +import io.opentelemetry.proto.trace.v1.Span; +import jakarta.annotation.PostConstruct; import jakarta.ws.rs.*; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; +import org.wearefrank.ladybug.SpanBuffer; import org.wearefrank.ladybug.storage.StorageException; import org.wearefrank.ladybug.web.common.TracingApiImpl; import org.wearefrank.ladybug.web.common.Constants; @Path("/" + Constants.LADYBUG_API_PATH + "/traces") public class TracingApi extends ApiBase { + private SpanBuffer spanBuffer; + @Autowired private @Setter TracingApiImpl delegate; + @PostConstruct + public void init() { + spanBuffer = new SpanBuffer(delegate); + } + @POST @Consumes({"application/x-protobuf", "application/json"}) public Response receiveTrace(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException, StorageException { @@ -54,7 +64,9 @@ public Response receiveTrace(@HeaderParam("Content-Type") String contentType, by for (ResourceSpans resourceSpans : request.getResourceSpansList()) { for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { - delegate.processSpans(scopeSpans.getSpansList()); + for (Span span : scopeSpans.getSpansList()) { + spanBuffer.addSpan(span); + } } } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index b0bc32a07..7767b2f88 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -37,25 +37,18 @@ public class TracingApiImpl { private @Setter TestTool testTool; public void processSpans(List spans) { - Map> spansByTrace = new LinkedHashMap<>(); - for (Span span : spans) { - String traceId = byteStringToHex(span.getTraceId()); - spansByTrace.computeIfAbsent(traceId, k -> new ArrayList<>()).add(span); - } + String traceId = byteStringToHex(spans.get(0).getTraceId()); - for (Map.Entry> entry : spansByTrace.entrySet()) { - String traceId = entry.getKey(); - for (Span span : entry.getValue()) { - String spanId = byteStringToHex(span.getSpanId()); - String parentSpanId = span.getParentSpanId().isEmpty() - ? "" : byteStringToHex(span.getParentSpanId()); - long startTime = span.getStartTimeUnixNano(); + for (Span span : spans) { + String spanId = byteStringToHex(span.getSpanId()); + String parentSpanId = span.getParentSpanId().isEmpty() + ? "" : byteStringToHex(span.getParentSpanId()); + long startTime = span.getStartTimeUnixNano(); - testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); - testTool.endpoint(traceId, null, span.getName(), null, spanId, parentSpanId, startTime); - } - testTool.close(traceId); + testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); + testTool.endpoint(traceId, null, span.getName(), null, spanId, parentSpanId, startTime); } + testTool.close(traceId); } public HashMap toHashMap(Span span) { From f4b195d78f7b679af7c789c5722f497431c6330a Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 09:48:42 +0200 Subject: [PATCH 30/47] Rename. --- .../ladybug/web/jaxrs/ApiAuthorizationFilter.java | 2 +- .../web/jaxrs/api/{CollectorApi.java => TracingApi.java} | 6 +++--- .../springmvc/api/{CollectorApi.java => TracingApi.java} | 8 ++++---- .../common/{CollectorApiImpl.java => TracingApiImpl.java} | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/{CollectorApi.java => TracingApi.java} (89%) rename ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/{CollectorApi.java => TracingApi.java} (90%) rename ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/{CollectorApiImpl.java => TracingApiImpl.java} (96%) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java index df634fd08..bdeebf70c 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/ApiAuthorizationFilter.java @@ -125,7 +125,7 @@ public void setTesterRoles(List testerRoles) { public void setWebServiceRoles(List webServiceRoles) { if (constructorDone) log.info("Set web service roles"); - addConfigurationPart("POST/" + Constants.LADYBUG_API_PATH + "/collector/.*$", webServiceRoles); + addConfigurationPart("POST/" + Constants.LADYBUG_API_PATH + "/traces/.*$", webServiceRoles); } public void setLadybugApiRoles(Map> ladybugApiRoles) { diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java similarity index 89% rename from ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java rename to ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index 408df91f9..6eb729303 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -22,13 +22,13 @@ import org.wearefrank.ladybug.Span; import org.springframework.beans.factory.annotation.Autowired; -import org.wearefrank.ladybug.web.common.CollectorApiImpl; +import org.wearefrank.ladybug.web.common.TracingApiImpl; import org.wearefrank.ladybug.web.common.Constants; @Path("/" + Constants.LADYBUG_API_PATH + "/collector") -public class CollectorApi extends ApiBase { +public class TracingApi extends ApiBase { @Autowired - private @Setter CollectorApiImpl delegate; + private @Setter TracingApiImpl delegate; @POST public Response collectSpans(Span[] trace) { diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java similarity index 90% rename from ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java rename to ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java index a2a252f77..850228ddf 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -1,5 +1,5 @@ /* - Copyright 2025 WeAreFrank! + Copyright 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,16 +23,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.wearefrank.ladybug.Span; -import org.wearefrank.ladybug.web.common.CollectorApiImpl; +import org.wearefrank.ladybug.web.common.TracingApiImpl; import lombok.Setter; @RestController @RequestMapping("/collector") @RolesAllowed("IbisWebService") -public class CollectorApi { +public class TracingApi { @Autowired - private @Setter CollectorApiImpl delegate; + private @Setter TracingApiImpl delegate; @PostMapping public ResponseEntity collectSpans(Span[] trace) { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java similarity index 96% rename from ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java rename to ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 65a0612d4..df32ae223 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/CollectorApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -1,5 +1,5 @@ /* - Copyright 2025 WeAreFrank! + Copyright 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ import java.util.ArrayList; @Component -public class CollectorApiImpl { +public class TracingApiImpl { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Autowired From de42b3e934a1701cf33e9c62ec64736e9c0207e3 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 10:00:25 +0200 Subject: [PATCH 31/47] Process OTel spans instead of Ladybug spans. --- ladybug-backend-jaxrs/pom.xml | 18 ++++++ .../ladybug/web/jaxrs/api/TracingApi.java | 50 ++++++++++++--- ladybug-backend-springmvc/pom.xml | 18 ++++++ .../ladybug/web/springmvc/api/TracingApi.java | 63 +++++++++++++------ ladybug-common/pom.xml | 6 ++ .../ladybug/web/common/TracingApiImpl.java | 30 ++++----- 6 files changed, 143 insertions(+), 42 deletions(-) diff --git a/ladybug-backend-jaxrs/pom.xml b/ladybug-backend-jaxrs/pom.xml index fc42717f0..f4c3bb1b3 100644 --- a/ladybug-backend-jaxrs/pom.xml +++ b/ladybug-backend-jaxrs/pom.xml @@ -45,5 +45,23 @@ org.projectlombok lombok + + com.google.protobuf + protobuf-java + 4.34.1 + compile + + + com.google.protobuf + protobuf-java-util + 4.34.0 + compile + + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + compile + diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index 6eb729303..a7ab2adec 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -15,11 +15,16 @@ */ package org.wearefrank.ladybug.web.jaxrs.api; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.Setter; -import org.wearefrank.ladybug.Span; import org.springframework.beans.factory.annotation.Autowired; import org.wearefrank.ladybug.web.common.TracingApiImpl; @@ -31,15 +36,40 @@ public class TracingApi extends ApiBase { private @Setter TracingApiImpl delegate; @POST - public Response collectSpans(Span[] trace) { - delegate.processSpans(trace); - return Response.ok().build(); - } + @Consumes({"application/x-protobuf", "application/json"}) + public Response receiveSpans(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException { + ExportTraceServiceRequest request; - @POST - @Consumes(MediaType.APPLICATION_JSON) - public Response collectSpansJson(Span[] trace) { - delegate.processSpans(trace); - return Response.ok().build(); + if (contentType != null && contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType != null && contentType.startsWith("application/json")) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); + } else { + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) + .type(MediaType.APPLICATION_JSON) + .entity(JsonFormat.printer().print(response)) + .build(); + } + + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { + for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { + delegate.processSpans(scopeSpans.getSpansList()); + } + } + + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + if (contentType.startsWith("application/x-protobuf")) { + return Response.ok(response.toByteArray()) + .type("application/x-protobuf") + .build(); + } else { + return Response.ok(JsonFormat.printer().print(response)) + .type(MediaType.APPLICATION_JSON) + .build(); + } } } diff --git a/ladybug-backend-springmvc/pom.xml b/ladybug-backend-springmvc/pom.xml index f05cb250f..322267ef4 100644 --- a/ladybug-backend-springmvc/pom.xml +++ b/ladybug-backend-springmvc/pom.xml @@ -50,5 +50,23 @@ org.projectlombok lombok + + com.google.protobuf + protobuf-java + 4.34.1 + compile + + + com.google.protobuf + protobuf-java-util + 4.34.0 + compile + + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + compile + diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java index 850228ddf..0170df5c6 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -1,5 +1,5 @@ /* - Copyright 2025, 2026 WeAreFrank! + Copyright 2025 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,35 +15,62 @@ */ package org.wearefrank.ladybug.web.springmvc.api; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; import jakarta.annotation.security.RolesAllowed; +import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.wearefrank.ladybug.Span; +import org.springframework.web.bind.annotation.*; import org.wearefrank.ladybug.web.common.TracingApiImpl; -import lombok.Setter; - @RestController -@RequestMapping("/collector") +@RequestMapping("/traces") @RolesAllowed("IbisWebService") public class TracingApi { @Autowired private @Setter TracingApiImpl delegate; - @PostMapping - public ResponseEntity collectSpans(Span[] trace) { - delegate.processSpans(trace); - return ResponseEntity.ok().build(); - } + @PostMapping(consumes = {"application/x-protobuf", MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity receiveSpans(@RequestHeader("Content-Type") String contentType, @RequestBody byte[] data) throws InvalidProtocolBufferException { + ExportTraceServiceRequest request; - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity collectSpansJson(Span[] trace) { - delegate.processSpans(trace); - return ResponseEntity.ok().build(); - } + if (contentType != null && contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType != null && contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); + + } else { + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + .contentType(MediaType.APPLICATION_JSON) + .body(JsonFormat.printer().print(response)); + } + for (ResourceSpans resourceSpans : request.getResourceSpansList()) { + for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { + delegate.processSpans(scopeSpans.getSpansList()); + } + } + + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + if (contentType.startsWith("application/x-protobuf")) { + return ResponseEntity.ok() + .contentType(org.springframework.http.MediaType.parseMediaType("application/x-protobuf")) + .body(response.toByteArray()); + } else { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(JsonFormat.printer().print(response)); + } + } } \ No newline at end of file diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index d227cc6b2..3ffbe429a 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -129,6 +129,12 @@ io.opentelemetry opentelemetry-exporter-otlp + + io.opentelemetry.proto + opentelemetry-proto + 1.9.0-alpha + compile + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index df32ae223..1562d387c 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -15,16 +15,18 @@ */ package org.wearefrank.ladybug.web.common; +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.trace.v1.Span; import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.wearefrank.ladybug.Span; import org.wearefrank.ladybug.TestTool; import java.lang.invoke.MethodHandles; import java.util.ArrayList; +import java.util.List; @Component public class TracingApiImpl { @@ -33,29 +35,29 @@ public class TracingApiImpl { @Autowired private @Setter TestTool testTool; - public void processSpans(Span[] trace) { - ArrayList parentIds = new ArrayList<>(); + public void processSpans(List trace) { + ArrayList parentIds = new ArrayList<>(); for (Span span: trace) { - if (span.getParentId() != null && !parentIds.contains(span.getParentId())) { - parentIds.add(span.getParentId()); + if (!span.getParentSpanId().isEmpty() && !parentIds.contains(span.getParentSpanId())) { + parentIds.add(span.getParentSpanId()); } } ArrayList endpoints = new ArrayList<>(); - for (int i = trace.length - 1; i >= 0; i--) { - if (trace[i].getParentId() == null) { - testTool.startpoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); - endpoints.add(trace[i].getName()); + for (int i = trace.size() - 1; i >= 0; i--) { + if (trace.get(i).getParentSpanId().isEmpty()) { + testTool.startpoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); + endpoints.add(trace.get(i).getName()); } else { - if (parentIds.contains(trace[i].getId())) { - testTool.startpoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); - endpoints.add(trace[i].getName()); + if (parentIds.contains(trace.get(i).getSpanId())) { + testTool.startpoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); + endpoints.add(trace.get(i).getName()); } else { - testTool.infopoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); + testTool.infopoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); } } } for (int i = endpoints.size() - 1; i >= 0; i--) { - testTool.endpoint(trace[0].getTraceId(), null, endpoints.get(i), "Endpoint"); + testTool.endpoint(trace.get(0).getTraceId(), null, endpoints.get(i), "Endpoint"); } } } From c1fe4de9ad7ca3b36494d44e42859dea576bdbb1 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 10:01:00 +0200 Subject: [PATCH 32/47] Delete old Ladybug Span class. --- .../java/org/wearefrank/ladybug/Span.java | 110 ------------------ 1 file changed, 110 deletions(-) delete mode 100644 ladybug-common/src/main/java/org/wearefrank/ladybug/Span.java diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Span.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Span.java deleted file mode 100644 index 16db6c57a..000000000 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Span.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - Copyright 2024, 2025 WeAreFrank! - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -package org.wearefrank.ladybug; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.Map; -import io.opentelemetry.api.trace.SpanKind; - -/** - * Created a Span class to map incoming telemetry data from the endpoint. There is no library available with classes to catch such telemetry data in spans. - */ - -public class Span { - private String traceId; - private String parentId; - private String id; - private SpanKind kind; - private String name; - private long timestamp; - private long duration; - private Map localEndpoint; - private Map tags; - - public Span(String traceId, String parentId, String id, SpanKind kind, String name, long timestamp, long duration, Map localEndpoint, Map tags) { - this.traceId = traceId; - this.parentId = parentId; - this.id = id; - this.kind = kind; - this.name = name; - this.timestamp = timestamp; - this.duration = duration; - this.localEndpoint = localEndpoint; - this.tags = tags; - } - - public Span() { - } - - public String getTraceId() { - return traceId; - } - - public String getParentId() { - return parentId; - } - - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public long getTimestamp() { - return timestamp; - } - - public long getDuration() { - return duration; - } - - public Map getLocalEndpoint() { - return localEndpoint; - } - - public Map getTags() { - return tags; - } - - public String getKind() { - if (kind == null) { - return null; - } - return kind.toString(); - } - - public Map toHashmap() { - String date = LocalDateTime.ofInstant(Instant.ofEpochMilli(this.timestamp / 1000), ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss")); - Map map = new HashMap<>(); - map.put("\"traceId\"", "\"" + this.traceId + "\""); - map.put("\"parentId\"", "\"" + this.parentId + "\""); - map.put("\"id\"", "\"" + this.id + "\""); - map.put("\"kind\"", "\"" + this.kind + "\""); - map.put("\"name\"", "\"" + this.name + "\""); - map.put("\"time\"", "\"" + date + "\""); - map.put("\"duration\"", "\"" + this.duration + "\""); - map.put("\"localEndpoint\"", "\"" + this.localEndpoint + "\""); - map.put("\"tags\"", "\"" + this.tags + "\""); - - return map; - } -} From 4dc8667f13990495db9acb6299228ea3cb1829f1 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 10:05:20 +0200 Subject: [PATCH 33/47] Fix. --- ladybug-common/pom.xml | 5 ++++ .../ladybug/web/common/TracingApiImpl.java | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index 3ffbe429a..1e08ae8d4 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -135,6 +135,11 @@ 1.9.0-alpha compile + + commons-codec + commons-codec + 1.17.0 + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 1562d387c..0bb1e97bf 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -23,9 +23,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.wearefrank.ladybug.TestTool; +import org.apache.commons.codec.binary.Hex; import java.lang.invoke.MethodHandles; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; @Component @@ -45,19 +47,37 @@ public void processSpans(List trace) { ArrayList endpoints = new ArrayList<>(); for (int i = trace.size() - 1; i >= 0; i--) { if (trace.get(i).getParentSpanId().isEmpty()) { - testTool.startpoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); + testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); endpoints.add(trace.get(i).getName()); } else { if (parentIds.contains(trace.get(i).getSpanId())) { - testTool.startpoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); + testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); endpoints.add(trace.get(i).getName()); } else { - testTool.infopoint(trace.get(i).getTraceId(), null, trace.get(i).getName(), trace.get(i).toHashmap().toString()); + testTool.infopoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); } } } for (int i = endpoints.size() - 1; i >= 0; i--) { - testTool.endpoint(trace.get(0).getTraceId(), null, endpoints.get(i), "Endpoint"); + testTool.endpoint(byteStringToHex(trace.get(0).getTraceId()), null, endpoints.get(i), "Endpoint"); } } + + public String byteStringToHex(ByteString byteString) { + return Hex.encodeHexString(byteString.toByteArray()); + } + + public HashMap toHashMap(Span span) { + HashMap map = new HashMap<>(); + + span.getAllFields().forEach((descriptor, value) -> { + if (value instanceof ByteString) { + map.put(descriptor.getName(), byteStringToHex((ByteString) value)); + } else { + map.put(descriptor.getName(), value.toString()); + } + }); + + return map; + } } From 1f6f9e5c52b315b24bf687ecc57b54d11b6f7a3f Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 10:08:23 +0200 Subject: [PATCH 34/47] Fix for export. --- README.md | 8 ++++++++ .../src/main/java/org/wearefrank/ladybug/TestTool.java | 6 +++++- .../org/wearefrank/ladybug/util/OpenTelemetryUtil.java | 4 ++-- .../src/main/resources/springTestToolTestWebapp.xml | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 84c64ebb8..c4fa80424 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,14 @@ To work with Jaeger, you can run the following command: `docker run --rm -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 16686:16686 -p 4317:4317 -p 4318:4318 -p 9411:9411 jaegertracing/all-in-one:latest` +You'll need to uncomment or add: + +``` + +``` + +In the testTool bean. + To choose between one of the collectors in the Ladybug application, there is a bean available to make your choice. You have to add the following and change the string value of this bean to the collector you want to use. For Zipkin, enter the endpoint in the string value. For Jaeger (which doesn't use a endpoint), you can just enter "jaeger": ``` diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 444eedadf..efe129b15 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -102,7 +102,7 @@ public class TestTool { private @Setter @Getter @Inject @Autowired Views views; private @Setter @Getter int reportsInProgressThreshold = 300000; boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() - private @Qualifier("openTelemetryEndpoint") String openTelemetryEndpoint; + private String openTelemetryEndpoint; private Tracer tracer; @PostConstruct @@ -117,6 +117,10 @@ public void reset() { reportGeneratorEnabled = defaultReportGeneratorEnabled; } + public void setOpenTelemetryEndpoint(String openTelemetryEndpoint) { + this.openTelemetryEndpoint = openTelemetryEndpoint; + } + public void setSecurityLoggerName(String securityLoggerName) { securityLog = LoggerFactory.getLogger(securityLoggerName); } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java index 5a02b1ff9..2aef74b61 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/util/OpenTelemetryUtil.java @@ -1,5 +1,5 @@ /* - Copyright 2024, 2025 WeAreFrank! + Copyright 2024, 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -71,7 +71,7 @@ public static Tracer getOpenTelemetryTracer(String openTelemetryEndpoint) { ContextPropagators.create( TextMapPropagator.composite( W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) - .buildAndRegisterGlobal(); + .build(); return openTelemetry.getTracer(Report.class.getName(), "0.1.0"); } return null; diff --git a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml index fb4839f6c..4f519e858 100644 --- a/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml +++ b/ladybug-test-webapp/src/main/resources/springTestToolTestWebapp.xml @@ -33,6 +33,7 @@ + From e075f4b768408f1b11a412cc06088f58eeff13bc Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 10:26:04 +0200 Subject: [PATCH 35/47] Fix. --- .../org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java | 2 +- ladybug-backend-springmvc/pom.xml | 8 ++++---- .../wearefrank/ladybug/web/springmvc/api/TracingApi.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index a7ab2adec..59df2403d 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -30,7 +30,7 @@ import org.wearefrank.ladybug.web.common.TracingApiImpl; import org.wearefrank.ladybug.web.common.Constants; -@Path("/" + Constants.LADYBUG_API_PATH + "/collector") +@Path("/" + Constants.LADYBUG_API_PATH + "/traces") public class TracingApi extends ApiBase { @Autowired private @Setter TracingApiImpl delegate; diff --git a/ladybug-backend-springmvc/pom.xml b/ladybug-backend-springmvc/pom.xml index 322267ef4..d31cfb23e 100644 --- a/ladybug-backend-springmvc/pom.xml +++ b/ladybug-backend-springmvc/pom.xml @@ -51,10 +51,10 @@ lombok - com.google.protobuf - protobuf-java - 4.34.1 - compile + com.google.protobuf + protobuf-java + 4.34.1 + compile com.google.protobuf diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java index 0170df5c6..240306458 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -1,5 +1,5 @@ /* - Copyright 2025 WeAreFrank! + Copyright 2025, 2026 WeAreFrank! Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 1a8d0661f02b2caf60531c1b456b3dfa81273bd3 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 13:53:33 +0200 Subject: [PATCH 36/47] Fix. --- ladybug-backend-jaxrs/pom.xml | 2 +- ladybug-backend-springmvc/pom.xml | 2 +- ladybug-common/pom.xml | 10 +++++----- .../src/main/java/org/wearefrank/ladybug/TestTool.java | 4 ---- .../wearefrank/ladybug/test/junit/util/TestExport.java | 3 +++ 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/ladybug-backend-jaxrs/pom.xml b/ladybug-backend-jaxrs/pom.xml index 2d3df8387..f4c3bb1b3 100644 --- a/ladybug-backend-jaxrs/pom.xml +++ b/ladybug-backend-jaxrs/pom.xml @@ -64,4 +64,4 @@ compile - \ No newline at end of file + diff --git a/ladybug-backend-springmvc/pom.xml b/ladybug-backend-springmvc/pom.xml index 53a541ae0..ddd124d15 100644 --- a/ladybug-backend-springmvc/pom.xml +++ b/ladybug-backend-springmvc/pom.xml @@ -68,4 +68,4 @@ compile - \ No newline at end of file + diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index fd1350724..e0a4fd03c 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -140,11 +140,11 @@ commons-codec 1.17.0 - - com.github.ben-manes.caffeine - caffeine - 3.2.3 - + + com.github.ben-manes.caffeine + caffeine + 3.2.3 + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 5b1908168..342e9c24a 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -129,10 +129,6 @@ public Logger getSecurityLog() { return securityLog; } - public void setOpenTelemetryEndpoint(String openTelemetryEndpoint) { - this.openTelemetryEndpoint = openTelemetryEndpoint; - } - public void setConfigName(String configName) { this.configName = configName; } diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java index cd0838aaa..388c41d04 100644 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/util/TestExport.java @@ -116,6 +116,9 @@ public void testExport() throws IllegalAccessException, IllegalArgumentException if (method.getParameters()[0].getType() == int.class) { Integer defaultValue = (Integer)getMethods.get(name).invoke(checkpoint, new Object[0]); method.invoke(checkpoint, defaultValue + name.length()); + } else if (method.getParameters()[0].getType() == long.class) { + Long defaultValue = (Long) getMethods.get(name).invoke(checkpoint, new Object[0]); + method.invoke(checkpoint, defaultValue + name.length()); } else if (method.getParameters()[0].getType() == boolean.class) { assertIsMethodAvailable(isMethods, name); Boolean defaultValue = (Boolean)isMethods.get(name).invoke(checkpoint, new Object[0]); From 521fcecb69b07f2e3d950b5c7db0b427cccfad52 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 14:06:33 +0200 Subject: [PATCH 37/47] Tests. --- .../ladybug/test/junit/TestSpanBuffer.java | 107 +++++++++ .../test/junit/TestTracingApiImpl.java | 131 +++++++++++ .../createreport/TestReportParenting.java | 219 ++++++++++++++++++ 3 files changed, 457 insertions(+) create mode 100644 ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestSpanBuffer.java create mode 100644 ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTracingApiImpl.java create mode 100644 ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestSpanBuffer.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestSpanBuffer.java new file mode 100644 index 000000000..e9a94316e --- /dev/null +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestSpanBuffer.java @@ -0,0 +1,107 @@ +/* + Copyright 2026 WeAreFrank! + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package org.wearefrank.ladybug.test.junit; + +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.trace.v1.Span; +import org.junit.Test; +import org.wearefrank.ladybug.SpanBuffer; +import org.wearefrank.ladybug.web.common.TracingApiImpl; + +import java.util.ArrayList; + +import static org.mockito.Mockito.*; + +public class TestSpanBuffer { + @Test + public void testAddSingleSpan() { + TracingApiImpl delegate = mock(TracingApiImpl.class); + + when(delegate.byteStringToHex(any())) + .thenReturn("trace1"); + + SpanBuffer spanBuffer = new SpanBuffer(delegate); + + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("span") + .build(); + + spanBuffer.addSpan(span); + + verify(delegate, times(1)) + .byteStringToHex(span.getTraceId()); + } + + @Test + public void testAddMultipleSpansSameTrace() { + TracingApiImpl delegate = mock(TracingApiImpl.class); + + when(delegate.byteStringToHex(any())) + .thenReturn("trace1"); + + SpanBuffer spanBuffer = new SpanBuffer(delegate); + + Span span1 = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("span1") + .build(); + + Span span2 = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span2")) + .setName("span2") + .build(); + + spanBuffer.addSpan(span1); + spanBuffer.addSpan(span2); + + verify(delegate, times(2)) + .byteStringToHex(any()); + } + + @Test + public void testExpirationProcessesSpans() throws Exception { + TracingApiImpl delegate = mock(TracingApiImpl.class); + + when(delegate.byteStringToHex(any())) + .thenReturn("trace1"); + + SpanBuffer spanBuffer = new SpanBuffer(delegate); + + Span span1 = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("span1") + .build(); + + Span span2 = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span2")) + .setName("span2") + .build(); + + spanBuffer.addSpan(span1); + spanBuffer.addSpan(span2); + + Thread.sleep(31000); + + verify(delegate, timeout(5000).times(1)) + .processSpans(any(ArrayList.class)); + } +} \ No newline at end of file diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTracingApiImpl.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTracingApiImpl.java new file mode 100644 index 000000000..298b7c557 --- /dev/null +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/TestTracingApiImpl.java @@ -0,0 +1,131 @@ +package org.wearefrank.ladybug.test.junit; + +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.Span; +import org.junit.Before; +import org.junit.Test; +import org.wearefrank.ladybug.TestTool; +import org.wearefrank.ladybug.web.common.TracingApiImpl; + +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class TestTracingApiImpl { + + private TracingApiImpl tracingApi; + private TestTool testTool; + + @Before + public void setUp() { + tracingApi = new TracingApiImpl(); + + testTool = mock(TestTool.class); + tracingApi.setTestTool(testTool); + } + + @Test + public void testByteStringToHex() { + ByteString byteString = ByteString.copyFromUtf8("test"); + + String result = tracingApi.byteStringToHex(byteString); + + assertEquals("74657374", result); + } + + @Test + public void testToHashMap() { + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace")) + .setSpanId(ByteString.copyFromUtf8("span")) + .setParentSpanId(ByteString.copyFromUtf8("parent")) + .setName("test-span") + .setStartTimeUnixNano(12345L) + .addAttributes( + KeyValue.newBuilder() + .setKey("http.method") + .setValue( + AnyValue.newBuilder() + .setStringValue("GET") + .build() + ) + .build() + ) + .build(); + + HashMap map = tracingApi.toHashMap(span); + + assertNotNull(map); + + assertEquals("test-span", map.get("name")); + assertEquals("12345", map.get("start_time_unix_nano")); + + assertTrue(map.containsKey("trace_id")); + assertTrue(map.containsKey("span_id")); + assertTrue(map.containsKey("parent_span_id")); + } + + @Test + public void testProcessSpans() { + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setParentSpanId(ByteString.copyFromUtf8("parent1")) + .setName("test-span") + .setStartTimeUnixNano(12345L) + .build(); + + tracingApi.processSpans(List.of(span)); + + verify(testTool, times(1)) + .startpoint( + anyString(), + isNull(), + eq("test-span"), + anyMap(), + anyString(), + anyString(), + eq(12345L) + ); + + verify(testTool, times(1)) + .endpoint( + anyString(), + isNull(), + eq("test-span"), + isNull(), + anyString(), + anyString(), + eq(12345L) + ); + + verify(testTool, times(1)) + .close(anyString()); + } + + @Test + public void testProcessSpansWithoutParent() { + Span span = Span.newBuilder() + .setTraceId(ByteString.copyFromUtf8("trace1")) + .setSpanId(ByteString.copyFromUtf8("span1")) + .setName("root-span") + .setStartTimeUnixNano(999L) + .build(); + + tracingApi.processSpans(List.of(span)); + + verify(testTool).startpoint( + anyString(), + isNull(), + eq("root-span"), + anyMap(), + anyString(), + eq(""), + eq(999L) + ); + } +} \ No newline at end of file diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java new file mode 100644 index 000000000..ee49f4946 --- /dev/null +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java @@ -0,0 +1,219 @@ +//package org.wearefrank.ladybug.test.junit.createreport; +// +//import org.junit.Before; +//import org.junit.Test; +//import org.wearefrank.ladybug.*; +// +//import java.util.List; +// +//import static org.junit.Assert.*; +//import static org.mockito.Mockito.*; +// +//public class TestReportParenting { +// +// private Report report; +// private TestTool testTool; +// +// @Before +// public void setup() { +// report = new Report(); +// report.setCorrelationId("corr"); +// +// testTool = mock(TestTool.class); +// +// when(testTool.getMaxCheckpoints()).thenReturn(1000); +// when(testTool.getMaxMemoryUsage()).thenReturn(Long.MAX_VALUE); +// when(testTool.getMessageEncoder()).thenReturn(new MessageEncoder()); +// +// report.setTestTool(testTool); +// +// report.init(); +// } +// +// @Test +// public void testCheckpointStoresIdParentIdAndStartTime() { +// report.checkpoint( +// null, +// null, +// "parent", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "id-1", +// "", +// true, +// 123L +// ); +// +// Checkpoint checkpoint = report.getCheckpoints().get(0); +// +// assertEquals("id-1", checkpoint.getId()); +// assertNull(checkpoint.getParentId()); +// assertEquals(123L, checkpoint.getStartTime()); +// } +// +// @Test +// public void testChildCheckpointGetsNestedUnderParent() { +// report.checkpoint( +// null, +// null, +// "parent", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "parent-id", +// "", +// true, +// 100L +// ); +// +// report.checkpoint( +// null, +// null, +// "child", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "child-id", +// "parent-id", +// true, +// 200L +// ); +// +// List checkpoints = report.getCheckpoints(); +// +// assertEquals(2, checkpoints.size()); +// +// Checkpoint parent = checkpoints.get(0); +// Checkpoint child = checkpoints.get(1); +// +// assertEquals(0, parent.getLevel()); +// assertEquals(1, child.getLevel()); +// +// assertEquals("parent-id", child.getParentId()); +// } +// +// @Test +// public void testOrphanGetsReparentedLater() { +// report.checkpoint( +// null, +// null, +// "orphan", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "child-id", +// "parent-id", +// true, +// 200L +// ); +// +// Checkpoint orphan = report.getCheckpoints().get(0); +// +// assertEquals(0, orphan.getLevel()); +// +// report.checkpoint( +// null, +// null, +// "parent", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "parent-id", +// "", +// true, +// 100L +// ); +// +// List checkpoints = report.getCheckpoints(); +// +// Checkpoint parent = checkpoints.get(0); +// Checkpoint child = checkpoints.get(1); +// +// assertEquals("parent", parent.getName()); +// assertEquals("orphan", child.getName()); +// +// assertEquals(0, parent.getLevel()); +// assertEquals(1, child.getLevel()); +// } +// +// @Test +// public void testChildrenOrderedByStartTime() { +// report.checkpoint( +// null, +// null, +// "parent", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "parent-id", +// "", +// true, +// 100L +// ); +// +// report.checkpoint( +// null, +// null, +// "child-2", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "child-2-id", +// "parent-id", +// true, +// 300L +// ); +// +// report.checkpoint( +// null, +// null, +// "child-1", +// "message", +// null, +// null, +// null, +// null, +// CheckpointType.STARTPOINT.toInt(), +// 1, +// "child-1-id", +// "parent-id", +// true, +// 200L +// ); +// +// List checkpoints = report.getCheckpoints(); +// +// assertEquals("parent", checkpoints.get(0).getName()); +// assertEquals("child-1", checkpoints.get(1).getName()); +// assertEquals("child-2", checkpoints.get(2).getName()); +// } +//} \ No newline at end of file From 533528bb7a3694b116d0e58db7474493355f63af Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 14:16:33 +0200 Subject: [PATCH 38/47] Tests. --- .../createreport/TestReportParenting.java | 459 +++++++++--------- 1 file changed, 240 insertions(+), 219 deletions(-) diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java index ee49f4946..5559d7207 100644 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java @@ -1,219 +1,240 @@ -//package org.wearefrank.ladybug.test.junit.createreport; -// -//import org.junit.Before; -//import org.junit.Test; -//import org.wearefrank.ladybug.*; -// -//import java.util.List; -// -//import static org.junit.Assert.*; -//import static org.mockito.Mockito.*; -// -//public class TestReportParenting { -// -// private Report report; -// private TestTool testTool; -// -// @Before -// public void setup() { -// report = new Report(); -// report.setCorrelationId("corr"); -// -// testTool = mock(TestTool.class); -// -// when(testTool.getMaxCheckpoints()).thenReturn(1000); -// when(testTool.getMaxMemoryUsage()).thenReturn(Long.MAX_VALUE); -// when(testTool.getMessageEncoder()).thenReturn(new MessageEncoder()); -// -// report.setTestTool(testTool); -// -// report.init(); -// } -// -// @Test -// public void testCheckpointStoresIdParentIdAndStartTime() { -// report.checkpoint( -// null, -// null, -// "parent", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "id-1", -// "", -// true, -// 123L -// ); -// -// Checkpoint checkpoint = report.getCheckpoints().get(0); -// -// assertEquals("id-1", checkpoint.getId()); -// assertNull(checkpoint.getParentId()); -// assertEquals(123L, checkpoint.getStartTime()); -// } -// -// @Test -// public void testChildCheckpointGetsNestedUnderParent() { -// report.checkpoint( -// null, -// null, -// "parent", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "parent-id", -// "", -// true, -// 100L -// ); -// -// report.checkpoint( -// null, -// null, -// "child", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "child-id", -// "parent-id", -// true, -// 200L -// ); -// -// List checkpoints = report.getCheckpoints(); -// -// assertEquals(2, checkpoints.size()); -// -// Checkpoint parent = checkpoints.get(0); -// Checkpoint child = checkpoints.get(1); -// -// assertEquals(0, parent.getLevel()); -// assertEquals(1, child.getLevel()); -// -// assertEquals("parent-id", child.getParentId()); -// } -// -// @Test -// public void testOrphanGetsReparentedLater() { -// report.checkpoint( -// null, -// null, -// "orphan", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "child-id", -// "parent-id", -// true, -// 200L -// ); -// -// Checkpoint orphan = report.getCheckpoints().get(0); -// -// assertEquals(0, orphan.getLevel()); -// -// report.checkpoint( -// null, -// null, -// "parent", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "parent-id", -// "", -// true, -// 100L -// ); -// -// List checkpoints = report.getCheckpoints(); -// -// Checkpoint parent = checkpoints.get(0); -// Checkpoint child = checkpoints.get(1); -// -// assertEquals("parent", parent.getName()); -// assertEquals("orphan", child.getName()); -// -// assertEquals(0, parent.getLevel()); -// assertEquals(1, child.getLevel()); -// } -// -// @Test -// public void testChildrenOrderedByStartTime() { -// report.checkpoint( -// null, -// null, -// "parent", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "parent-id", -// "", -// true, -// 100L -// ); -// -// report.checkpoint( -// null, -// null, -// "child-2", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "child-2-id", -// "parent-id", -// true, -// 300L -// ); -// -// report.checkpoint( -// null, -// null, -// "child-1", -// "message", -// null, -// null, -// null, -// null, -// CheckpointType.STARTPOINT.toInt(), -// 1, -// "child-1-id", -// "parent-id", -// true, -// 200L -// ); -// -// List checkpoints = report.getCheckpoints(); -// -// assertEquals("parent", checkpoints.get(0).getName()); -// assertEquals("child-1", checkpoints.get(1).getName()); -// assertEquals("child-2", checkpoints.get(2).getName()); -// } -//} \ No newline at end of file +package org.wearefrank.ladybug.test.junit.createreport; + +import org.junit.Before; +import org.junit.Test; +import org.wearefrank.ladybug.*; +import org.wearefrank.ladybug.test.junit.ReportRelatedTestCase; + +import static org.junit.Assert.*; + +public class TestReportParenting extends ReportRelatedTestCase { + private TestableReport report; + + private static class TestableReport extends Report { + public T callCheckpoint( + String childThreadId, + String sourceClassName, + String name, + T message, + java.util.Map messageContext, + StubableCode stubableCode, + StubableCodeThrowsException stubableCodeThrowsException, + java.util.Set matchingStubStrategies, + int checkpointType, + int levelChangeNextCheckpoint, + String id, + String parentId, + boolean findParent, + long startTime + ) { + return checkpoint( + childThreadId, + sourceClassName, + name, + message, + messageContext, + stubableCode, + stubableCodeThrowsException, + matchingStubStrategies, + checkpointType, + levelChangeNextCheckpoint, + id, + parentId, + findParent, + startTime + ); + } + + public void initialize() { + init(); + } + } + + @Before + public void setup() { + super.setUp(); + + report = new TestableReport(); + report.setCorrelationId("corr"); + report.setTestTool(testTool); + + report.initialize(); + } + + @Test + public void testCheckpointStoresIdParentIdAndStartTime() { + report.callCheckpoint( + null, + null, + "parent", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "id-1", + "", + true, + 123L + ); + + Checkpoint checkpoint = report.getCheckpoints().get(0); + + assertEquals("id-1", checkpoint.getId()); + assertNull(checkpoint.getParentId()); + assertEquals(123L, checkpoint.getStartTime()); + } + + @Test + public void testChildCheckpointGetsNestedUnderParent() { + report.callCheckpoint( + null, + null, + "parent", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "parent-id", + "", + true, + 100L + ); + + report.callCheckpoint( + null, + null, + "child", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "child-id", + "parent-id", + true, + 200L + ); + + Checkpoint parent = report.getCheckpoints().get(0); + Checkpoint child = report.getCheckpoints().get(1); + + assertEquals(0, parent.getLevel()); + assertEquals(1, child.getLevel()); + + assertEquals("parent-id", child.getParentId()); + } + + @Test + public void testOrphanGetsReparentedLater() { + report.callCheckpoint( + null, + null, + "orphan", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "child-id", + "parent-id", + true, + 200L + ); + + assertEquals(0, report.getCheckpoints().get(0).getLevel()); + + report.callCheckpoint( + null, + null, + "parent", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "parent-id", + "", + true, + 100L + ); + + Checkpoint parent = report.getCheckpoints().get(0); + Checkpoint child = report.getCheckpoints().get(1); + + assertEquals("parent", parent.getName()); + assertEquals("orphan", child.getName()); + + assertEquals(0, parent.getLevel()); + assertEquals(1, child.getLevel()); + } + + @Test + public void testChildrenOrderedByStartTime() { + report.callCheckpoint( + null, + null, + "parent", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "parent-id", + "", + true, + 100L + ); + + report.callCheckpoint( + null, + null, + "child-2", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "child-2-id", + "parent-id", + true, + 300L + ); + + report.callCheckpoint( + null, + null, + "child-1", + "message", + null, + null, + null, + null, + CheckpointType.STARTPOINT.toInt(), + 1, + "child-1-id", + "parent-id", + true, + 200L + ); + + assertEquals("parent", report.getCheckpoints().get(0).getName()); + assertEquals("child-1", report.getCheckpoints().get(1).getName()); + assertEquals("child-2", report.getCheckpoints().get(2).getName()); + } +} \ No newline at end of file From 52538e724ddb9fb23e12e420f19c03bf8c6692ad Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 26 May 2026 14:34:29 +0200 Subject: [PATCH 39/47] Fix. --- ladybug-common/pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index e0a4fd03c..c574ea7c7 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -145,6 +145,18 @@ caffeine 3.2.3 + + org.mockito + mockito-core + 5.18.0 + test + + + org.mockito + mockito-junit-jupiter + 5.18.0 + test + From f1202b9f28563022afbdd0ef161556a5e7d235ff Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 27 May 2026 13:24:53 +0200 Subject: [PATCH 40/47] Add info points. --- .../java/org/wearefrank/ladybug/Report.java | 46 ++++++++++++++++--- .../java/org/wearefrank/ladybug/TestTool.java | 14 ++++-- .../ladybug/web/common/TracingApiImpl.java | 7 +++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 17e9aa033..439fdd291 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -520,8 +520,11 @@ private T addCheckpoint(String threadName, String sourceClassName, String n level = 0; index = 0; } else if (checkpointType == CheckpointType.ENDPOINT.toInt()) { - level = 0; + level = 1; index = checkpoints.size(); + } else if (checkpointType == CheckpointType.INFOPOINT.toInt()) { + level = 1; + index = 1; } } else if (!parentId.isEmpty()) { if (checkpointType == CheckpointType.STARTPOINT.toInt()) { @@ -589,7 +592,7 @@ private T addCheckpoint(String threadName, String sourceClassName, String n Checkpoint matchingStartpoint = null; for (Checkpoint checkpoint : checkpoints) { - if (Objects.equals(checkpoint.getId(), id)) { + if (Objects.equals(checkpoint.getId(), id) && checkpoint.getType() == CheckpointType.STARTPOINT.toInt()) { matchingStartpoint = checkpoint; break; } @@ -606,8 +609,25 @@ private T addCheckpoint(String threadName, String sourceClassName, String n index++; } } - } + } else if (checkpointType == CheckpointType.INFOPOINT.toInt()) { + Checkpoint parentCheckpoint = null; + for (Checkpoint checkpoint : checkpoints) { + if (Objects.equals(checkpoint.getId(), parentId) + && checkpoint.getType() == CheckpointType.STARTPOINT.toInt()) { + parentCheckpoint = checkpoint; + break; + } + } + + if (parentCheckpoint != null) { + level = parentCheckpoint.getLevel() + 1; + index = checkpoints.indexOf(parentCheckpoint) + 1; + } else { + level = 0; + index = checkpoints.size(); + } + } } } @@ -776,11 +796,23 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { int parentIndex = checkpoints.indexOf(newParent); - int insertIndex = parentIndex + 1; + int insertIndex; + insertIndex = parentIndex + 1; + while (insertIndex < checkpoints.size()) { + Checkpoint current = checkpoints.get(insertIndex); + + if (current.getLevel() == newParent.getLevel() + && current.getType() == CheckpointType.ENDPOINT.toInt()) { + break; + } + + if (current.getLevel() == newParent.getLevel() + 1 + && current.getType() == CheckpointType.INFOPOINT.toInt()) { + insertIndex++; + continue; + } - while (insertIndex < checkpoints.size() - && checkpoints.get(insertIndex).getLevel() > newParent.getLevel()) { - insertIndex++; + break; } int levelDelta = (newParent.getLevel() + 1) - orphanRoot.getLevel(); diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 342e9c24a..25c758617 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -335,7 +335,7 @@ public Tracer getOpenTelemetryTracer() { return tracer; } - public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { return checkpoint(correlationId, childThreadId, sourceClassName, name, @@ -343,7 +343,7 @@ public T checkpoint(String correlationId, String childThreadId, String sourc matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null, -1); } - public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint) { return checkpoint(correlationId, childThreadId, sourceClassName, name, @@ -351,7 +351,7 @@ public T checkpoint(String correlationId, String childThreadId, String sourc matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, null, null, -1); } - public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId) { return checkpoint(correlationId, childThreadId, sourceClassName, name, @@ -359,7 +359,7 @@ public T checkpoint(String correlationId, String childThreadId, String sourc matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, -1); } - public T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, + private T checkpoint(String correlationId, String childThreadId, String sourceClassName, String name, T message, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { return checkpoint(correlationId, childThreadId, sourceClassName, name, @@ -882,11 +882,17 @@ public T infopoint(String correlationId, String sourceClassName, String name return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, CheckpointType.INFOPOINT.toInt(), 0); } + public T infopoint(String correlationId, String sourceClassName, String name, T message, Map messageContext) { return checkpoint(correlationId, null, sourceClassName, name, message, messageContext, null, null, null, CheckpointType.INFOPOINT.toInt(), 0); } + public T infopoint(String correlationId, String sourceClassName, String name, T message, String id, String parentId) { + return checkpoint(correlationId, null, sourceClassName, name, message, null, null, null, + CheckpointType.INFOPOINT.toInt(), 0, id, parentId); + } + /** * Use abortpoint instead of endpoint in case an exception is thrown after a startpoint. The exception object can * be passed as the message parameter. diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index ab2509cd3..2c83851a0 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -16,6 +16,8 @@ package org.wearefrank.ladybug.web.common; import com.google.protobuf.ByteString; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; import lombok.Setter; import org.apache.commons.codec.binary.Hex; import org.slf4j.Logger; @@ -45,6 +47,11 @@ public void processSpans(List spans) { long startTime = span.getStartTimeUnixNano(); testTool.startpoint(traceId, null, span.getName(), toHashMap(span), spanId, parentSpanId, startTime); + for (KeyValue keyValue : span.getAttributesList()) { + AnyValue anyValue = keyValue.getValue(); + String value = String.valueOf(anyValue.getField(anyValue.getDescriptorForType().findFieldByNumber(anyValue.getValueCase().getNumber()))); + testTool.infopoint(byteStringToHex(span.getTraceId()), null, (span.getName() + " " + keyValue.getKey()), value, spanId, spanId); + } testTool.endpoint(traceId, null, span.getName(), null, spanId, parentSpanId, startTime); } testTool.close(traceId); From 557bc0980f8c87e6cd353ac27c7f3ed4b79c72fa Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 27 May 2026 13:59:10 +0200 Subject: [PATCH 41/47] Fix. --- .../java/org/wearefrank/ladybug/Report.java | 23 +++++++++++-------- .../ladybug/web/common/TracingApiImpl.java | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 439fdd291..99fd6279d 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -796,8 +796,8 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { int parentIndex = checkpoints.indexOf(newParent); - int insertIndex; - insertIndex = parentIndex + 1; + int insertIndex = parentIndex + 1; + while (insertIndex < checkpoints.size()) { Checkpoint current = checkpoints.get(insertIndex); @@ -806,13 +806,7 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { break; } - if (current.getLevel() == newParent.getLevel() + 1 - && current.getType() == CheckpointType.INFOPOINT.toInt()) { - insertIndex++; - continue; - } - - break; + insertIndex++; } int levelDelta = (newParent.getLevel() + 1) - orphanRoot.getLevel(); @@ -822,6 +816,17 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { } checkpoints.addAll(insertIndex, subtree); + + for (int i = insertIndex; i < checkpoints.size(); i++) { + Checkpoint cp = checkpoints.get(i); + + if (Objects.equals(cp.getId(), orphanRoot.getId()) + && cp.getType() == CheckpointType.ENDPOINT.toInt()) { + + cp.setLevel(newParent.getLevel() + 2); + break; + } + } } public String getThreadInfo() { diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 2c83851a0..509abf74f 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -50,7 +50,7 @@ public void processSpans(List spans) { for (KeyValue keyValue : span.getAttributesList()) { AnyValue anyValue = keyValue.getValue(); String value = String.valueOf(anyValue.getField(anyValue.getDescriptorForType().findFieldByNumber(anyValue.getValueCase().getNumber()))); - testTool.infopoint(byteStringToHex(span.getTraceId()), null, (span.getName() + " " + keyValue.getKey()), value, spanId, spanId); + testTool.infopoint(byteStringToHex(span.getTraceId()), null, keyValue.getKey(), value, spanId, spanId); } testTool.endpoint(traceId, null, span.getName(), null, spanId, parentSpanId, startTime); } From 0648b10fe65e6349d9cb8bf18a22f138962c8049 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 27 May 2026 14:58:37 +0200 Subject: [PATCH 42/47] Fix. --- .../main/java/org/wearefrank/ladybug/Report.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 99fd6279d..67f7c489e 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -801,11 +801,13 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { while (insertIndex < checkpoints.size()) { Checkpoint current = checkpoints.get(insertIndex); + if (current.getLevel() < newParent.getLevel()) { + break; + } if (current.getLevel() == newParent.getLevel() && current.getType() == CheckpointType.ENDPOINT.toInt()) { break; } - insertIndex++; } @@ -816,17 +818,6 @@ private void moveSubtree(Checkpoint newParent, Checkpoint orphanRoot) { } checkpoints.addAll(insertIndex, subtree); - - for (int i = insertIndex; i < checkpoints.size(); i++) { - Checkpoint cp = checkpoints.get(i); - - if (Objects.equals(cp.getId(), orphanRoot.getId()) - && cp.getType() == CheckpointType.ENDPOINT.toInt()) { - - cp.setLevel(newParent.getLevel() + 2); - break; - } - } } public String getThreadInfo() { From 584721b05b7779669bf6a417db9fecffac6e60c4 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 27 May 2026 15:14:50 +0200 Subject: [PATCH 43/47] Fix. --- .../src/main/java/org/wearefrank/ladybug/Report.java | 12 ++++++------ .../main/java/org/wearefrank/ladybug/TestTool.java | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java index 67f7c489e..142a4b008 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/Report.java @@ -326,7 +326,7 @@ public void restoreRuntimeState() { protected T checkpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent, long startTime) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { if (checkpointType == CheckpointType.THREAD_CREATEPOINT.toInt()) { String parentThreadName = Thread.currentThread().getName(); if (!threads.contains(parentThreadName)) { @@ -374,7 +374,7 @@ protected T checkpoint(String childThreadId, String sourceClassName, String } message = addCheckpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, - matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, findParent, startTime); + matchingStubStrategies, checkpointType, levelChangeNextCheckpoint, id, parentId, startTime); return message; } @@ -431,7 +431,7 @@ private void removeThreadCreatepoint(int index, String childThreadId) { private T addCheckpoint(String childThreadId, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, boolean findParent, long startTime) { + Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { String threadName = Thread.currentThread().getName(); Integer index = threadCheckpointIndex.get(threadName); Integer level = threadLevel.get(threadName); @@ -497,7 +497,7 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin } } else { message = addCheckpoint(threadName, sourceClassName, name, message, messageContext, stubableCode, - stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level, id, parentId, findParent, startTime); + stubableCodeThrowsException, matchingStubStrategies, checkpointType, index, level, id, parentId, startTime); } Integer newLevel = level + levelChangeNextCheckpoint; threadLevel.put(threadName, newLevel); @@ -513,8 +513,8 @@ private T addCheckpoint(String childThreadId, String sourceClassName, Strin @SneakyThrows private T addCheckpoint(String threadName, String sourceClassName, String name, T message, Map messageContext, StubableCode stubableCode, StubableCodeThrowsException stubableCodeThrowsException, - Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String id, String parentId, boolean findParent, long startTime) { - if (findParent && parentId != null) { + Set matchingStubStrategies, int checkpointType, Integer index, Integer level, String id, String parentId, long startTime) { + if (beingUpdated && parentId != null) { if (parentId.isEmpty()) { if (checkpointType == CheckpointType.STARTPOINT.toInt()) { level = 0; diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 25c758617..6a8f159bf 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -433,7 +433,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour executeStubableCode = false; message = report.checkpoint(childThreadId, sourceClassName, name, message, messageContext, stubableCode, stubableCodeThrowsException, matchingStubStrategies, checkpointType, - levelChangeNextCheckpoint, id, parentId, report.isBeingUpdated(), startTime); + levelChangeNextCheckpoint, id, parentId, startTime); closeReportIfFinished(report); } report = null; From af958bc4405e5521f257943e8d255a62314db296 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Wed, 27 May 2026 15:42:31 +0200 Subject: [PATCH 44/47] Fix. --- .../main/java/org/wearefrank/ladybug/TestTool.java | 12 +++++++++++- .../ladybug/web/common/TracingApiImpl.java | 2 ++ .../test/junit/createreport/TestReportParenting.java | 11 +---------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index 6a8f159bf..230c06fdc 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -104,6 +104,7 @@ public class TestTool { boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() private String openTelemetryEndpoint; private Tracer tracer; + private boolean updateReportsEnabled = false; @PostConstruct public void init() { @@ -117,6 +118,14 @@ public void reset() { reportGeneratorEnabled = defaultReportGeneratorEnabled; } + public void setUpdateReportsEnabled(boolean updateReportsEnabled) { + this.updateReportsEnabled = updateReportsEnabled; + } + + public boolean isUpdateReportsEnabled() { + return updateReportsEnabled; + } + public void setOpenTelemetryEndpoint(String openTelemetryEndpoint) { this.openTelemetryEndpoint = openTelemetryEndpoint; } @@ -378,7 +387,7 @@ private T checkpoint(String correlationId, String childThreadId, String sour synchronized(reportsInProgress) { report = getReportInProgress(correlationId); if (report == null) { - if (debugStorage.isCrudStorage()) { + if (updateReportsEnabled && debugStorage.isCrudStorage()) { try { for (Integer storageId : debugStorage.getStorageIds()) { if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { @@ -531,6 +540,7 @@ protected void closeReportIfFinished(Report report) { if (debugStorage.isCrudStorage()) { try { ((CrudStorage) debugStorage).update(report); + report.setBeingUpdated(false); } catch (StorageException e) { log.error("Failed to store report", e); } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 509abf74f..e0e94ca5e 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -38,6 +38,8 @@ public class TracingApiImpl { private @Setter TestTool testTool; public void processSpans(List spans) { + testTool.setUpdateReportsEnabled(true); + String traceId = byteStringToHex(spans.get(0).getTraceId()); for (Span span : spans) { diff --git a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java index 5559d7207..9a77302c9 100644 --- a/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java +++ b/ladybug-common/src/test/java/org/wearefrank/ladybug/test/junit/createreport/TestReportParenting.java @@ -24,7 +24,6 @@ public T callCheckpoint( int levelChangeNextCheckpoint, String id, String parentId, - boolean findParent, long startTime ) { return checkpoint( @@ -40,7 +39,6 @@ public T callCheckpoint( levelChangeNextCheckpoint, id, parentId, - findParent, startTime ); } @@ -56,6 +54,7 @@ public void setup() { report = new TestableReport(); report.setCorrelationId("corr"); + report.setBeingUpdated(true); report.setTestTool(testTool); report.initialize(); @@ -76,7 +75,6 @@ public void testCheckpointStoresIdParentIdAndStartTime() { 1, "id-1", "", - true, 123L ); @@ -102,7 +100,6 @@ public void testChildCheckpointGetsNestedUnderParent() { 1, "parent-id", "", - true, 100L ); @@ -119,7 +116,6 @@ public void testChildCheckpointGetsNestedUnderParent() { 1, "child-id", "parent-id", - true, 200L ); @@ -147,7 +143,6 @@ public void testOrphanGetsReparentedLater() { 1, "child-id", "parent-id", - true, 200L ); @@ -166,7 +161,6 @@ public void testOrphanGetsReparentedLater() { 1, "parent-id", "", - true, 100L ); @@ -195,7 +189,6 @@ public void testChildrenOrderedByStartTime() { 1, "parent-id", "", - true, 100L ); @@ -212,7 +205,6 @@ public void testChildrenOrderedByStartTime() { 1, "child-2-id", "parent-id", - true, 300L ); @@ -229,7 +221,6 @@ public void testChildrenOrderedByStartTime() { 1, "child-1-id", "parent-id", - true, 200L ); From dea73d4c47a80189c6af0e9fa6c6711946677e80 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 2 Jun 2026 13:21:32 +0200 Subject: [PATCH 45/47] Fixes. --- .../ladybug/web/jaxrs/api/TracingApi.java | 52 ++++++-------- .../ladybug/web/springmvc/api/TracingApi.java | 52 +++++--------- ladybug-common/pom.xml | 12 ++++ .../java/org/wearefrank/ladybug/TestTool.java | 8 +-- .../ladybug/web/common/TracingApiImpl.java | 69 +++++++++++++------ 5 files changed, 101 insertions(+), 92 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index 59df2403d..e7f2612a0 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -17,10 +17,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; -import io.opentelemetry.proto.trace.v1.ResourceSpans; -import io.opentelemetry.proto.trace.v1.ScopeSpans; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -37,39 +34,30 @@ public class TracingApi extends ApiBase { @POST @Consumes({"application/x-protobuf", "application/json"}) - public Response receiveSpans(@HeaderParam("Content-Type") String contentType, byte[] data) throws InvalidProtocolBufferException { - ExportTraceServiceRequest request; - - if (contentType != null && contentType.startsWith("application/x-protobuf")) { - request = ExportTraceServiceRequest.parseFrom(data); - } else if (contentType != null && contentType.startsWith("application/json")) { - String json = new String(data); - ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); - JsonFormat.parser().merge(json, builder); - request = builder.build(); - } else { - ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + public Response receiveSpans(@HeaderParam("Content-Type") String contentType, byte[] data) { + if (!contentType.startsWith("application/x-protobuf") && !contentType.startsWith("application/json")) { return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE) .type(MediaType.APPLICATION_JSON) - .entity(JsonFormat.printer().print(response)) - .build(); - } - - for (ResourceSpans resourceSpans : request.getResourceSpansList()) { - for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { - delegate.processSpans(scopeSpans.getSpansList()); - } - } - - ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); - if (contentType.startsWith("application/x-protobuf")) { - return Response.ok(response.toByteArray()) - .type("application/x-protobuf") .build(); } else { - return Response.ok(JsonFormat.printer().print(response)) - .type(MediaType.APPLICATION_JSON) - .build(); + try { + delegate.processSpans(contentType, data); + + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + if (contentType.startsWith("application/x-protobuf")) { + return Response.ok(response.toByteArray()) + .type("application/x-protobuf") + .build(); + } else { + return Response.ok(JsonFormat.printer().print(response)) + .type(MediaType.APPLICATION_JSON) + .build(); + } + } catch (InvalidProtocolBufferException e) { + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON) + .build(); + } } } } diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java index 240306458..7c0412155 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -17,10 +17,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; -import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; -import io.opentelemetry.proto.trace.v1.ResourceSpans; -import io.opentelemetry.proto.trace.v1.ScopeSpans; import jakarta.annotation.security.RolesAllowed; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; @@ -38,39 +35,26 @@ public class TracingApi { private @Setter TracingApiImpl delegate; @PostMapping(consumes = {"application/x-protobuf", MediaType.APPLICATION_JSON_VALUE}) - public ResponseEntity receiveSpans(@RequestHeader("Content-Type") String contentType, @RequestBody byte[] data) throws InvalidProtocolBufferException { - ExportTraceServiceRequest request; - - if (contentType != null && contentType.startsWith("application/x-protobuf")) { - request = ExportTraceServiceRequest.parseFrom(data); - } else if (contentType != null && contentType.startsWith(MediaType.APPLICATION_JSON_VALUE)) { - String json = new String(data); - ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); - JsonFormat.parser().merge(json, builder); - request = builder.build(); - + public ResponseEntity receiveSpans(@RequestHeader("Content-Type") String contentType, @RequestBody byte[] data) { + if (!contentType.startsWith("application/x-protobuf") && !contentType.startsWith("application/json")) { + return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).build(); } else { - ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); - return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE) - .contentType(MediaType.APPLICATION_JSON) - .body(JsonFormat.printer().print(response)); - } - - for (ResourceSpans resourceSpans : request.getResourceSpansList()) { - for (ScopeSpans scopeSpans : resourceSpans.getScopeSpansList()) { - delegate.processSpans(scopeSpans.getSpansList()); + try { + delegate.processSpans(contentType, data); + + ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); + if (contentType.startsWith("application/x-protobuf")) { + return ResponseEntity.ok() + .contentType(org.springframework.http.MediaType.parseMediaType("application/x-protobuf")) + .body(response.toByteArray()); + } else { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(JsonFormat.printer().print(response)); + } + } catch (InvalidProtocolBufferException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } } - - ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); - if (contentType.startsWith("application/x-protobuf")) { - return ResponseEntity.ok() - .contentType(org.springframework.http.MediaType.parseMediaType("application/x-protobuf")) - .body(response.toByteArray()); - } else { - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_JSON) - .body(JsonFormat.printer().print(response)); - } } } \ No newline at end of file diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index 1e08ae8d4..b430d5acf 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -140,6 +140,18 @@ commons-codec 1.17.0 + + com.google.protobuf + protobuf-java + 4.34.1 + compile + + + com.google.protobuf + protobuf-java-util + 4.34.1 + compile + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index efe129b15..6565f2d2b 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -102,7 +102,7 @@ public class TestTool { private @Setter @Getter @Inject @Autowired Views views; private @Setter @Getter int reportsInProgressThreshold = 300000; boolean devMode = false; // See testConcurrentLastEndpointAndFirstStartpointForSameCorrelationId() - private String openTelemetryEndpoint; + private @Setter String openTelemetryEndpoint; private Tracer tracer; @PostConstruct @@ -117,11 +117,7 @@ public void reset() { reportGeneratorEnabled = defaultReportGeneratorEnabled; } - public void setOpenTelemetryEndpoint(String openTelemetryEndpoint) { - this.openTelemetryEndpoint = openTelemetryEndpoint; - } - - public void setSecurityLoggerName(String securityLoggerName) { + public void setSecurityLoggerName(String securityLoggerName) { securityLog = LoggerFactory.getLogger(securityLoggerName); } diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 0bb1e97bf..9678e54ee 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -16,6 +16,11 @@ package org.wearefrank.ladybug.web.common; import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; import lombok.Setter; import org.slf4j.Logger; @@ -37,30 +42,54 @@ public class TracingApiImpl { @Autowired private @Setter TestTool testTool; - public void processSpans(List trace) { - ArrayList parentIds = new ArrayList<>(); - for (Span span: trace) { - if (!span.getParentSpanId().isEmpty() && !parentIds.contains(span.getParentSpanId())) { - parentIds.add(span.getParentSpanId()); - } - } - ArrayList endpoints = new ArrayList<>(); - for (int i = trace.size() - 1; i >= 0; i--) { - if (trace.get(i).getParentSpanId().isEmpty()) { - testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); - endpoints.add(trace.get(i).getName()); - } else { - if (parentIds.contains(trace.get(i).getSpanId())) { - testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); - endpoints.add(trace.get(i).getName()); - } else { - testTool.infopoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); + public void processSpans(String contentType, byte[] data) throws InvalidProtocolBufferException { + List resourceSpans = parseData(contentType, data); + + for (ResourceSpans resourceSpan : resourceSpans) { + for (ScopeSpans scopeSpan : resourceSpan.getScopeSpansList()) { + List trace = scopeSpan.getSpansList(); + + ArrayList parentIds = new ArrayList<>(); + for (Span span: trace) { + if (!span.getParentSpanId().isEmpty() && !parentIds.contains(span.getParentSpanId())) { + parentIds.add(span.getParentSpanId()); + } + } + ArrayList endpoints = new ArrayList<>(); + for (int i = trace.size() - 1; i >= 0; i--) { + if (trace.get(i).getParentSpanId().isEmpty()) { + testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); + endpoints.add(trace.get(i).getName()); + } else { + if (parentIds.contains(trace.get(i).getSpanId())) { + testTool.startpoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); + endpoints.add(trace.get(i).getName()); + } else { + testTool.infopoint(byteStringToHex(trace.get(i).getTraceId()), null, trace.get(i).getName(), toHashMap(trace.get(i)).toString()); + } + } } + for (int i = endpoints.size() - 1; i >= 0; i--) { + testTool.endpoint(byteStringToHex(trace.get(0).getTraceId()), null, endpoints.get(i), "Endpoint"); + } + } } - for (int i = endpoints.size() - 1; i >= 0; i--) { - testTool.endpoint(byteStringToHex(trace.get(0).getTraceId()), null, endpoints.get(i), "Endpoint"); + } + + public List parseData(String contentType, byte[] data) throws InvalidProtocolBufferException { + ExportTraceServiceRequest request = null; + + if (contentType != null && contentType.startsWith("application/x-protobuf")) { + request = ExportTraceServiceRequest.parseFrom(data); + } else if (contentType != null && contentType.startsWith("application/json")) { + String json = new String(data); + ExportTraceServiceRequest.Builder builder = ExportTraceServiceRequest.newBuilder(); + JsonFormat.parser().merge(json, builder); + request = builder.build(); } + + return request.getResourceSpansList(); } public String byteStringToHex(ByteString byteString) { From 87162a0691d233ee9aaa5c6cddee3944d4d6235b Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Tue, 2 Jun 2026 15:07:46 +0200 Subject: [PATCH 46/47] Fix. --- .../ladybug/web/jaxrs/api/TracingApi.java | 2 +- .../ladybug/web/springmvc/api/TracingApi.java | 2 +- ladybug-common/pom.xml | 24 +++++++++---------- .../ladybug/web/common/TracingApiImpl.java | 16 +++++++++++-- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java index e7f2612a0..45b4bc703 100644 --- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java +++ b/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/TracingApi.java @@ -41,7 +41,7 @@ public Response receiveSpans(@HeaderParam("Content-Type") String contentType, by .build(); } else { try { - delegate.processSpans(contentType, data); + delegate.addSpansToBuffer(contentType, data); ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); if (contentType.startsWith("application/x-protobuf")) { diff --git a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java index 7c0412155..42e4ceab1 100644 --- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java +++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java @@ -40,7 +40,7 @@ public ResponseEntity receiveSpans(@RequestHeader("Content-Type") String cont return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).build(); } else { try { - delegate.processSpans(contentType, data); + delegate.addSpansToBuffer(contentType, data); ExportTraceServiceResponse response = ExportTraceServiceResponse.newBuilder().build(); if (contentType.startsWith("application/x-protobuf")) { diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml index 91499acf6..9e350ba1c 100644 --- a/ladybug-common/pom.xml +++ b/ladybug-common/pom.xml @@ -145,18 +145,6 @@ caffeine 3.2.3 - - org.mockito - mockito-core - 5.18.0 - test - - - org.mockito - mockito-junit-jupiter - 5.18.0 - test - com.google.protobuf protobuf-java @@ -169,6 +157,18 @@ 4.34.1 compile + + org.mockito + mockito-core + 5.18.0 + test + + + org.mockito + mockito-junit-jupiter + 5.18.0 + test + diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java index 4520eace1..4b68e8a0c 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/web/common/TracingApiImpl.java @@ -30,17 +30,17 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import io.opentelemetry.proto.trace.v1.Span; +import org.wearefrank.ladybug.SpanBuffer; import org.wearefrank.ladybug.TestTool; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; @Component public class TracingApiImpl { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private SpanBuffer spanBuffer = new SpanBuffer(this); @Autowired private @Setter TestTool testTool; @@ -67,6 +67,18 @@ public void processSpans(List spans) { testTool.close(traceId); } + public void addSpansToBuffer(String contentType, byte[] data) throws InvalidProtocolBufferException { + List resourceSpans = parseData(contentType, data); + + for (ResourceSpans resourceSpan : resourceSpans) { + for (ScopeSpans scopeSpans : resourceSpan.getScopeSpansList()) { + for (Span span : scopeSpans.getSpansList()) { + spanBuffer.addSpan(span); + } + } + } + } + public List parseData(String contentType, byte[] data) throws InvalidProtocolBufferException { ExportTraceServiceRequest request = null; From 6a8604610144712f945fbda7d9f0b63485e974c3 Mon Sep 17 00:00:00 2001 From: FrancesTwisk Date: Mon, 8 Jun 2026 14:04:16 +0200 Subject: [PATCH 47/47] Simplify logic on TestTool. --- .../java/org/wearefrank/ladybug/TestTool.java | 79 +++++++++++-------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java index b2ff8b366..9baf06cb7 100644 --- a/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java +++ b/ladybug-common/src/main/java/org/wearefrank/ladybug/TestTool.java @@ -368,40 +368,8 @@ private T checkpoint(String correlationId, String childThreadId, String sour Set matchingStubStrategies, int checkpointType, int levelChangeNextCheckpoint, String id, String parentId, long startTime) { boolean executeStubableCode = true; if (reportGeneratorEnabled) { - Report report; - // Blocking for all threads for all reports - synchronized(reportsInProgress) { - report = getReportInProgress(correlationId); - if (report == null) { - if (updateReportsEnabled && debugStorage.isCrudStorage()) { - try { - for (Integer storageId : debugStorage.getStorageIds()) { - if (debugStorage.getReport(storageId).getCorrelationId().equals(correlationId)) { - report = debugStorage.getReport(storageId); - synchronized (report) { - report.restoreRuntimeState(); - report.setClosed(false); - report.setTestTool(this); - - reportsInProgress.add(0, report); - reportsInProgressByCorrelationId.put(correlationId, report); - numberOfReportsInProgress++; - - report.setBeingUpdated(true); - } - break; - } - } - } catch (StorageException e) { - log.error("Failed to find report in storage", e); - } - } + Report report = getOrCreateReport(correlationId, name, checkpointType); - if (report == null) { - report = createReport(correlationId, name, checkpointType); - } - } - } if (devMode) randomSleep(); while (report != null) { // "synchronized(report)" is only blocking for threads writing to the same report (which is only the @@ -440,6 +408,51 @@ private T checkpoint(String correlationId, String childThreadId, String sour return message; } + private Report getOrCreateReport(String correlationId, String name, int checkpointType) { + synchronized (reportsInProgress) { + Report report = getReportInProgress(correlationId); + if (report != null) { + return report; + } + + if (updateReportsEnabled) { + report = tryLoadReportFromStorage(correlationId); + if (report != null) { + return report; + } + } + + return createReport(correlationId, name, checkpointType); + } + } + + private Report tryLoadReportFromStorage(String correlationId) { + if (!debugStorage.isCrudStorage()) { + log.error("Can't update report because storage does not support CRUD operations"); + return null; + } + try { + for (Integer storageId : debugStorage.getStorageIds()) { + Report stored = debugStorage.getReport(storageId); + if (stored.getCorrelationId().equals(correlationId)) { + stored.restoreRuntimeState(); + stored.setClosed(false); + stored.setTestTool(this); + + reportsInProgress.add(0, stored); + reportsInProgressByCorrelationId.put(correlationId, stored); + numberOfReportsInProgress++; + + stored.setBeingUpdated(true); + return stored; + } + } + } catch (StorageException e) { + log.error("Failed to find report in storage", e); + } + return null; + } + private Report createReport(String correlationId, String name, int checkpointType) { Report report = null; if (checkpointType == CheckpointType.STARTPOINT.toInt()) {