Add trace api to trace long running tasks

This commit is contained in:
Sebastian Sdorra
2020-10-26 15:41:32 +01:00
parent ea1eab6356
commit e3a6111056
5 changed files with 462 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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<String,String> 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));
}
}

View File

@@ -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<String,String> 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);
}
}

View File

@@ -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.:
* <pre>
* 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();
* }
* }
* </pre>
*
* 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<Exporter> exporters;
/**
* Constructs a new tracer with the given set of exporters.
*
* @param exporters set of exporters
*/
@Inject
public Tracer(Set<Exporter> 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);
}
}
}

View File

@@ -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<String, String> 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<SpanContext> 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;
}
}
}