Core metrics (#1586)

Expose metrics for http requests and executor services.
This commit is contained in:
Sebastian Sdorra
2021-03-17 11:09:52 +01:00
committed by GitHub
parent 4dbcc019b7
commit 26b65582ce
25 changed files with 632 additions and 93 deletions

View File

@@ -0,0 +1,4 @@
- type: added
description: Metrics for http requests ([#1586](https://github.com/scm-manager/scm-manager/issues/1586))
- type: added
description: Metrics for executor services ([#1586](https://github.com/scm-manager/scm-manager/issues/1586))

View File

@@ -0,0 +1,60 @@
/*
* 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.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
/**
* Util methods to collect metrics from known apis.
*
* @since 2.16.0
*/
public final class Metrics {
private Metrics() {
}
/**
* Collect metrics from an {@link ExecutorService}.
*
* @param registry meter registry
* @param executorService executor service to monitor
* @param name name of executor service
* @param type type of executor service e.g.: cached, fixed, etc.
*/
public static void executor(MeterRegistry registry, ExecutorService executorService, String name, String type) {
new ExecutorServiceMetrics(
executorService,
name,
Collections.singleton(Tag.of("type", type))
).bindTo(registry);
}
}

View File

@@ -25,8 +25,10 @@
package sonia.scm.web; package sonia.scm.web;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.metrics.Metrics;
import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.RepositoryHookType;
import sonia.scm.repository.spi.GitHookContextProvider; import sonia.scm.repository.spi.GitHookContextProvider;
import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.spi.HookEventFacade;
@@ -59,9 +61,9 @@ public class GitHookEventFacade implements Closeable {
private final ExecutorService internalThreadHookHandler; private final ExecutorService internalThreadHookHandler;
@Inject @Inject
public GitHookEventFacade(HookEventFacade hookEventFacade) { public GitHookEventFacade(HookEventFacade hookEventFacade, MeterRegistry registry) {
this.hookEventFacade = hookEventFacade; this.hookEventFacade = hookEventFacade;
this.internalThreadHookHandler = createInternalThreadHookHandlerPool(); this.internalThreadHookHandler = createInternalThreadHookHandlerPool(registry);
} }
public void fire(RepositoryHookType type, GitHookContextProvider context) { public void fire(RepositoryHookType type, GitHookContextProvider context) {
@@ -122,12 +124,14 @@ public class GitHookEventFacade implements Closeable {
} }
@Nonnull @Nonnull
private ExecutorService createInternalThreadHookHandlerPool() { private ExecutorService createInternalThreadHookHandlerPool(MeterRegistry registry) {
return Executors.newCachedThreadPool( ExecutorService executorService = Executors.newCachedThreadPool(
new ThreadFactoryBuilder() new ThreadFactoryBuilder()
.setNameFormat("GitInternalThreadHookHandler-%d") .setNameFormat("GitInternalThreadHookHandler-%d")
.build() .build()
); );
Metrics.executor(registry, executorService, "GitInternalHookHandler", "cached");
return executorService;
} }
@Override @Override

View File

@@ -24,6 +24,7 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.Transport;
import org.junit.rules.ExternalResource; import org.junit.rules.ExternalResource;
@@ -49,7 +50,7 @@ class BindTransportProtocolRule extends ExternalResource {
@Override @Override
protected void before() { protected void before() {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
hookEventFacade = new GitHookEventFacade(new HookEventFacade(of(repositoryManager), hookContextFactory)); hookEventFacade = new GitHookEventFacade(new HookEventFacade(of(repositoryManager), hookContextFactory), new SimpleMeterRegistry());
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
scmTransportProtocol = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); scmTransportProtocol = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler));

View File

@@ -24,6 +24,7 @@
package sonia.scm.repository.spi; package sonia.scm.repository.spi;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
@@ -67,7 +68,7 @@ public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase
@Before @Before
public void bindScmProtocol() throws IOException { public void bindScmProtocol() throws IOException {
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class)); HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
hookEventFacade = new GitHookEventFacade(new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory)); hookEventFacade = new GitHookEventFacade(new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory), new SimpleMeterRegistry());
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class); GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
proto = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); proto = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler));
Transport.register(proto); Transport.register(proto);

View File

@@ -25,11 +25,13 @@
package sonia.scm.repository.hooks; package sonia.scm.repository.hooks;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.metrics.Metrics;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.inject.Inject; import javax.inject.Inject;
@@ -48,6 +50,7 @@ public class HookServer implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(HookServer.class); private static final Logger LOG = LoggerFactory.getLogger(HookServer.class);
private final HookHandlerFactory handlerFactory; private final HookHandlerFactory handlerFactory;
private final MeterRegistry registry;
private ExecutorService acceptor; private ExecutorService acceptor;
private ExecutorService workerPool; private ExecutorService workerPool;
@@ -55,8 +58,9 @@ public class HookServer implements AutoCloseable {
private SecurityManager securityManager; private SecurityManager securityManager;
@Inject @Inject
public HookServer(HookHandlerFactory handlerFactory) { public HookServer(HookHandlerFactory handlerFactory, MeterRegistry registry) {
this.handlerFactory = handlerFactory; this.handlerFactory = handlerFactory;
this.registry = registry;
} }
public int start() throws IOException { public int start() throws IOException {
@@ -110,15 +114,19 @@ public class HookServer implements AutoCloseable {
} }
private ExecutorService createAcceptor() { private ExecutorService createAcceptor() {
return Executors.newSingleThreadExecutor( ExecutorService executorService = Executors.newSingleThreadExecutor(
createThreadFactory("HgHookAcceptor") createThreadFactory("HgHookAcceptor")
); );
Metrics.executor(registry, executorService, "HgHookServerAcceptor", "single");
return executorService;
} }
private ExecutorService createWorkerPool() { private ExecutorService createWorkerPool() {
return Executors.newCachedThreadPool( ExecutorService executorService = Executors.newCachedThreadPool(
createThreadFactory("HgHookWorker-%d") createThreadFactory("HgHookWorker-%d")
); );
Metrics.executor(registry, executorService, "HgHookServerWorker", "cached");
return executorService;
} }
@Nonnull @Nonnull

View File

@@ -24,6 +24,7 @@
package sonia.scm.repository.hooks; package sonia.scm.repository.hooks;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
@@ -74,7 +75,7 @@ class HookServerTest {
} }
private Response send(Request request) throws IOException { private Response send(Request request) throws IOException {
try (HookServer server = new HookServer(HelloHandler::new)) { try (HookServer server = new HookServer(HelloHandler::new, new SimpleMeterRegistry())) {
int port = server.start(); int port = server.start();
try ( try (
Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);

View File

@@ -26,8 +26,11 @@ package sonia.scm.admin;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.metrics.Metrics;
import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.version.Version; import sonia.scm.version.Version;
@@ -46,7 +49,7 @@ public class ReleaseFeedParser {
public static final int DEFAULT_TIMEOUT_IN_MILLIS = 5000; public static final int DEFAULT_TIMEOUT_IN_MILLIS = 5000;
private static final Logger LOG = LoggerFactory.getLogger(ReleaseFeedParser.class); private static final Logger LOG = LoggerFactory.getLogger(ReleaseFeedParser.class);
@VisibleForTesting @VisibleForTesting
static final String SPAN_KIND = "Release Feed"; static final String SPAN_KIND = "Release Feed";
@@ -56,14 +59,24 @@ public class ReleaseFeedParser {
private Future<Optional<UpdateInfo>> updateInfoFuture; private Future<Optional<UpdateInfo>> updateInfoFuture;
@Inject @Inject
public ReleaseFeedParser(AdvancedHttpClient client) { public ReleaseFeedParser(AdvancedHttpClient client, MeterRegistry registry) {
this(client, DEFAULT_TIMEOUT_IN_MILLIS); this(client, registry, DEFAULT_TIMEOUT_IN_MILLIS);
} }
public ReleaseFeedParser(AdvancedHttpClient client, long timeoutInMillis) { public ReleaseFeedParser(AdvancedHttpClient client, MeterRegistry registry, long timeoutInMillis) {
this.client = client; this.client = client;
this.timeoutInMillis = timeoutInMillis; this.timeoutInMillis = timeoutInMillis;
this.executorService = Executors.newSingleThreadExecutor(); this.executorService = createExecutorService(registry);
}
private ExecutorService createExecutorService(MeterRegistry registry) {
ExecutorService executor = Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder()
.setNameFormat("ReleaseFeedParser")
.build()
);
Metrics.executor(registry, executor, "ReleaseFeedParser", "single");
return executor;
} }
Optional<UpdateInfo> findLatestRelease(String url) { Optional<UpdateInfo> findLatestRelease(String url) {

View File

@@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.micrometer.core.instrument.MeterRegistry;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -43,6 +44,7 @@ import sonia.scm.importexport.ExportFileExtensionResolver;
import sonia.scm.importexport.ExportService; import sonia.scm.importexport.ExportService;
import sonia.scm.importexport.FullScmRepositoryExporter; import sonia.scm.importexport.FullScmRepositoryExporter;
import sonia.scm.importexport.RepositoryImportExportEncryption; import sonia.scm.importexport.RepositoryImportExportEncryption;
import sonia.scm.metrics.Metrics;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
@@ -104,7 +106,10 @@ public class RepositoryExportResource {
RepositoryImportExportEncryption repositoryImportExportEncryption, RepositoryImportExportEncryption repositoryImportExportEncryption,
ExportService exportService, ExportService exportService,
RepositoryExportInformationToDtoMapper informationToDtoMapper, RepositoryExportInformationToDtoMapper informationToDtoMapper,
ExportFileExtensionResolver fileExtensionResolver, ResourceLinks resourceLinks) { ExportFileExtensionResolver fileExtensionResolver,
ResourceLinks resourceLinks,
MeterRegistry registry
) {
this.manager = manager; this.manager = manager;
this.serviceFactory = serviceFactory; this.serviceFactory = serviceFactory;
this.fullScmRepositoryExporter = fullScmRepositoryExporter; this.fullScmRepositoryExporter = fullScmRepositoryExporter;
@@ -113,7 +118,7 @@ public class RepositoryExportResource {
this.informationToDtoMapper = informationToDtoMapper; this.informationToDtoMapper = informationToDtoMapper;
this.fileExtensionResolver = fileExtensionResolver; this.fileExtensionResolver = fileExtensionResolver;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.repositoryExportHandler = this.createExportHandlerPool(); this.repositoryExportHandler = this.createExportHandlerPool(registry);
} }
/** /**
@@ -518,12 +523,14 @@ public class RepositoryExportResource {
return Instant.now().toString().replace(":", "-").split("\\.")[0]; return Instant.now().toString().replace(":", "-").split("\\.")[0];
} }
private ExecutorService createExportHandlerPool() { private ExecutorService createExportHandlerPool(MeterRegistry registry) {
return Executors.newCachedThreadPool( ExecutorService executorService = Executors.newCachedThreadPool(
new ThreadFactoryBuilder() new ThreadFactoryBuilder()
.setNameFormat("RepositoryExportHandler-%d") .setNameFormat("RepositoryExportHandler-%d")
.build() .build()
); );
Metrics.executor(registry, executorService, "RepositoryExport", "cached");
return executorService;
} }

View File

@@ -0,0 +1,86 @@
/*
* 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.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.DefaultHttpServletRequestTagsProvider;
import io.micrometer.core.instrument.binder.http.HttpServletRequestTagsProvider;
import sonia.scm.Priority;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
import sonia.scm.web.filter.HttpFilter;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebElement(Filters.PATTERN_ALL)
@Priority(Filters.PRIORITY_PRE_BASEURL)
public class HttpMetricsFilter extends HttpFilter {
static final String METRIC_DURATION = "scm.http.requests";
private final HttpServletRequestTagsProvider tagsProvider = new DefaultHttpServletRequestTagsProvider();
private final Provider<MeterRegistry> registryProvider;
private final RequestCategoryDetector detector;
@Inject
public HttpMetricsFilter(Provider<MeterRegistry> registryProvider, RequestCategoryDetector detector) {
this.registryProvider = registryProvider;
this.detector = detector;
}
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
MeterRegistry registry = registryProvider.get();
Timer.Sample sample = Timer.start(registry);
try {
chain.doFilter(request, response);
} finally {
Tags tags = tags(request, response);
sample.stop(timer(registry, tags));
}
}
private Timer timer(MeterRegistry registry, Tags tags) {
return Timer.builder(METRIC_DURATION)
.description("Duration of an http request")
.tags(tags)
.register(registry);
}
private Tags tags(HttpServletRequest request, HttpServletResponse response) {
Iterable<Tag> tags = tagsProvider.getTags(request, response);
return Tags.concat(tags, "category", detector.detect(request).name());
}
}

View File

@@ -31,6 +31,8 @@ import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics;
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics; import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider; import javax.inject.Provider;
@@ -40,6 +42,8 @@ import java.util.Set;
@Singleton @Singleton
public class MeterRegistryProvider implements Provider<MeterRegistry> { public class MeterRegistryProvider implements Provider<MeterRegistry> {
private static final Logger LOG = LoggerFactory.getLogger(MeterRegistryProvider.class);
private final Set<MonitoringSystem> providerSet; private final Set<MonitoringSystem> providerSet;
@Inject @Inject
@@ -58,15 +62,20 @@ public class MeterRegistryProvider implements Provider<MeterRegistry> {
private MeterRegistry createRegistry() { private MeterRegistry createRegistry() {
if (providerSet.size() == 1) { if (providerSet.size() == 1) {
return providerSet.iterator().next().getRegistry(); MeterRegistry registry = providerSet.iterator().next().getRegistry();
LOG.debug("create meter registry from single registration: {}", registry.getClass());
return registry;
} }
return createCompositeRegistry(); return createCompositeRegistry();
} }
private CompositeMeterRegistry createCompositeRegistry() { private CompositeMeterRegistry createCompositeRegistry() {
LOG.debug("create composite meter registry");
CompositeMeterRegistry registry = new CompositeMeterRegistry(); CompositeMeterRegistry registry = new CompositeMeterRegistry();
for (MonitoringSystem provider : providerSet) { for (MonitoringSystem provider : providerSet) {
registry.add(provider.getRegistry()); MeterRegistry subRegistry = provider.getRegistry();
LOG.debug("register {} as part of composite meter registry", subRegistry.getClass());
registry.add(subRegistry);
} }
return registry; return registry;
} }

View File

@@ -0,0 +1,32 @@
/*
* 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.metrics;
public enum RequestCategory {
UI, API, PROTOCOL, STATIC, UNKNOWN
}

View File

@@ -0,0 +1,74 @@
/*
* 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.metrics;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.UserAgent;
import sonia.scm.web.UserAgentParser;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
public final class RequestCategoryDetector {
private final UserAgentParser userAgentParser;
@Inject
public RequestCategoryDetector(UserAgentParser userAgentParser) {
this.userAgentParser = userAgentParser;
}
public RequestCategory detect(HttpServletRequest request) {
String uri = HttpUtil.getStrippedURI(request);
if (isStatic(uri)) {
return RequestCategory.STATIC;
} else if (HttpUtil.isWUIRequest(request)) {
return RequestCategory.UI;
} else if (uri.startsWith("/api/")) {
return RequestCategory.API;
} else if (uri.startsWith("/repo/") && isScmClient(request)) {
return RequestCategory.PROTOCOL;
}
return RequestCategory.UNKNOWN;
}
private boolean isStatic(String uri) {
return uri.startsWith("/assets")
|| uri.endsWith(".js")
|| uri.endsWith(".css")
|| uri.endsWith(".jpg")
|| uri.endsWith(".jpeg")
|| uri.endsWith(".png")
|| uri.endsWith(".gif")
|| uri.endsWith(".svg")
|| uri.endsWith(".html");
}
private boolean isScmClient(HttpServletRequest request) {
UserAgent agent = userAgentParser.parse(request);
return agent != null && agent.isScmClient();
}
}

View File

@@ -26,10 +26,8 @@ package sonia.scm.repository;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.apache.shiro.concurrent.SubjectAwareExecutorService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ConfigurationException; import sonia.scm.ConfigurationException;
@@ -56,9 +54,6 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -75,10 +70,7 @@ import static sonia.scm.NotFoundException.notFound;
@Singleton @Singleton
public class DefaultRepositoryManager extends AbstractRepositoryManager { public class DefaultRepositoryManager extends AbstractRepositoryManager {
private static final String THREAD_NAME = "Hook-%s"; private static final Logger logger = LoggerFactory.getLogger(DefaultRepositoryManager.class);
private static final Logger logger =
LoggerFactory.getLogger(DefaultRepositoryManager.class);
private final ExecutorService executorService;
private final Map<String, RepositoryHandler> handlerMap; private final Map<String, RepositoryHandler> handlerMap;
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;
private final RepositoryDAO repositoryDAO; private final RepositoryDAO repositoryDAO;
@@ -94,12 +86,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
this.repositoryDAO = repositoryDAO; this.repositoryDAO = repositoryDAO;
this.namespaceStrategyProvider = namespaceStrategyProvider; this.namespaceStrategyProvider = namespaceStrategyProvider;
ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat(THREAD_NAME).build();
this.executorService = new SubjectAwareExecutorService(
Executors.newCachedThreadPool(factory)
);
handlerMap = new HashMap<>(); handlerMap = new HashMap<>();
types = new HashSet<>(); types = new HashSet<>();
@@ -109,11 +95,8 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
managerDaoAdapter = new ManagerDaoAdapter<>(repositoryDAO); managerDaoAdapter = new ManagerDaoAdapter<>(repositoryDAO);
} }
@Override @Override
public void close() { public void close() {
executorService.shutdown();
for (RepositoryHandler handler : handlerMap.values()) { for (RepositoryHandler handler : handlerMap.values()) {
IOUtil.close(handler); IOUtil.close(handler);
} }

View File

@@ -21,9 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.repository; package sonia.scm.repository;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import sonia.scm.metrics.Metrics;
import sonia.scm.repository.spi.SyncAsyncExecutor; import sonia.scm.repository.spi.SyncAsyncExecutor;
import sonia.scm.repository.spi.SyncAsyncExecutorProvider; import sonia.scm.repository.spi.SyncAsyncExecutorProvider;
@@ -48,8 +51,19 @@ public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvid
private final int defaultMaxAsyncAbortSeconds; private final int defaultMaxAsyncAbortSeconds;
@Inject @Inject
public DefaultSyncAsyncExecutorProvider() { public DefaultSyncAsyncExecutorProvider(MeterRegistry registry) {
this(Executors.newFixedThreadPool(getProperty(NUMBER_OF_THREADS_PROPERTY, DEFAULT_NUMBER_OF_THREADS))); this(createExecutorService(registry, getProperty(NUMBER_OF_THREADS_PROPERTY, DEFAULT_NUMBER_OF_THREADS)));
}
private static ExecutorService createExecutorService(MeterRegistry registry, int fixed) {
ExecutorService executorService = Executors.newFixedThreadPool(
fixed,
new ThreadFactoryBuilder()
.setNameFormat("SyncAsyncExecutorProvider-%d")
.build()
);
Metrics.executor(registry, executorService, "SyncAsyncExecutorProvider", "fixed");
return executorService;
} }
public DefaultSyncAsyncExecutorProvider(ExecutorService executor) { public DefaultSyncAsyncExecutorProvider(ExecutorService executor) {

View File

@@ -21,11 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.schedule; package sonia.scm.schedule;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.metrics.Metrics;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@@ -44,14 +46,16 @@ public class CronScheduler implements Scheduler {
private final CronThreadFactory threadFactory; private final CronThreadFactory threadFactory;
@Inject @Inject
public CronScheduler(CronTaskFactory taskFactory) { public CronScheduler(CronTaskFactory taskFactory, MeterRegistry registry) {
this.taskFactory = taskFactory; this.taskFactory = taskFactory;
this.threadFactory = new CronThreadFactory(); this.threadFactory = new CronThreadFactory();
this.executorService = createExecutor(); this.executorService = createExecutor(registry);
} }
private ScheduledExecutorService createExecutor() { private ScheduledExecutorService createExecutor(MeterRegistry registry) {
return Executors.newScheduledThreadPool(2, threadFactory); ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, threadFactory);
Metrics.executor(registry, executor, "Cron", "scheduled");
return executor;
} }
@Override @Override

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.template; package sonia.scm.template;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
@@ -32,16 +32,19 @@ import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.Default; import sonia.scm.Default;
import sonia.scm.metrics.Metrics;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
import javax.annotation.Nullable;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -61,37 +64,43 @@ public class MustacheTemplateEngine implements TemplateEngine
@Inject(optional = true) PluginLoader pluginLoader; @Inject(optional = true) PluginLoader pluginLoader;
} }
/**
* Used to implement optional injection for the MeterRegistry.
* @see <a href="https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-can-i-inject-optional-parameters-into-a-constructor">Optional Injection</a>
*/
static class MeterRegistryHolder {
@Inject(optional = true) MeterRegistry registry;
}
/** Field description */ /** Field description */
public static final TemplateType TYPE = new TemplateType("mustache", public static final TemplateType TYPE = new TemplateType("mustache",
"Mustache", "mustache"); "Mustache", "mustache");
/** Field description */
private static final String THREAD_NAME = "Mustache-%s";
/** /**
* the logger for MustacheTemplateEngine * the logger for MustacheTemplateEngine
*/ */
private static final Logger logger = private static final Logger logger =
LoggerFactory.getLogger(MustacheTemplateEngine.class); LoggerFactory.getLogger(MustacheTemplateEngine.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param context
* @param pluginLoaderHolder
*/
@Inject @Inject
public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder) public MustacheTemplateEngine(@Default ServletContext context, PluginLoaderHolder pluginLoaderHolder, MeterRegistryHolder registryHolder)
{ {
factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader)); factory = new ServletMustacheFactory(context, createClassLoader(pluginLoaderHolder.pluginLoader));
factory.setExecutorService(createExecutorService(registryHolder.registry));
}
ThreadFactory threadFactory = private static ExecutorService createExecutorService(@Nullable MeterRegistry registry) {
new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build(); ExecutorService executorService = Executors.newCachedThreadPool(
new ThreadFactoryBuilder()
.setNameFormat("MustacheTemplateEngine-%d")
.build()
);
factory.setExecutorService(Executors.newCachedThreadPool(threadFactory)); if (registry != null) {
Metrics.executor(registry, executorService,"MustacheTemplateEngine", "cached" );
}
return executorService;
} }
private ClassLoader createClassLoader(PluginLoader pluginLoader) { private ClassLoader createClassLoader(PluginLoader pluginLoader) {

View File

@@ -21,23 +21,24 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.web.cgi; package sonia.scm.web.cgi;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.MeterRegistry;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.metrics.Metrics;
//~--- JDK imports ------------------------------------------------------------ import javax.inject.Inject;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
@@ -50,13 +51,19 @@ public class DefaultCGIExecutorFactory implements CGIExecutorFactory, AutoClosea
* Constructs ... * Constructs ...
* *
*/ */
public DefaultCGIExecutorFactory() @Inject
{ public DefaultCGIExecutorFactory(MeterRegistry registry) {
//J- this.executor = createExecutor(registry);
this.executor = Executors.newCachedThreadPool( }
new ThreadFactoryBuilder().setNameFormat("cgi-pool-%d").build()
private ExecutorService createExecutor(MeterRegistry registry) {
ExecutorService executorService = Executors.newCachedThreadPool(
new ThreadFactoryBuilder()
.setNameFormat("cgi-pool-%d")
.build()
); );
//J+ Metrics.executor(registry, executorService, "CGI", "cached");
return executorService;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------

View File

@@ -25,6 +25,7 @@
package sonia.scm.admin; package sonia.scm.admin;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -57,7 +58,7 @@ class ReleaseFeedParserTest {
@BeforeEach @BeforeEach
void createSut() { void createSut() {
releaseFeedParser = new ReleaseFeedParser(client, 500); releaseFeedParser = new ReleaseFeedParser(client, new SimpleMeterRegistry(), 500);
} }
@Test @Test

View File

@@ -28,6 +28,7 @@ import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpRequest;
@@ -178,7 +179,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
super.repositoryImportResource = new RepositoryImportResource(dtoToRepositoryMapper, resourceLinks, fullScmRepositoryImporter, new RepositoryImportDtoToRepositoryImportParametersMapperImpl(), repositoryImportExportEncryption, fromUrlImporter, fromBundleImporter, importLoggerFactory); super.repositoryImportResource = new RepositoryImportResource(dtoToRepositoryMapper, resourceLinks, fullScmRepositoryImporter, new RepositoryImportDtoToRepositoryImportParametersMapperImpl(), repositoryImportExportEncryption, fromUrlImporter, fromBundleImporter, importLoggerFactory);
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks); super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks, new SimpleMeterRegistry());
dispatcher.addSingletonResource(getRepositoryRootResource()); dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service);
doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator();

View File

@@ -0,0 +1,114 @@
/*
* 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.metrics;
import com.google.inject.util.Providers;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.binder.http.Outcome;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class HttpMetricsFilterTest {
private MeterRegistry registry;
@BeforeEach
void setUpRegistry() {
registry = new SimpleMeterRegistry();
}
@Test
void shouldCollectMetrics() throws IOException, ServletException {
filter("GET", HttpServletResponse.SC_OK);
filter("GET", HttpServletResponse.SC_OK);
Timer timer = timer("GET", HttpServletResponse.SC_OK, Outcome.SUCCESS);
assertThat(timer.count()).isEqualTo(2);
}
@Test
void shouldCollectDifferentMetrics() throws IOException, ServletException {
filter("GET", HttpServletResponse.SC_OK);
filter("POST", HttpServletResponse.SC_CREATED);
filter("DELETE", HttpServletResponse.SC_NOT_FOUND);
Timer ok = timer("GET", HttpServletResponse.SC_OK, Outcome.SUCCESS);
Timer created = timer("POST", HttpServletResponse.SC_CREATED, Outcome.SUCCESS);
Timer notFound = timer("DELETE", HttpServletResponse.SC_NOT_FOUND, Outcome.CLIENT_ERROR);
assertThat(ok.count()).isEqualTo(1);
assertThat(created.count()).isEqualTo(1);
assertThat(notFound.count()).isEqualTo(1);
}
private Timer timer(String method, int status, Outcome outcome) {
return registry.get(HttpMetricsFilter.METRIC_DURATION)
.tags("category", "UNKNOWN", "method", method, "outcome", outcome.name(), "status", String.valueOf(status))
.timer();
}
private void filter(String requestMethod, int responseStatus) throws IOException, ServletException {
HttpServletRequest request = request(requestMethod);
HttpServletResponse response = response(responseStatus);
FilterChain chain = chain();
filter(request, response, chain);
}
private void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException, IOException, ServletException {
RequestCategoryDetector detector = mock(RequestCategoryDetector.class);
when(detector.detect(request)).thenReturn(RequestCategory.UNKNOWN);
HttpMetricsFilter filter = new HttpMetricsFilter(Providers.of(registry), detector);
filter.doFilter(request, response, chain);
}
private FilterChain chain() {
return mock(FilterChain.class);
}
private HttpServletRequest request(String method) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getMethod()).thenReturn(method);
return request;
}
private HttpServletResponse response(int status) {
HttpServletResponse response = mock(HttpServletResponse.class);
when(response.getStatus()).thenReturn(status);
return response;
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.metrics;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.util.HttpUtil;
import sonia.scm.web.UserAgent;
import sonia.scm.web.UserAgentParser;
import javax.servlet.http.HttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RequestCategoryDetectorTest {
@Mock
private UserAgentParser userAgentParser;
@InjectMocks
private RequestCategoryDetector detector;
@Test
void shouldReturnStatic() {
assertThat(category("/assets/bla")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/assets/bla/foo/bar")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/some/path.jpg")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/some/path.css")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/some/path.js")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/my.png")).isEqualTo(RequestCategory.STATIC);
assertThat(category("/images/loading.svg")).isEqualTo(RequestCategory.STATIC);
}
@Test
void shouldReturnUi() {
RequestCategory category = category("/", HttpUtil.HEADER_SCM_CLIENT, HttpUtil.SCM_CLIENT_WUI);
assertThat(category).isEqualTo(RequestCategory.UI);
}
@Test
void shouldReturnApi() {
assertThat(category("/api/v2")).isEqualTo(RequestCategory.API);
}
@Test
void shouldReturnProtocol() {
HttpServletRequest request = request("/repo/my/repo");
when(userAgentParser.parse(request)).thenReturn(UserAgent.scmClient("MySCM").build());
assertThat(detector.detect(request)).isEqualTo(RequestCategory.PROTOCOL);
}
@Test
void shouldReturnUnknown() {
assertThat(category("/unknown")).isEqualTo(RequestCategory.UNKNOWN);
}
private RequestCategory category(String uri) {
HttpServletRequest request = request(uri);
return detector.detect(request);
}
private HttpServletRequest request(String uri) {
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getContextPath()).thenReturn("/scm");
when(request.getRequestURI()).thenReturn("/scm" + uri);
return request;
}
private RequestCategory category(String uri, String header, String value) {
HttpServletRequest request = request(uri);
when(request.getHeader(header)).thenReturn(value);
return detector.detect(request);
}
}

View File

@@ -48,7 +48,6 @@ import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.SCMContext; import sonia.scm.SCMContext;
import sonia.scm.ScmConstraintViolationException; import sonia.scm.ScmConstraintViolationException;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.HookContextFactory;

View File

@@ -21,9 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.schedule; package sonia.scm.schedule;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,7 +33,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@@ -56,7 +56,7 @@ class CronSchedulerTest {
@Test @Test
void shouldScheduleWithClass() { void shouldScheduleWithClass() {
when(task.hasNextRun()).thenReturn(true); when(task.hasNextRun()).thenReturn(true);
try (CronScheduler scheduler = new CronScheduler(taskFactory)) { try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) {
scheduler.schedule("vep", TestingRunnable.class); scheduler.schedule("vep", TestingRunnable.class);
verify(task).setFuture(any(Future.class)); verify(task).setFuture(any(Future.class));
} }
@@ -65,7 +65,7 @@ class CronSchedulerTest {
@Test @Test
void shouldScheduleWithRunnable() { void shouldScheduleWithRunnable() {
when(task.hasNextRun()).thenReturn(true); when(task.hasNextRun()).thenReturn(true);
try (CronScheduler scheduler = new CronScheduler(taskFactory)) { try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) {
scheduler.schedule("vep", new TestingRunnable()); scheduler.schedule("vep", new TestingRunnable());
verify(task).setFuture(any(Future.class)); verify(task).setFuture(any(Future.class));
} }
@@ -73,7 +73,7 @@ class CronSchedulerTest {
@Test @Test
void shouldSkipSchedulingWithoutNextRun(){ void shouldSkipSchedulingWithoutNextRun(){
try (CronScheduler scheduler = new CronScheduler(taskFactory)) { try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) {
scheduler.schedule("vep", new TestingRunnable()); scheduler.schedule("vep", new TestingRunnable());
verify(task, never()).setFuture(any(Future.class)); verify(task, never()).setFuture(any(Future.class));
} }

View File

@@ -21,12 +21,13 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.template; package sonia.scm.template;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.Test; import org.junit.Test;
import sonia.scm.plugin.PluginLoader; import sonia.scm.plugin.PluginLoader;
@@ -64,10 +65,13 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
when(loader.getUberClassLoader()).thenReturn( when(loader.getUberClassLoader()).thenReturn(
Thread.currentThread().getContextClassLoader()); Thread.currentThread().getContextClassLoader());
MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); MustacheTemplateEngine.PluginLoaderHolder pluginLoaderHolder = new MustacheTemplateEngine.PluginLoaderHolder();
holder.pluginLoader = loader; pluginLoaderHolder.pluginLoader = loader;
return new MustacheTemplateEngine(context, holder); MustacheTemplateEngine.MeterRegistryHolder meterRegistryHolder = new MustacheTemplateEngine.MeterRegistryHolder();
meterRegistryHolder.registry = new SimpleMeterRegistry();
return new MustacheTemplateEngine(context, pluginLoaderHolder, meterRegistryHolder);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -119,14 +123,15 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase
@Test @Test
public void testCreateEngineWithoutPluginLoader() throws IOException { public void testCreateEngineWithoutPluginLoader() throws IOException {
ServletContext context = mock(ServletContext.class); ServletContext context = mock(ServletContext.class);
MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); MustacheTemplateEngine.PluginLoaderHolder pluginLoaderHolder = new MustacheTemplateEngine.PluginLoaderHolder();
MustacheTemplateEngine engine = new MustacheTemplateEngine(context, holder); MustacheTemplateEngine.MeterRegistryHolder meterRegistryHolder = new MustacheTemplateEngine.MeterRegistryHolder();
MustacheTemplateEngine engine = new MustacheTemplateEngine(context, pluginLoaderHolder, meterRegistryHolder);
Template template = engine.getTemplate(getTemplateResource()); Template template = engine.getTemplate(getTemplateResource());
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
template.execute(writer, ImmutableMap.of("name", "World")); template.execute(writer, ImmutableMap.of("name", "World"));
Assertions.assertThat(writer.toString()).isEqualTo("Hello World!"); Assertions.assertThat(writer).hasToString("Hello World!");
} }
} }