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-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/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/CollectorApi.java
deleted file mode 100644
index 408df91f9..000000000
--- a/ladybug-backend-jaxrs/src/main/java/org/wearefrank/ladybug/web/jaxrs/api/CollectorApi.java
+++ /dev/null
@@ -1,45 +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 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;
-
-@Path("/" + Constants.LADYBUG_API_PATH + "/collector")
-public class CollectorApi extends ApiBase {
- @Autowired
- private @Setter CollectorApiImpl delegate;
-
- @POST
- public Response collectSpans(Span[] trace) {
- delegate.processSpans(trace);
- return Response.ok().build();
- }
-
- @POST
- @Consumes(MediaType.APPLICATION_JSON)
- public Response collectSpansJson(Span[] trace) {
- delegate.processSpans(trace);
- return Response.ok().build();
- }
-}
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..45b4bc703
--- /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.ExportTraceServiceResponse;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Autowired;
+
+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 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)
+ .build();
+ } else {
+ try {
+ delegate.addSpansToBuffer(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/pom.xml b/ladybug-backend-springmvc/pom.xml
index f05cb250f..ddd124d15 100644
--- a/ladybug-backend-springmvc/pom.xml
+++ b/ladybug-backend-springmvc/pom.xml
@@ -21,7 +21,6 @@
org.wearefrank
ladybug-common
-
org.springframework.security
spring-security-web
@@ -50,5 +49,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/CollectorApi.java b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java
deleted file mode 100644
index a2a252f77..000000000
--- a/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/CollectorApi.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- 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.
- 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..42e4ceab1
--- /dev/null
+++ b/ladybug-backend-springmvc/src/main/java/org/wearefrank/ladybug/web/springmvc/api/TracingApi.java
@@ -0,0 +1,60 @@
+/*
+ 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.
+ 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.ExportTraceServiceResponse;
+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.*;
+import org.wearefrank.ladybug.web.common.TracingApiImpl;
+
+@RestController
+@RequestMapping("/traces")
+@RolesAllowed("IbisWebService")
+public class TracingApi {
+ @Autowired
+ private @Setter TracingApiImpl delegate;
+
+ @PostMapping(consumes = {"application/x-protobuf", MediaType.APPLICATION_JSON_VALUE})
+ 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 {
+ try {
+ delegate.addSpansToBuffer(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();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ladybug-common/pom.xml b/ladybug-common/pom.xml
index d227cc6b2..9e350ba1c 100644
--- a/ladybug-common/pom.xml
+++ b/ladybug-common/pom.xml
@@ -129,6 +129,46 @@
io.opentelemetry
opentelemetry-exporter-otlp
+
+ io.opentelemetry.proto
+ opentelemetry-proto
+ 1.9.0-alpha
+ compile
+
+
+ commons-codec
+ commons-codec
+ 1.17.0
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+ 3.2.3
+
+
+ com.google.protobuf
+ protobuf-java
+ 4.34.1
+ compile
+
+
+ com.google.protobuf
+ protobuf-java-util
+ 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/Checkpoint.java b/ladybug-common/src/main/java/org/wearefrank/ladybug/Checkpoint.java
index 74198571d..3a21ecf43 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,9 @@ public class Checkpoint implements Serializable, Cloneable {
private transient ByteArrayOutputStream messageCapturerOutputStream;
private transient Map variablesPatternMap;
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.
@@ -95,6 +100,24 @@ 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; }
+
+ @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 3cbba3924..142a4b008 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;
@@ -134,6 +123,15 @@ public class Report implements Serializable {
private transient boolean logMaxMemoryUsage = true;
private transient Map
+
+
+
+