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);
+ }
+}
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/main/java/sonia/scm/web/UserAgent.java b/scm-core/src/main/java/sonia/scm/web/UserAgent.java
index 0647587a19..35e4060a91 100644
--- a/scm-core/src/main/java/sonia/scm/web/UserAgent.java
+++ b/scm-core/src/main/java/sonia/scm/web/UserAgent.java
@@ -24,18 +24,14 @@
package sonia.scm.web;
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import static com.google.common.base.Preconditions.checkNotNull;
-//~--- JDK imports ------------------------------------------------------------
-
/**
* The software agent that is acting on behalf of a user. The user agent
* represents a browser or one of the repository client (svn, git or hg).
@@ -118,7 +114,7 @@ public final class UserAgent
return Objects.equal(name, other.name)
&& Objects.equal(browser, other.browser)
- && Objects.equal(basicAuthenticationCharset, basicAuthenticationCharset);
+ && Objects.equal(basicAuthenticationCharset, other.basicAuthenticationCharset);
}
/**
@@ -268,7 +264,7 @@ public final class UserAgent
private boolean scmClient = false;
/** basic authentication charset */
- private Charset basicAuthenticationCharset = Charsets.ISO_8859_1;
+ private Charset basicAuthenticationCharset = StandardCharsets.ISO_8859_1;
}
diff --git a/scm-core/src/test/java/sonia/scm/trace/SpanContextTest.java b/scm-core/src/test/java/sonia/scm/trace/SpanContextTest.java
new file mode 100644
index 0000000000..87bba53753
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/trace/SpanContextTest.java
@@ -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();
+ }
+
+}
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..12985038f8
--- /dev/null
+++ b/scm-core/src/test/java/sonia/scm/trace/TracerTest.java
@@ -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 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;
+ }
+ }
+
+}
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java
index 5e93c1d859..3d333015ee 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgHookManager.java
@@ -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+
diff --git a/scm-ui/ui-components/src/forms/FilterInput.tsx b/scm-ui/ui-components/src/forms/FilterInput.tsx
index fad1611c00..15dcabb9e1 100644
--- a/scm-ui/ui-components/src/forms/FilterInput.tsx
+++ b/scm-ui/ui-components/src/forms/FilterInput.tsx
@@ -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 {
- constructor(props: Props) {
- super(props);
- this.state = {
- value: this.props.value ? this.props.value : ""
- };
- }
+const FilterInput: FC = ({ filter, value, testId, placeholder }) => {
+ const [stateValue, setStateValue] = useState(value || "");
+ const [timeoutId, setTimeoutId] = useState(0);
+ const [t] = useTranslation("commons");
- handleChange = (event: ChangeEvent) => {
- 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 (
+
+ );
+};
- render() {
- const { t, testId } = this.props;
- return (
-
- );
- }
-}
-
-export default withTranslation("commons")(FilterInput);
+export default FilterInput;
diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index ce26d72a9d..5874113e55 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -210,7 +210,7 @@
org.hibernate.validator
hibernate-validator
- 6.1.5.Final
+ 6.1.6.Final
diff --git a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java
index 26c4c63b94..6576cdbceb 100644
--- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java
+++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java
@@ -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);
diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java
index af95c09aa8..1f47f03686 100644
--- a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java
+++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java
@@ -30,6 +30,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
+import sonia.scm.group.Group;
+import sonia.scm.group.GroupManager;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.PermissionAssigner;
@@ -44,6 +46,8 @@ import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Collections;
+import static sonia.scm.group.GroupCollector.AUTHENTICATED;
+
@Extension
public class SetupContextListener implements ServletContextListener {
@@ -75,13 +79,18 @@ public class SetupContextListener implements ServletContextListener {
private final PasswordService passwordService;
private final PermissionAssigner permissionAssigner;
private final ScmConfiguration scmConfiguration;
+ private final GroupManager groupManager;
+
+ @VisibleForTesting
+ static final String AUTHENTICATED_GROUP_DESCRIPTION = "Includes all authenticated users";
@Inject
- public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner, ScmConfiguration scmConfiguration) {
+ public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner, ScmConfiguration scmConfiguration, GroupManager groupManager) {
this.userManager = userManager;
this.passwordService = passwordService;
this.permissionAssigner = permissionAssigner;
this.scmConfiguration = scmConfiguration;
+ this.groupManager = groupManager;
}
@Override
@@ -92,6 +101,10 @@ public class SetupContextListener implements ServletContextListener {
if (anonymousUserRequiredButNotExists()) {
userManager.create(SCMContext.ANONYMOUS);
}
+
+ if (authenticatedGroupDoesNotExists()) {
+ createAuthenticatedGroup();
+ }
}
private boolean anonymousUserRequiredButNotExists() {
@@ -115,5 +128,16 @@ public class SetupContextListener implements ServletContextListener {
PermissionDescriptor descriptor = new PermissionDescriptor("*");
permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor));
}
+
+ private boolean authenticatedGroupDoesNotExists() {
+ return groupManager.get(AUTHENTICATED) == null;
+ }
+
+ private void createAuthenticatedGroup() {
+ Group authenticated = new Group("xml", AUTHENTICATED);
+ authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION);
+ authenticated.setExternal(true);
+ groupManager.create(authenticated);
+ }
}
}
diff --git a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java
index 610ec39533..ee3bdbb1e5 100644
--- a/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java
+++ b/scm-webapp/src/main/java/sonia/scm/net/ahc/DefaultAdvancedHttpClient.java
@@ -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 contentTransformers, Provider sslContextProvider)
+ Tracer tracer, Set contentTransformers, Provider 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 contentTransformers;
-
+
/** ssl context provider */
private final Provider sslContextProvider;
+
+ /** tracer used for request tracing */
+ private final Tracer tracer;
}
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
index efc967df49..0ab33dd6b1 100644
--- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java
@@ -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 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);
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java
index 5c371aa323..bea6451028 100644
--- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java
@@ -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 {
diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/Tracing.java b/scm-webapp/src/main/java/sonia/scm/plugin/Tracing.java
new file mode 100644
index 0000000000..41903830f2
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/plugin/Tracing.java
@@ -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() {
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/trace/LoggingExporter.java b/scm-webapp/src/main/java/sonia/scm/trace/LoggingExporter.java
new file mode 100644
index 0000000000..1e46a81605
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/trace/LoggingExporter.java
@@ -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 logger;
+
+ @Inject
+ LoggingExporter() {
+ this(LOG::info);
+ }
+
+ LoggingExporter(Consumer 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 labels = span.getLabels();
+ if (!labels.isEmpty()) {
+ message.append(":");
+ for (Map.Entry e : labels.entrySet()) {
+ message.append("\n - ").append(e.getKey()).append(": ").append(e.getValue());
+ }
+ }
+ return message.toString();
+ }
+
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java
index f2e9516f1e..1e67b74df9 100644
--- a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java
@@ -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 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();
});
diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java
index fab7b789ad..e6984ff537 100644
--- a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java
@@ -37,6 +37,8 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
+import sonia.scm.group.Group;
+import sonia.scm.group.GroupManager;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
@@ -56,6 +58,8 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static sonia.scm.group.GroupCollector.AUTHENTICATED;
+import static sonia.scm.lifecycle.SetupContextListener.SetupAction.AUTHENTICATED_GROUP_DESCRIPTION;
@ExtendWith(MockitoExtension.class)
class SetupContextListenerTest {
@@ -75,6 +79,9 @@ class SetupContextListenerTest {
@Mock
ScmConfiguration scmConfiguration;
+ @Mock
+ private GroupManager groupManager;
+
@Mock
private PermissionAssigner permissionAssigner;
@@ -96,6 +103,7 @@ class SetupContextListenerTest {
@Test
void shouldCreateAdminAccountIfNoUserExistsAndAssignPermissions() {
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null);
@@ -108,6 +116,7 @@ class SetupContextListenerTest {
void shouldCreateAdminAccountIfOnlyAnonymousUserExistsAndAssignPermissions() {
when(userManager.getAll()).thenReturn(Lists.newArrayList(SCMContext.ANONYMOUS));
when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true);
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
when(passwordService.encryptPassword("scmadmin")).thenReturn("secret");
setupContextListener.contextInitialized(null);
@@ -135,6 +144,7 @@ class SetupContextListenerTest {
void shouldDoNothingOnSecondStart() {
List users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
setupContextListener.contextInitialized(null);
@@ -146,6 +156,7 @@ class SetupContextListenerTest {
void shouldCreateAnonymousUserIfRequired() {
List users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
setupContextListener.contextInitialized(null);
@@ -157,6 +168,7 @@ class SetupContextListenerTest {
void shouldNotCreateAnonymousUserIfNotRequired() {
List users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
setupContextListener.contextInitialized(null);
@@ -167,6 +179,7 @@ class SetupContextListenerTest {
void shouldNotCreateAnonymousUserIfAlreadyExists() {
List users = Lists.newArrayList(SCMContext.ANONYMOUS);
when(userManager.getAll()).thenReturn(users);
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
setupContextListener.contextInitialized(null);
@@ -174,6 +187,28 @@ class SetupContextListenerTest {
verify(userManager, times(1)).create(SCMContext.ANONYMOUS);
}
+ @Test
+ void shouldCreateAuthenticatedGroupIfMissing() {
+ when(groupManager.get(AUTHENTICATED)).thenReturn(null);
+
+ setupContextListener.contextInitialized(null);
+
+ Group authenticated = createAuthenticatedGroup();
+ authenticated.setDescription(AUTHENTICATED_GROUP_DESCRIPTION);
+ authenticated.setExternal(true);
+
+ verify(groupManager, times(1)).create(authenticated);
+ }
+
+ @Test
+ void shouldNotCreateAuthenticatedGroupIfAlreadyExists() {
+ when(groupManager.get(AUTHENTICATED)).thenReturn(createAuthenticatedGroup());
+
+ setupContextListener.contextInitialized(null);
+
+ verify(groupManager, never()).create(any());
+ }
+
private void verifyAdminPermissionsAssigned() {
ArgumentCaptor usernameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor> permissionCaptor = ArgumentCaptor.forClass(Collection.class);
@@ -192,4 +227,7 @@ class SetupContextListenerTest {
assertThat(user.getPassword()).isEqualTo("secret");
}
+ private Group createAuthenticatedGroup() {
+ return new Group("xml", AUTHENTICATED);
+ }
}
diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java
index 7da5ddfb8a..89004ece27 100644
--- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpClientTest.java
@@ -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();
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 transformers)
+ public TestingAdvacedHttpClient(ScmConfiguration configuration, Set transformers)
{
- super(configuration, transformers, new SSLContextProvider());
+ super(configuration, tracer, transformers, new SSLContextProvider());
}
//~--- methods ------------------------------------------------------------
@@ -364,4 +417,10 @@ public class DefaultAdvancedHttpClientTest
/** Field description */
private Set transformers;
+
+ @Mock
+ private Tracer tracer;
+
+ @Mock
+ private Span span;
}
diff --git a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java
index 0496b318e6..e358820a2e 100644
--- a/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/net/ahc/DefaultAdvancedHttpResponseTest.java
@@ -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;
}
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
index cc59e42f62..ebd48a3b24 100644
--- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java
@@ -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 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 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 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);
diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java
index a619289094..7d18082833 100644
--- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java
@@ -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();
diff --git a/scm-webapp/src/test/java/sonia/scm/trace/LoggingExporterTest.java b/scm-webapp/src/test/java/sonia/scm/trace/LoggingExporterTest.java
new file mode 100644
index 0000000000..b1e285a5e2
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/trace/LoggingExporterTest.java
@@ -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");
+ }
+
+}
diff --git a/yarn.lock b/yarn.lock
index 92d3df06cc..90a978e77d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"