From 1956413dcfaf3230be0544cb9d567dd718819263 Mon Sep 17 00:00:00 2001 From: ricklue <101653572+ricklue@users.noreply.github.com> Date: Fri, 23 Aug 2024 18:07:19 +0200 Subject: [PATCH] Collect telemetry (#258) --- README.md | 16 +++- pom.xml | 6 ++ src/main/java/nl/nn/testtool/Span.java | 95 +++++++++++++++++++ .../testtool/web/ApiAuthorizationFilter.java | 6 ++ .../nl/nn/testtool/web/api/CollectorApi.java | 79 +++++++++++++++ src/main/resources/ladybug/cxf-beans.xml | 4 + 6 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nl/nn/testtool/Span.java create mode 100644 src/main/java/nl/nn/testtool/web/api/CollectorApi.java diff --git a/README.md b/README.md index 02e4ee80..96735324 100644 --- a/README.md +++ b/README.md @@ -243,4 +243,18 @@ What version number will be published by the Jenkins server? In the frontend `po You can also test your frontend code as Maven artifact before merging your code with the master branch. To do this, you need Maven 3.9.6 or later running on Java 17 or Java 21. Run `mvn clean install` in your checkout of the frontend. This will build the frontend artifact with exactly the version number in `pom.xml` (e.g. `0.1.0-SNAPSHOT`, no timestamp in this case) and install it on your development device. You can temporarily update the backend `pom.xml` to reference that frontend version. If you then start ladybug as described before, your work is reachable at port 80 (not 4200). You can test your work as explained in the sections on backend development. > [!WARNING] -> When you run the Maven build on your development device, it will update `package.json`. Please do not check in that change. Otherwise, the build will not work for other developers anymore. \ No newline at end of file +> When you run the Maven build on your development device, it will update `package.json`. Please do not check in that change. Otherwise, the build will not work for other developers anymore. + +Collecting OpenTelemetry data +============================= + +In Ladybug, there is also an API available to gather telemetry data from OpenTelemetry. When code is instrumented with the OpenTelemetry library, it is possible to use the endpoint from this API to gather it in Ladybug. For a manual OpenTelemetry-instrumentation, you can configure the Zipkin exporter and make use of the endpoint to Ladybug. See code example below: + +``` +SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() +.addSpanProcessor(BatchSpanProcessor.builder(ZipkinSpanExporter.builder().setEndpoint("http://localhost/ladybug/api/collector/").build()).build()) +.setResource(resource) +.build(); +``` + +For more info about OpenTelemetry, see https://opentelemetry.io/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3ff249f3..56a669ad 100644 --- a/pom.xml +++ b/pom.xml @@ -245,6 +245,12 @@ 1.0 provided + + + io.opentelemetry + opentelemetry-api + 1.39.0 + diff --git a/src/main/java/nl/nn/testtool/Span.java b/src/main/java/nl/nn/testtool/Span.java new file mode 100644 index 00000000..ba7d1556 --- /dev/null +++ b/src/main/java/nl/nn/testtool/Span.java @@ -0,0 +1,95 @@ +package nl.nn.testtool; + +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; + } +} diff --git a/src/main/java/nl/nn/testtool/web/ApiAuthorizationFilter.java b/src/main/java/nl/nn/testtool/web/ApiAuthorizationFilter.java index 15d1675e..22101f89 100644 --- a/src/main/java/nl/nn/testtool/web/ApiAuthorizationFilter.java +++ b/src/main/java/nl/nn/testtool/web/ApiAuthorizationFilter.java @@ -71,6 +71,7 @@ public ApiAuthorizationFilter() { setObserverRoles(null); setDataAdminRoles(null); setTesterRoles(null); + setWebServiceRoles(null); constructorDone = true; } @@ -115,6 +116,11 @@ public void setTesterRoles(List testerRoles) { addConfigurationPart("POST/" + ApiServlet.LADYBUG_API_PATH + "/runner/run/.*", testerRoles); } + public void setWebServiceRoles(List webServiceRoles) { + if (constructorDone) log.info("Set web service roles"); + addConfigurationPart("POST/" + ApiServlet.LADYBUG_API_PATH + "/collector/.*$", webServiceRoles); + } + public void setLadybugApiRoles(Map> ladybugApiRoles) { log.info("Set Ladybug api roles"); for (String path : ladybugApiRoles.keySet()) { diff --git a/src/main/java/nl/nn/testtool/web/api/CollectorApi.java b/src/main/java/nl/nn/testtool/web/api/CollectorApi.java new file mode 100644 index 00000000..c51c7d11 --- /dev/null +++ b/src/main/java/nl/nn/testtool/web/api/CollectorApi.java @@ -0,0 +1,79 @@ +/* + Copyright 2021-2024 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 nl.nn.testtool.web.api; + +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.Setter; +import nl.nn.testtool.Span; +import nl.nn.testtool.TestTool; +import nl.nn.testtool.web.ApiServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; + +@Path("/" + ApiServlet.LADYBUG_API_PATH + "/collector") +public class CollectorApi extends ApiBase { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private @Setter @Inject @Autowired TestTool testTool; + + @POST + @Path("/") + public Response collectSpans(Span[] trace) { + processSpans(trace); + + return Response.ok().build(); + } + + @POST + @Path("/") + @Consumes(MediaType.APPLICATION_JSON) + public Response collectSpansJson(Span[] trace) { + processSpans(trace); + + return Response.ok().build(); + } + + private void processSpans(Span[] trace) { + ArrayList parentIds = new ArrayList<>(); + for (Span span: trace) { + if (span.getParentId() != null && !parentIds.contains(span.getParentId())) { + parentIds.add(span.getParentId()); + } + } + 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()); + } 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()); + } else { + testTool.infopoint(trace[i].getTraceId(), null, trace[i].getName(), trace[i].toHashmap().toString()); + } + } + } + for (int i = endpoints.size() - 1; i >= 0; i--) { + testTool.endpoint(trace[0].getTraceId(), null, endpoints.get(i), "Endpoint"); + } + } +} diff --git a/src/main/resources/ladybug/cxf-beans.xml b/src/main/resources/ladybug/cxf-beans.xml index 3e9f4445..228b51a0 100644 --- a/src/main/resources/ladybug/cxf-beans.xml +++ b/src/main/resources/ladybug/cxf-beans.xml @@ -13,8 +13,12 @@ + + + +