From e3a6111056c33f27abfec45eccc5c82f8d8fffb5 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 26 Oct 2020 15:41:32 +0100 Subject: [PATCH] Add trace api to trace long running tasks --- .../main/java/sonia/scm/trace/Exporter.java | 43 +++++ .../src/main/java/sonia/scm/trace/Span.java | 148 ++++++++++++++++++ .../java/sonia/scm/trace/SpanContext.java | 58 +++++++ .../src/main/java/sonia/scm/trace/Tracer.java | 83 ++++++++++ .../test/java/sonia/scm/trace/TracerTest.java | 130 +++++++++++++++ 5 files changed, 462 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/trace/Exporter.java create mode 100644 scm-core/src/main/java/sonia/scm/trace/Span.java create mode 100644 scm-core/src/main/java/sonia/scm/trace/SpanContext.java create mode 100644 scm-core/src/main/java/sonia/scm/trace/Tracer.java create mode 100644 scm-core/src/test/java/sonia/scm/trace/TracerTest.java diff --git a/scm-core/src/main/java/sonia/scm/trace/Exporter.java b/scm-core/src/main/java/sonia/scm/trace/Exporter.java new file mode 100644 index 0000000000..91349d1cf2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/trace/Exporter.java @@ -0,0 +1,43 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.trace; + +import sonia.scm.plugin.ExtensionPoint; + +/** + * An exporter could be used to collect and process spans. + * + * @since 2.9.0 + */ +@ExtensionPoint +public interface Exporter { + + /** + * Process the collected span. + * + * @param span collected span + */ + void export(SpanContext span); +} diff --git a/scm-core/src/main/java/sonia/scm/trace/Span.java b/scm-core/src/main/java/sonia/scm/trace/Span.java new file mode 100644 index 0000000000..a386c72b19 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/trace/Span.java @@ -0,0 +1,148 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.trace; + +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A span represents a single unit of work e.g. a request to an external system. + * + * @since 2.9.0 + */ +public final class Span implements AutoCloseable { + + private final Clock clock; + private final Tracer tracer; + private final String kind; + private final Map labels = new LinkedHashMap<>(); + private final Instant opened; + private boolean failed; + + Span(Tracer tracer, String kind) { + this(tracer, kind, Clock.systemUTC()); + } + + Span(Tracer tracer, String kind, Clock clock) { + this.clock = clock; + this.tracer = tracer; + this.kind = kind; + this.opened = clock.instant(); + } + + /** + * Adds a label to the span. + * + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, String value) { + labels.put(key, value); + return this; + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, int value) { + return label(key, String.valueOf(value)); + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, long value) { + return label(key, String.valueOf(value)); + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, float value) { + return label(key, String.valueOf(value)); + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, double value) { + return label(key, String.valueOf(value)); + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, boolean value) { + return label(key, String.valueOf(value)); + } + + /** + * Adds a label to the span. + * @param key key of label + * @param value label value + * @return {@code this} + */ + public Span label(String key, Object value) { + return label(key, String.valueOf(value)); + } + + /** + * Marks the span as failed. + * + * @return {@code this} + */ + public Span failed() { + failed = true; + return this; + } + + /** + * Closes the span a reports the context to the {@link Tracer}. + */ + @Override + public void close() { + tracer.export(new SpanContext(kind, Collections.unmodifiableMap(labels), opened, clock.instant(), failed)); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/trace/SpanContext.java b/scm-core/src/main/java/sonia/scm/trace/SpanContext.java new file mode 100644 index 0000000000..964068d71e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/trace/SpanContext.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.trace; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; + +/** + * The {@link SpanContext} represents a finsished span which could be processed by an {@link Exporter}. + * @since 2.9.0 + */ +@Value +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public final class SpanContext { + + String kind; + Map labels; + Instant opened; + Instant closed; + boolean failed; + + /** + * Calculates the duration of the span. + * + * @return duration of the span + */ + public Duration duration() { + return Duration.between(opened, closed); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/trace/Tracer.java b/scm-core/src/main/java/sonia/scm/trace/Tracer.java new file mode 100644 index 0000000000..7f35c16dff --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/trace/Tracer.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.trace; + +import javax.inject.Inject; +import java.util.Set; + +/** + * The tracer api allows the tracing of long running tasks, such as calling external systems. + * The api is able to collect tracing points called spans. + * + * To use the tracer api inject the {@link Tracer} and open a span in a try with resources block e.g.: + *
+ *   try (Span span = tracer.span("jenkins").label("repository", "builds/core")) {
+ *     Response response = jenkins.call("http://...");
+ *     if (!response.isSuccess()) {
+ *       span.label("reason", response.getFailedReason());
+ *       span.failed();
+ *     }
+ *   }
+ * 
+ * + * As seen in the example we can mark span as failed and add more context to the span with labels. + * After a span is closed it is delegated to an {@link Exporter}, which + * + * @since 2.9.0 + */ +public final class Tracer { + + private final Set exporters; + + /** + * Constructs a new tracer with the given set of exporters. + * + * @param exporters set of exporters + */ + @Inject + public Tracer(Set exporters) { + this.exporters = exporters; + } + + /** + * Creates a new span. + * @param kind kind of span + * @return new span + */ + public Span span(String kind) { + return new Span(this, kind); + } + + /** + * Pass the finished span to the exporters. + * + * @param span finished span + */ + void export(SpanContext span) { + for (Exporter exporter : exporters) { + exporter.export(span); + } + } +} diff --git a/scm-core/src/test/java/sonia/scm/trace/TracerTest.java b/scm-core/src/test/java/sonia/scm/trace/TracerTest.java new file mode 100644 index 0000000000..db9d36001e --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/trace/TracerTest.java @@ -0,0 +1,130 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.trace; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + + +class TracerTest { + + private Tracer tracer; + private CollectingExporter exporter; + + @BeforeEach + void setUpTracer() { + exporter = new CollectingExporter(); + tracer = new Tracer(Collections.singleton(exporter)); + } + + @Test + void shouldReturnSpan() { + tracer.span("sample").close(); + + SpanContext span = exporter.spans.get(0); + assertThat(span.getKind()).isEqualTo("sample"); + assertThat(span.getOpened()).isNotNull(); + assertThat(span.getClosed()).isNotNull(); + assertThat(span.isFailed()).isFalse(); + } + + @Test + @SuppressWarnings("java:S2925") // it is ok, to use sleep here + void shouldReturnPositiveDuration() throws InterruptedException { + try (Span span = tracer.span("sample")) { + span.label("l1", "one"); + Thread.sleep(1L); + } catch (Exception ex) { + span. + } + + SpanContext span = exporter.spans.get(0); + assertThat(span.duration()).isPositive(); + } + + @Test + void shouldConvertLabels() { + try (Span span = tracer.span("sample")) { + span.label("int", 21); + span.label("long", 42L); + span.label("float", 21.0f); + span.label("double", 42.0d); + span.label("boolean", true); + span.label("object", new StringWrapper("value")); + } + + Map labels = exporter.spans.get(0).getLabels(); + assertThat(labels) + .containsEntry("int", "21") + .containsEntry("long", "42") + .containsEntry("float", "21.0") + .containsEntry("double", "42.0") + .containsEntry("boolean", "true") + .containsEntry("object", "value"); + } + + @Test + void shouldReturnFailedSpan() { + try (Span span = tracer.span("failing")) { + span.failed(); + } + + SpanContext span = exporter.spans.get(0); + assertThat(span.getKind()).isEqualTo("failing"); + assertThat(span.isFailed()).isTrue(); + } + + public static class CollectingExporter implements Exporter { + + private final List spans = new ArrayList<>(); + + @Override + public void export(SpanContext spanContext) { + spans.add(spanContext); + } + } + + private static class StringWrapper { + + private final String value; + + public StringWrapper(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + +}