mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-01-01 21:29:47 +01:00
Merge pull request #1393 from scm-manager/feature/trace_api
Tracing API
This commit is contained in:
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Tracing api ([#1393](https://github.com/scm-manager/scm-manager/pull/#1393))
|
||||
- Automatic user converter for external users ([#1380](https://github.com/scm-manager/scm-manager/pull/1380))
|
||||
- Create _authenticated group on setup ([#1396](https://github.com/scm-manager/scm-manager/pull/1396))
|
||||
- The name of the initial git branch can be configured and is set to `main` by default ([#1399](https://github.com/scm-manager/scm-manager/pull/1399))
|
||||
|
||||
@@ -21,30 +21,28 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.net.ahc;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.LinkedHashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import org.apache.shiro.codec.Base64;
|
||||
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Base class for http requests.
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @param <T> request implementation
|
||||
*
|
||||
*
|
||||
* @since 1.46
|
||||
*/
|
||||
public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
@@ -75,7 +73,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public AdvancedHttpResponse request() throws IOException
|
||||
public AdvancedHttpResponse request() throws IOException
|
||||
{
|
||||
return client.request(this);
|
||||
}
|
||||
@@ -102,7 +100,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
String auth = Strings.nullToEmpty(username).concat(":").concat(
|
||||
Strings.nullToEmpty(password));
|
||||
|
||||
auth = Base64.encodeToString(auth.getBytes(Charsets.ISO_8859_1));
|
||||
auth = Base64.encodeToString(auth.getBytes(StandardCharsets.ISO_8859_1));
|
||||
headers.put("Authorization", "Basic ".concat(auth));
|
||||
|
||||
return self();
|
||||
@@ -129,7 +127,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
*
|
||||
*
|
||||
* @param disableCertificateValidation true to disable certificate validation
|
||||
*
|
||||
*
|
||||
* @return request instance
|
||||
*/
|
||||
public T disableCertificateValidation(boolean disableCertificateValidation)
|
||||
@@ -246,6 +244,30 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
return self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the kind of span for tracing api.
|
||||
*
|
||||
* @param spanKind kind of span
|
||||
* @return request instance
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
public T spanKind(String spanKind) {
|
||||
this.spanKind = spanKind;
|
||||
return self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables tracing for the request.
|
||||
* This should only be done for internal requests.
|
||||
*
|
||||
* @return request instance
|
||||
*/
|
||||
public T disableTracing() {
|
||||
this.spanKind = null;
|
||||
return self();
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -281,6 +303,17 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the kind of span which is used for the trace api.
|
||||
*
|
||||
* @return kind of span
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
public String getSpanKind() {
|
||||
return spanKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request decodes gzip compression.
|
||||
*
|
||||
@@ -317,7 +350,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
/**
|
||||
* Returns true if the proxy settings are ignored.
|
||||
*
|
||||
*
|
||||
*
|
||||
* @return true if the proxy settings are ignored
|
||||
*/
|
||||
public boolean isIgnoreProxySettings()
|
||||
@@ -341,7 +374,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of the given object or {@code null}, if the
|
||||
* Returns string representation of the given object or {@code null}, if the
|
||||
* object is {@code null}.
|
||||
*
|
||||
*
|
||||
@@ -398,4 +431,7 @@ public abstract class BaseHttpRequest<T extends BaseHttpRequest>
|
||||
|
||||
/** url of request */
|
||||
private String url;
|
||||
|
||||
/** kind of span for trace api */
|
||||
private String spanKind = "HTTP Request";
|
||||
}
|
||||
|
||||
43
scm-core/src/main/java/sonia/scm/trace/Exporter.java
Normal file
43
scm-core/src/main/java/sonia/scm/trace/Exporter.java
Normal 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);
|
||||
}
|
||||
141
scm-core/src/main/java/sonia/scm/trace/Span.java
Normal file
141
scm-core/src/main/java/sonia/scm/trace/Span.java
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.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 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 = tracer;
|
||||
this.kind = kind;
|
||||
this.opened = Instant.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, Instant.now(), failed));
|
||||
}
|
||||
|
||||
}
|
||||
80
scm-core/src/main/java/sonia/scm/trace/SpanContext.java
Normal file
80
scm-core/src/main/java/sonia/scm/trace/SpanContext.java
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import sonia.scm.xml.XmlInstantAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The {@link SpanContext} represents a finished span which could be processed by an {@link Exporter}.
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
@Getter
|
||||
@XmlRootElement
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
public class SpanContext {
|
||||
|
||||
private String kind;
|
||||
private Map<String, String> labels;
|
||||
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||
private Instant opened;
|
||||
@XmlJavaTypeAdapter(XmlInstantAdapter.class)
|
||||
private Instant closed;
|
||||
private boolean failed;
|
||||
|
||||
/**
|
||||
* Returns the label with the given key or {@code null}.
|
||||
* @param key key of label
|
||||
* @return label or {@code null}
|
||||
*/
|
||||
public String label(String key) {
|
||||
return labels.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the duration of the span.
|
||||
*
|
||||
* @return duration of the span
|
||||
*/
|
||||
public Duration duration() {
|
||||
return Duration.between(opened, closed);
|
||||
}
|
||||
}
|
||||
83
scm-core/src/main/java/sonia/scm/trace/Tracer.java
Normal file
83
scm-core/src/main/java/sonia/scm/trace/Tracer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
scm-core/src/test/java/sonia/scm/trace/SpanContextTest.java
Normal file
57
scm-core/src/test/java/sonia/scm/trace/SpanContextTest.java
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 com.google.common.collect.ImmutableMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.xml.bind.JAXB;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SpanContextTest {
|
||||
|
||||
@Test
|
||||
void shouldMarshalAndUnmarshal() {
|
||||
Instant now = Instant.now();
|
||||
SpanContext span = new SpanContext(
|
||||
"jenkins", ImmutableMap.of("one", "1"), now, now, true
|
||||
);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
JAXB.marshal(span, baos);
|
||||
span = JAXB.unmarshal(new ByteArrayInputStream(baos.toByteArray()), SpanContext.class);
|
||||
|
||||
assertThat(span.getKind()).isEqualTo("jenkins");
|
||||
assertThat(span.label("one")).isEqualTo("1");
|
||||
assertThat(span.getOpened()).isEqualTo(now);
|
||||
assertThat(span.getClosed()).isEqualTo(now);
|
||||
assertThat(span.isFailed()).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
127
scm-core/src/test/java/sonia/scm/trace/TracerTest.java
Normal file
127
scm-core/src/test/java/sonia/scm/trace/TracerTest.java
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -54,11 +54,10 @@ import java.util.UUID;
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
public class HgHookManager
|
||||
{
|
||||
public class HgHookManager {
|
||||
|
||||
/** Field description */
|
||||
public static final String URL_HOOKPATH = "/hook/hg/";
|
||||
@SuppressWarnings("java:S1075") // this url is fixed
|
||||
private static final String URL_HOOKPATH = "/hook/hg/";
|
||||
|
||||
/**
|
||||
* the logger for HgHookManager
|
||||
@@ -191,64 +190,27 @@ public class HgHookManager
|
||||
return accessTokenBuilderFactory.create().build();
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
private void buildHookUrl(HttpServletRequest request)
|
||||
{
|
||||
if (configuration.isForceBaseUrl())
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug(
|
||||
"create hook url from configured base url because force base url is enabled");
|
||||
}
|
||||
private void buildHookUrl(HttpServletRequest request) {
|
||||
if (configuration.isForceBaseUrl()) {
|
||||
logger.debug("create hook url from configured base url because force base url is enabled");
|
||||
|
||||
hookUrl = createConfiguredUrl();
|
||||
|
||||
if (!isUrlWorking(hookUrl))
|
||||
{
|
||||
if (!isUrlWorking(hookUrl)) {
|
||||
disableHooks();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("create hook url from request");
|
||||
}
|
||||
} else {
|
||||
logger.debug("create hook url from request");
|
||||
|
||||
hookUrl = HttpUtil.getCompleteUrl(request, URL_HOOKPATH);
|
||||
|
||||
if (!isUrlWorking(hookUrl))
|
||||
{
|
||||
if (logger.isWarnEnabled())
|
||||
{
|
||||
logger.warn(
|
||||
"hook url {} from request does not work, try now localhost",
|
||||
hookUrl);
|
||||
}
|
||||
if (!isUrlWorking(hookUrl)) {
|
||||
logger.warn("hook url {} from request does not work, try now localhost", hookUrl);
|
||||
|
||||
hookUrl = createLocalUrl(request);
|
||||
|
||||
if (!isUrlWorking(hookUrl))
|
||||
{
|
||||
if (logger.isWarnEnabled())
|
||||
{
|
||||
logger.warn(
|
||||
"localhost hook url {} does not work, try now from configured base url",
|
||||
hookUrl);
|
||||
}
|
||||
if (!isUrlWorking(hookUrl)) {
|
||||
logger.warn("localhost hook url {} does not work, try now from configured base url", hookUrl);
|
||||
|
||||
hookUrl = createConfiguredUrl();
|
||||
|
||||
if (!isUrlWorking(hookUrl))
|
||||
{
|
||||
if (!isUrlWorking(hookUrl)) {
|
||||
disableHooks();
|
||||
}
|
||||
}
|
||||
@@ -270,7 +232,7 @@ public class HgHookManager
|
||||
configuration.getBaseUrl(),
|
||||
"http://localhost:8080/scm"
|
||||
)
|
||||
).concat("/hook/hg/");
|
||||
).concat(URL_HOOKPATH);
|
||||
//J+
|
||||
}
|
||||
|
||||
@@ -324,11 +286,7 @@ public class HgHookManager
|
||||
{
|
||||
request = httpServletRequestProvider.get();
|
||||
}
|
||||
catch (ProvisionException ex)
|
||||
{
|
||||
logger.debug("http servlet request is not available");
|
||||
}
|
||||
catch (OutOfScopeException ex)
|
||||
catch (ProvisionException | OutOfScopeException ex)
|
||||
{
|
||||
logger.debug("http servlet request is not available");
|
||||
}
|
||||
@@ -358,6 +316,7 @@ public class HgHookManager
|
||||
.disableHostnameValidation(true)
|
||||
.disableCertificateValidation(true)
|
||||
.ignoreProxySettings(true)
|
||||
.disableTracing()
|
||||
.request()
|
||||
.getStatus();
|
||||
//J+
|
||||
|
||||
@@ -21,73 +21,60 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, FormEvent } from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import React, { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { createAttributesForTesting } from "../devBuild";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
type Props = {
|
||||
filter: (p: string) => void;
|
||||
value?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const FixedHeightInput = styled.input`
|
||||
height: 2.5rem;
|
||||
`;
|
||||
|
||||
class FilterInput extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.props.value ? this.props.value : ""
|
||||
};
|
||||
}
|
||||
const FilterInput: FC<Props> = ({ filter, value, testId, placeholder }) => {
|
||||
const [stateValue, setStateValue] = useState(value || "");
|
||||
const [timeoutId, setTimeoutId] = useState(0);
|
||||
const [t] = useTranslation("commons");
|
||||
|
||||
handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
value: event.target.value
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!stateValue) {
|
||||
// no delay if filter input was deleted
|
||||
filter(stateValue);
|
||||
} else {
|
||||
// with delay while typing
|
||||
const id = setTimeout(() => filter(stateValue), 1000);
|
||||
setTimeoutId(id);
|
||||
}
|
||||
}, [stateValue]);
|
||||
|
||||
handleSubmit = (event: FormEvent) => {
|
||||
this.props.filter(this.state.value);
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
filter(stateValue);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
componentDidUpdate = ({ value: oldValue }: Props) => {
|
||||
const { value: newValue } = this.props;
|
||||
const { value: stateValue } = this.state;
|
||||
if (oldValue !== newValue && newValue !== stateValue) {
|
||||
this.setState({
|
||||
value: newValue || ""
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form className="input-field" onSubmit={handleSubmit} {...createAttributesForTesting(testId)}>
|
||||
<div className="control has-icons-left">
|
||||
<FixedHeightInput
|
||||
className="input"
|
||||
type="search"
|
||||
placeholder={placeholder || t("filterEntries")}
|
||||
value={stateValue}
|
||||
onChange={event => setStateValue(event.target.value)}
|
||||
/>
|
||||
<span className="icon is-small is-left">
|
||||
<i className="fas fa-search" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, testId } = this.props;
|
||||
return (
|
||||
<form className="input-field" onSubmit={this.handleSubmit} {...createAttributesForTesting(testId)}>
|
||||
<div className="control has-icons-left">
|
||||
<FixedHeightInput
|
||||
className="input"
|
||||
type="search"
|
||||
placeholder={t("filterEntries")}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<span className="icon is-small is-left">
|
||||
<i className="fas fa-search" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation("commons")(FilterInput);
|
||||
export default FilterInput;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package sonia.scm.admin;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -46,6 +47,9 @@ public class ReleaseFeedParser {
|
||||
public static final int DEFAULT_TIMEOUT_IN_MILLIS = 1000;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReleaseFeedParser.class);
|
||||
|
||||
@VisibleForTesting
|
||||
static final String SPAN_KIND = "Release Feed";
|
||||
|
||||
private final AdvancedHttpClient client;
|
||||
private final ExecutorService executorService;
|
||||
@@ -103,7 +107,10 @@ public class ReleaseFeedParser {
|
||||
if (Strings.isNullOrEmpty(url)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
ReleaseFeedDto releaseFeed = client.get(url).request().contentFromXml(ReleaseFeedDto.class);
|
||||
ReleaseFeedDto releaseFeed = client.get(url)
|
||||
.spanKind(SPAN_KIND)
|
||||
.request()
|
||||
.contentFromXml(ReleaseFeedDto.class);
|
||||
return filterForLatestRelease(releaseFeed);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not parse release feed from {}", url, e);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.net.ahc;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -38,8 +38,11 @@ import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.net.Proxies;
|
||||
import sonia.scm.net.TrustAllHostnameVerifier;
|
||||
import sonia.scm.net.TrustAllTrustManager;
|
||||
import sonia.scm.trace.Span;
|
||||
import sonia.scm.trace.Tracer;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.inject.Provider;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
@@ -99,9 +102,10 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
*/
|
||||
@Inject
|
||||
public DefaultAdvancedHttpClient(ScmConfiguration configuration,
|
||||
Set<ContentTransformer> contentTransformers, Provider<SSLContext> sslContextProvider)
|
||||
Tracer tracer, Set<ContentTransformer> contentTransformers, Provider<SSLContext> sslContextProvider)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.tracer = tracer;
|
||||
this.contentTransformers = contentTransformers;
|
||||
this.sslContextProvider = sslContextProvider;
|
||||
}
|
||||
@@ -185,45 +189,65 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
protected AdvancedHttpResponse request(BaseHttpRequest<?> request)
|
||||
throws IOException
|
||||
{
|
||||
HttpURLConnection connection = openConnection(request,
|
||||
new URL(request.getUrl()));
|
||||
protected AdvancedHttpResponse request(BaseHttpRequest<?> request) throws IOException {
|
||||
String spanKind = request.getSpanKind();
|
||||
if (Strings.isNullOrEmpty(spanKind)) {
|
||||
logger.debug("execute request {} without tracing", request.getUrl());
|
||||
return doRequest(request);
|
||||
}
|
||||
return doRequestWithTracing(request);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private DefaultAdvancedHttpResponse doRequestWithTracing(BaseHttpRequest<?> request) throws IOException {
|
||||
try (Span span = tracer.span(request.getSpanKind())) {
|
||||
span.label("url", request.getUrl());
|
||||
span.label("method", request.getMethod());
|
||||
try {
|
||||
DefaultAdvancedHttpResponse response = doRequest(request);
|
||||
span.label("status", response.getStatus());
|
||||
if (!response.isSuccessful()) {
|
||||
span.failed();
|
||||
}
|
||||
return response;
|
||||
} catch (IOException ex) {
|
||||
span.label("exception", ex.getClass().getName());
|
||||
span.label("message", ex.getMessage());
|
||||
span.failed();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private DefaultAdvancedHttpResponse doRequest(BaseHttpRequest<?> request) throws IOException {
|
||||
HttpURLConnection connection = openConnection(request, new URL(request.getUrl()));
|
||||
|
||||
applyBaseSettings(request, connection);
|
||||
|
||||
if (connection instanceof HttpsURLConnection)
|
||||
{
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
applySSLSettings(request, (HttpsURLConnection) connection);
|
||||
}
|
||||
|
||||
Content content = null;
|
||||
|
||||
if (request instanceof AdvancedHttpRequestWithBody)
|
||||
{
|
||||
if (request instanceof AdvancedHttpRequestWithBody) {
|
||||
AdvancedHttpRequestWithBody ahrwb = (AdvancedHttpRequestWithBody) request;
|
||||
|
||||
content = ahrwb.getContent();
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
if (content != null) {
|
||||
content.prepare(ahrwb);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
request.header(HttpUtil.HEADER_CONTENT_LENGTH, "0");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
request.header(HttpUtil.HEADER_CONTENT_LENGTH, "0");
|
||||
}
|
||||
|
||||
applyHeaders(request, connection);
|
||||
|
||||
if (content != null)
|
||||
{
|
||||
if (content != null) {
|
||||
applyContent(connection, content);
|
||||
}
|
||||
|
||||
@@ -300,7 +324,7 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
{
|
||||
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||
new TrustAllTrustManager() };
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
SSLContext sc = SSLContext.getInstance("TLS");
|
||||
|
||||
sc.init(null, trustAllCerts, new java.security.SecureRandom());
|
||||
connection.setSSLSocketFactory(sc.getSocketFactory());
|
||||
@@ -309,10 +333,10 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
{
|
||||
logger.error("could not disable certificate validation", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.trace("set ssl socker factory from provider");
|
||||
logger.trace("set ssl socket factory from provider");
|
||||
connection.setSSLSocketFactory(sslContextProvider.get().getSocketFactory());
|
||||
}
|
||||
|
||||
@@ -330,7 +354,7 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
|
||||
if (isProxyEnabled(request))
|
||||
{
|
||||
connection = openProxyConnection(request, url);
|
||||
connection = openProxyConnection(url);
|
||||
appendProxyAuthentication(connection);
|
||||
}
|
||||
else
|
||||
@@ -340,7 +364,9 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
logger.trace("ignore proxy settings");
|
||||
}
|
||||
|
||||
logger.debug("fetch {}", url.toExternalForm());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("fetch {}", url.toExternalForm());
|
||||
}
|
||||
|
||||
connection = createConnection(url);
|
||||
}
|
||||
@@ -348,8 +374,7 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
return connection;
|
||||
}
|
||||
|
||||
private HttpURLConnection openProxyConnection(BaseHttpRequest<?> request,
|
||||
URL url)
|
||||
private HttpURLConnection openProxyConnection(URL url)
|
||||
throws IOException
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
@@ -380,7 +405,10 @@ public class DefaultAdvancedHttpClient extends AdvancedHttpClient
|
||||
|
||||
/** set of content transformers */
|
||||
private final Set<ContentTransformer> contentTransformers;
|
||||
|
||||
|
||||
/** ssl context provider */
|
||||
private final Provider<SSLContext> sslContextProvider;
|
||||
|
||||
/** tracer used for request tracing */
|
||||
private final Tracer tracer;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -34,6 +34,8 @@ import javax.inject.Inject;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static sonia.scm.plugin.Tracing.SPAN_KIND;
|
||||
|
||||
class PluginCenterLoader {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PluginCenterLoader.class);
|
||||
@@ -57,7 +59,8 @@ class PluginCenterLoader {
|
||||
Set<AvailablePlugin> load(String url) {
|
||||
try {
|
||||
LOG.info("fetch plugins from {}", url);
|
||||
PluginCenterDto pluginCenterDto = client.get(url).request().contentFromJson(PluginCenterDto.class);
|
||||
PluginCenterDto pluginCenterDto = client.get(url).spanKind(SPAN_KIND).request()
|
||||
.contentFromJson(PluginCenterDto.class);
|
||||
return mapper.map(pluginCenterDto);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("failed to load plugins from plugin center, returning empty list", ex);
|
||||
|
||||
@@ -38,6 +38,8 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
import static sonia.scm.plugin.Tracing.SPAN_KIND;
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage")
|
||||
// guava hash is marked as unstable
|
||||
class PluginInstaller {
|
||||
@@ -126,7 +128,7 @@ class PluginInstaller {
|
||||
}
|
||||
|
||||
private InputStream download(AvailablePlugin plugin) throws IOException {
|
||||
return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream();
|
||||
return client.get(plugin.getDescriptor().getUrl()).spanKind(SPAN_KIND).request().contentAsStream();
|
||||
}
|
||||
|
||||
private Path createFile(AvailablePlugin plugin) throws IOException {
|
||||
|
||||
33
scm-webapp/src/main/java/sonia/scm/plugin/Tracing.java
Normal file
33
scm-webapp/src/main/java/sonia/scm/plugin/Tracing.java
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.plugin;
|
||||
|
||||
final class Tracing {
|
||||
|
||||
public static final String SPAN_KIND = "Plugin Center";
|
||||
|
||||
private Tracing() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* An {@link Exporter} which logs every collected span.
|
||||
*
|
||||
* @since 2.9.0
|
||||
*/
|
||||
@Extension
|
||||
public final class LoggingExporter implements Exporter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LoggingExporter.class);
|
||||
|
||||
private final Consumer<String> logger;
|
||||
|
||||
@Inject
|
||||
LoggingExporter() {
|
||||
this(LOG::info);
|
||||
}
|
||||
|
||||
LoggingExporter(Consumer<String> logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void export(SpanContext span) {
|
||||
logger.accept(format(span));
|
||||
}
|
||||
|
||||
private String format(SpanContext span) {
|
||||
StringBuilder message = new StringBuilder("received ");
|
||||
if (span.isFailed()) {
|
||||
message.append("failed ");
|
||||
}
|
||||
message.append(span.getKind()).append(" span, which took ");
|
||||
message.append(span.duration().toMillis()).append("ms");
|
||||
Map<String, String> labels = span.getLabels();
|
||||
if (!labels.isEmpty()) {
|
||||
message.append(":");
|
||||
for (Map.Entry<String, String> e : labels.entrySet()) {
|
||||
message.append("\n - ").append(e.getKey()).append(": ").append(e.getValue());
|
||||
}
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.AdvancedHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
@@ -44,6 +45,7 @@ import java.util.concurrent.Semaphore;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.admin.ReleaseFeedParser.SPAN_KIND;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReleaseFeedParserTest {
|
||||
@@ -62,7 +64,7 @@ class ReleaseFeedParserTest {
|
||||
void shouldFindLatestRelease() throws IOException {
|
||||
String url = "https://www.scm-manager.org/download/rss.xml";
|
||||
|
||||
when(client.get(url).request().contentFromXml(ReleaseFeedDto.class)).thenReturn(createReleaseFeedDto());
|
||||
when(request(url).contentFromXml(ReleaseFeedDto.class)).thenReturn(createReleaseFeedDto());
|
||||
|
||||
Optional<UpdateInfo> update = releaseFeedParser.findLatestRelease(url);
|
||||
|
||||
@@ -71,13 +73,17 @@ class ReleaseFeedParserTest {
|
||||
assertThat(update.get().getLink()).isEqualTo("download-3");
|
||||
}
|
||||
|
||||
private AdvancedHttpResponse request(String url) throws IOException {
|
||||
return client.get(url).spanKind(SPAN_KIND).request();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleTimeout() throws IOException {
|
||||
String url = "https://www.scm-manager.org/download/rss.xml";
|
||||
|
||||
Semaphore waitWithResultUntilTimeout = new Semaphore(0);
|
||||
|
||||
when(client.get(url).request().contentFromXml(ReleaseFeedDto.class)).thenAnswer(invocation -> {
|
||||
when(request(url).contentFromXml(ReleaseFeedDto.class)).thenAnswer(invocation -> {
|
||||
waitWithResultUntilTimeout.acquire();
|
||||
return createReleaseFeedDto();
|
||||
});
|
||||
@@ -95,7 +101,7 @@ class ReleaseFeedParserTest {
|
||||
|
||||
Semaphore waitWithResultUntilBothTriggered = new Semaphore(0);
|
||||
|
||||
when(client.get(url).request().contentFromXml(ReleaseFeedDto.class)).thenAnswer(invocation -> {
|
||||
when(request(url).contentFromXml(ReleaseFeedDto.class)).thenAnswer(invocation -> {
|
||||
waitWithResultUntilBothTriggered.acquire();
|
||||
return createReleaseFeedDto();
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.net.ahc;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -29,33 +29,29 @@ package sonia.scm.net.ahc;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
import sonia.scm.net.TrustAllHostnameVerifier;
|
||||
import sonia.scm.trace.Span;
|
||||
import sonia.scm.trace.Tracer;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URL;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -82,12 +78,12 @@ public class DefaultAdvancedHttpClientTest
|
||||
DefaultAdvancedHttpClient.TIMEOUT_CONNECTION);
|
||||
verify(connection).addRequestProperty(HttpUtil.HEADER_CONTENT_LENGTH, "0");
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = ContentTransformerNotFoundException.class)
|
||||
public void testContentTransformerNotFound(){
|
||||
client.createTransformer(String.class, "text/plain");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testContentTransformer(){
|
||||
ContentTransformer transformer = mock(ContentTransformer.class);
|
||||
@@ -265,6 +261,63 @@ public class DefaultAdvancedHttpClientTest
|
||||
"Basic dHJpY2lhOnRyaWNpYXMgc2VjcmV0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateTracingSpan() throws IOException {
|
||||
when(connection.getResponseCode()).thenReturn(200);
|
||||
|
||||
new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").spanKind("spaceships").request();
|
||||
verify(tracer).span("spaceships");
|
||||
verify(span).label("url", "https://www.scm-manager.org");
|
||||
verify(span).label("method", "GET");
|
||||
verify(span).label("status", 200);
|
||||
verify(span, never()).failed();
|
||||
verify(span).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateFailedTracingSpan() throws IOException {
|
||||
when(connection.getResponseCode()).thenReturn(500);
|
||||
|
||||
new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org").request();
|
||||
verify(tracer).span("HTTP Request");
|
||||
verify(span).label("url", "https://www.scm-manager.org");
|
||||
verify(span).label("method", "GET");
|
||||
verify(span).label("status", 500);
|
||||
verify(span).failed();
|
||||
verify(span).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateFailedTracingSpanOnIOException() throws IOException {
|
||||
when(connection.getResponseCode()).thenThrow(new IOException("failed"));
|
||||
|
||||
boolean thrown = false;
|
||||
try {
|
||||
new AdvancedHttpRequest(client, HttpMethod.DELETE, "http://failing.host").spanKind("failures").request();
|
||||
} catch (IOException ex) {
|
||||
thrown = true;
|
||||
}
|
||||
assertTrue(thrown);
|
||||
|
||||
verify(tracer).span("failures");
|
||||
verify(span).label("url", "http://failing.host");
|
||||
verify(span).label("method", "DELETE");
|
||||
verify(span).label("exception", IOException.class.getName());
|
||||
verify(span).label("message", "failed");
|
||||
verify(span).failed();
|
||||
verify(span).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateSpan() throws IOException {
|
||||
when(connection.getResponseCode()).thenReturn(200);
|
||||
|
||||
new AdvancedHttpRequest(client, HttpMethod.GET, "https://www.scm-manager.org")
|
||||
.disableTracing().request();
|
||||
verify(tracer, never()).span(anyString());
|
||||
}
|
||||
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -277,6 +330,7 @@ public class DefaultAdvancedHttpClientTest
|
||||
configuration = new ScmConfiguration();
|
||||
transformers = new HashSet<ContentTransformer>();
|
||||
client = new TestingAdvacedHttpClient(configuration, transformers);
|
||||
when(tracer.span(anyString())).thenReturn(span);
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
@@ -298,10 +352,9 @@ public class DefaultAdvancedHttpClientTest
|
||||
* @param configuration
|
||||
* @param transformers
|
||||
*/
|
||||
public TestingAdvacedHttpClient(ScmConfiguration configuration,
|
||||
Set<ContentTransformer> transformers)
|
||||
public TestingAdvacedHttpClient(ScmConfiguration configuration, Set<ContentTransformer> transformers)
|
||||
{
|
||||
super(configuration, transformers, new SSLContextProvider());
|
||||
super(configuration, tracer, transformers, new SSLContextProvider());
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
@@ -364,4 +417,10 @@ public class DefaultAdvancedHttpClientTest
|
||||
|
||||
/** Field description */
|
||||
private Set<ContentTransformer> transformers;
|
||||
|
||||
@Mock
|
||||
private Tracer tracer;
|
||||
|
||||
@Mock
|
||||
private Span span;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.net.ahc;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
@@ -33,9 +33,11 @@ import com.google.common.collect.Multimap;
|
||||
import com.google.common.io.ByteSource;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
@@ -56,6 +58,7 @@ import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import sonia.scm.net.SSLContextProvider;
|
||||
import sonia.scm.trace.Tracer;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -65,6 +68,13 @@ import sonia.scm.net.SSLContextProvider;
|
||||
public class DefaultAdvancedHttpResponseTest
|
||||
{
|
||||
|
||||
private DefaultAdvancedHttpClient client;
|
||||
|
||||
@Before
|
||||
public void setUpClient() {
|
||||
client = new DefaultAdvancedHttpClient(new ScmConfiguration(), tracer, new HashSet<>(), new SSLContextProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -130,13 +140,10 @@ public class DefaultAdvancedHttpResponseTest
|
||||
assertTrue(headers.get("Test-2").isEmpty());
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final DefaultAdvancedHttpClient client =
|
||||
new DefaultAdvancedHttpClient(new ScmConfiguration(), new HashSet<>(), new SSLContextProvider());
|
||||
|
||||
/** Field description */
|
||||
@Mock
|
||||
private HttpURLConnection connection;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Tracer tracer;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.AdvancedHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
@@ -41,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.plugin.Tracing.SPAN_KIND;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginCenterLoaderTest {
|
||||
@@ -63,16 +65,20 @@ class PluginCenterLoaderTest {
|
||||
void shouldFetch() throws IOException {
|
||||
Set<AvailablePlugin> plugins = Collections.emptySet();
|
||||
PluginCenterDto dto = new PluginCenterDto();
|
||||
when(client.get(PLUGIN_URL).request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
|
||||
when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
|
||||
when(mapper.map(dto)).thenReturn(plugins);
|
||||
|
||||
Set<AvailablePlugin> fetched = loader.load(PLUGIN_URL);
|
||||
assertThat(fetched).isSameAs(plugins);
|
||||
}
|
||||
|
||||
private AdvancedHttpResponse request() throws IOException {
|
||||
return client.get(PLUGIN_URL).spanKind(SPAN_KIND).request();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException {
|
||||
when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch"));
|
||||
when(request()).thenThrow(new IOException("failed to fetch"));
|
||||
|
||||
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL);
|
||||
assertThat(fetch).isEmpty();
|
||||
@@ -80,7 +86,7 @@ class PluginCenterLoaderTest {
|
||||
|
||||
@Test
|
||||
void shouldFirePluginCenterErrorEvent() throws IOException {
|
||||
when(client.get(PLUGIN_URL).request()).thenThrow(new IOException("failed to fetch"));
|
||||
when(request()).thenThrow(new IOException("failed to fetch"));
|
||||
|
||||
loader.load(PLUGIN_URL);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.net.ahc.AdvancedHttpClient;
|
||||
import sonia.scm.net.ahc.AdvancedHttpResponse;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -50,6 +51,7 @@ import static org.mockito.Mockito.anyInt;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.plugin.Tracing.SPAN_KIND;
|
||||
|
||||
@ExtendWith({MockitoExtension.class})
|
||||
class PluginInstallerTest {
|
||||
@@ -101,10 +103,14 @@ class PluginInstallerTest {
|
||||
}
|
||||
|
||||
private void mockContent(String content) throws IOException {
|
||||
when(client.get("https://download.hitchhiker.com").request().contentAsStream())
|
||||
when(request("https://download.hitchhiker.com").contentAsStream())
|
||||
.thenReturn(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
private AdvancedHttpResponse request(String url) throws IOException {
|
||||
return client.get(url).spanKind(SPAN_KIND).request();
|
||||
}
|
||||
|
||||
private AvailablePlugin createGitPlugin() {
|
||||
return createPlugin(
|
||||
"scm-git-plugin",
|
||||
@@ -115,7 +121,7 @@ class PluginInstallerTest {
|
||||
|
||||
@Test
|
||||
void shouldThrowPluginDownloadException() throws IOException {
|
||||
when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download"));
|
||||
when(request("https://download.hitchhiker.com")).thenThrow(new IOException("failed to download"));
|
||||
|
||||
PluginInstallationContext context = PluginInstallationContext.empty();
|
||||
AvailablePlugin gitPlugin = createGitPlugin();
|
||||
@@ -136,7 +142,7 @@ class PluginInstallerTest {
|
||||
void shouldThrowPluginDownloadExceptionAndCleanup() throws IOException {
|
||||
InputStream stream = mock(InputStream.class);
|
||||
when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read"));
|
||||
when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream);
|
||||
when(request("https://download.hitchhiker.com").contentAsStream()).thenReturn(stream);
|
||||
|
||||
PluginInstallationContext context = PluginInstallationContext.empty();
|
||||
AvailablePlugin gitPlugin = createGitPlugin();
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 com.google.common.collect.ImmutableMap;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LoggingExporterTest {
|
||||
|
||||
private String message;
|
||||
|
||||
private LoggingExporter exporter;
|
||||
|
||||
@BeforeEach
|
||||
void setUpLogger() {
|
||||
exporter = new LoggingExporter((message) -> this.message = message);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLogTheSpanKind() {
|
||||
exporter.export(new SpanContext(
|
||||
"AwesomeSpanKind", Collections.emptyMap(), Instant.now(), Instant.now(), false
|
||||
));
|
||||
|
||||
assertThat(message).contains("AwesomeSpanKind");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLogFailed() {
|
||||
exporter.export(new SpanContext(
|
||||
"sample", Collections.emptyMap(), Instant.now(), Instant.now(), true
|
||||
));
|
||||
|
||||
assertThat(message).contains("failed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLogDuration() {
|
||||
Instant opened = Instant.now();
|
||||
exporter.export(new SpanContext(
|
||||
"sample", ImmutableMap.of(), opened, opened.plusMillis(42L), false
|
||||
));
|
||||
|
||||
assertThat(message).contains("42ms");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLogLabels() {
|
||||
exporter.export(new SpanContext(
|
||||
"sample", ImmutableMap.of("l1", "v1", "l2", "v2"), Instant.now(), Instant.now(), false
|
||||
));
|
||||
|
||||
assertThat(message)
|
||||
.contains("l1")
|
||||
.contains("v1")
|
||||
.contains("l2")
|
||||
.contains("v2");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12435,10 +12435,10 @@ mini-create-react-context@^0.4.0:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
tiny-warning "^1.0.3"
|
||||
|
||||
mini-css-extract-plugin@^0.11.0:
|
||||
version "0.11.2"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.2.tgz#e3af4d5e04fbcaaf11838ab230510073060b37bf"
|
||||
integrity sha512-h2LknfX4U1kScXxH8xE9LCOqT5B+068EAj36qicMb8l4dqdJoyHcmWmpd+ueyZfgu/POvIn+teoUnTtei2ikug==
|
||||
mini-css-extract-plugin@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.12.0.tgz#ddeb74fd6304ca9f99c1db74acc7d5b507705454"
|
||||
integrity sha512-z6PQCe9rd1XUwZ8gMaEVwwRyZlrYy8Ba1gRjFP5HcV51HkXX+XlwZ+a1iAYTjSYwgNBXoNR7mhx79mDpOn5fdw==
|
||||
dependencies:
|
||||
loader-utils "^1.1.0"
|
||||
normalize-url "1.9.1"
|
||||
|
||||
Reference in New Issue
Block a user