From 26b65582ce0691a9efa255e14ebf4d889f127110 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 17 Mar 2021 11:09:52 +0100 Subject: [PATCH] Core metrics (#1586) Expose metrics for http requests and executor services. --- gradle/changelog/core_metrics.yaml | 4 + .../main/java/sonia/scm/metrics/Metrics.java | 60 +++++++++ .../sonia/scm/web/GitHookEventFacade.java | 12 +- .../spi/BindTransportProtocolRule.java | 3 +- .../spi/SimpleGitWorkingCopyFactoryTest.java | 3 +- .../scm/repository/hooks/HookServer.java | 14 ++- .../scm/repository/hooks/HookServerTest.java | 3 +- .../sonia/scm/admin/ReleaseFeedParser.java | 23 +++- .../resources/RepositoryExportResource.java | 15 ++- .../sonia/scm/metrics/HttpMetricsFilter.java | 86 +++++++++++++ .../scm/metrics/MeterRegistryProvider.java | 13 +- .../sonia/scm/metrics/RequestCategory.java | 32 +++++ .../scm/metrics/RequestCategoryDetector.java | 74 ++++++++++++ .../repository/DefaultRepositoryManager.java | 19 +-- .../DefaultSyncAsyncExecutorProvider.java | 20 ++- .../sonia/scm/schedule/CronScheduler.java | 14 ++- .../scm/template/MustacheTemplateEngine.java | 45 ++++--- .../web/cgi/DefaultCGIExecutorFactory.java | 33 +++-- .../scm/admin/ReleaseFeedParserTest.java | 3 +- .../resources/RepositoryRootResourceTest.java | 3 +- .../scm/metrics/HttpMetricsFilterTest.java | 114 ++++++++++++++++++ .../metrics/RequestCategoryDetectorTest.java | 102 ++++++++++++++++ .../DefaultRepositoryManagerTest.java | 1 - .../sonia/scm/schedule/CronSchedulerTest.java | 10 +- .../template/MustacheTemplateEngineTest.java | 19 +-- 25 files changed, 632 insertions(+), 93 deletions(-) create mode 100644 gradle/changelog/core_metrics.yaml create mode 100644 scm-core/src/main/java/sonia/scm/metrics/Metrics.java create mode 100644 scm-webapp/src/main/java/sonia/scm/metrics/HttpMetricsFilter.java create mode 100644 scm-webapp/src/main/java/sonia/scm/metrics/RequestCategory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/metrics/RequestCategoryDetector.java create mode 100644 scm-webapp/src/test/java/sonia/scm/metrics/HttpMetricsFilterTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/metrics/RequestCategoryDetectorTest.java diff --git a/gradle/changelog/core_metrics.yaml b/gradle/changelog/core_metrics.yaml new file mode 100644 index 0000000000..9092fd262e --- /dev/null +++ b/gradle/changelog/core_metrics.yaml @@ -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)) diff --git a/scm-core/src/main/java/sonia/scm/metrics/Metrics.java b/scm-core/src/main/java/sonia/scm/metrics/Metrics.java new file mode 100644 index 0000000000..7ca4d1a47f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/metrics/Metrics.java @@ -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); + } + +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHookEventFacade.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHookEventFacade.java index 32fef659a4..664372c08b 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHookEventFacade.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitHookEventFacade.java @@ -25,8 +25,10 @@ package sonia.scm.web; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.metrics.Metrics; import sonia.scm.repository.RepositoryHookType; import sonia.scm.repository.spi.GitHookContextProvider; import sonia.scm.repository.spi.HookEventFacade; @@ -59,9 +61,9 @@ public class GitHookEventFacade implements Closeable { private final ExecutorService internalThreadHookHandler; @Inject - public GitHookEventFacade(HookEventFacade hookEventFacade) { + public GitHookEventFacade(HookEventFacade hookEventFacade, MeterRegistry registry) { this.hookEventFacade = hookEventFacade; - this.internalThreadHookHandler = createInternalThreadHookHandlerPool(); + this.internalThreadHookHandler = createInternalThreadHookHandlerPool(registry); } public void fire(RepositoryHookType type, GitHookContextProvider context) { @@ -122,12 +124,14 @@ public class GitHookEventFacade implements Closeable { } @Nonnull - private ExecutorService createInternalThreadHookHandlerPool() { - return Executors.newCachedThreadPool( + private ExecutorService createInternalThreadHookHandlerPool(MeterRegistry registry) { + ExecutorService executorService = Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("GitInternalThreadHookHandler-%d") .build() ); + Metrics.executor(registry, executorService, "GitInternalHookHandler", "cached"); + return executorService; } @Override diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java index d949267e69..9e7cad94d4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/BindTransportProtocolRule.java @@ -24,6 +24,7 @@ package sonia.scm.repository.spi; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.eclipse.jgit.transport.ScmTransportProtocol; import org.eclipse.jgit.transport.Transport; import org.junit.rules.ExternalResource; @@ -49,7 +50,7 @@ class BindTransportProtocolRule extends ExternalResource { @Override protected void before() { 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); scmTransportProtocol = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java index 8f2a47796f..64532956f2 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkingCopyFactoryTest.java @@ -24,6 +24,7 @@ package sonia.scm.repository.spi; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.lib.Repository; @@ -67,7 +68,7 @@ public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase @Before public void bindScmProtocol() throws IOException { 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); proto = new ScmTransportProtocol(of(GitTestHelper.createConverterFactory()), of(hookEventFacade), of(gitRepositoryHandler)); Transport.register(proto); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java index 90a15c6fb4..fbb6de035a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/hooks/HookServer.java @@ -25,11 +25,13 @@ package sonia.scm.repository.hooks; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.micrometer.core.instrument.MeterRegistry; import org.apache.shiro.SecurityUtils; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.util.ThreadContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.metrics.Metrics; import javax.annotation.Nonnull; import javax.inject.Inject; @@ -48,6 +50,7 @@ public class HookServer implements AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(HookServer.class); private final HookHandlerFactory handlerFactory; + private final MeterRegistry registry; private ExecutorService acceptor; private ExecutorService workerPool; @@ -55,8 +58,9 @@ public class HookServer implements AutoCloseable { private SecurityManager securityManager; @Inject - public HookServer(HookHandlerFactory handlerFactory) { + public HookServer(HookHandlerFactory handlerFactory, MeterRegistry registry) { this.handlerFactory = handlerFactory; + this.registry = registry; } public int start() throws IOException { @@ -110,15 +114,19 @@ public class HookServer implements AutoCloseable { } private ExecutorService createAcceptor() { - return Executors.newSingleThreadExecutor( + ExecutorService executorService = Executors.newSingleThreadExecutor( createThreadFactory("HgHookAcceptor") ); + Metrics.executor(registry, executorService, "HgHookServerAcceptor", "single"); + return executorService; } private ExecutorService createWorkerPool() { - return Executors.newCachedThreadPool( + ExecutorService executorService = Executors.newCachedThreadPool( createThreadFactory("HgHookWorker-%d") ); + Metrics.executor(registry, executorService, "HgHookServerWorker", "cached"); + return executorService; } @Nonnull diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java index ccffd36b04..0acbd2749c 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/hooks/HookServerTest.java @@ -24,6 +24,7 @@ package sonia.scm.repository.hooks; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import lombok.AllArgsConstructor; import lombok.Data; import org.apache.shiro.SecurityUtils; @@ -74,7 +75,7 @@ class HookServerTest { } 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(); try ( Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); 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 e520864c35..34cfc0a29d 100644 --- a/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java +++ b/scm-webapp/src/main/java/sonia/scm/admin/ReleaseFeedParser.java @@ -26,8 +26,11 @@ package sonia.scm.admin; import com.google.common.annotations.VisibleForTesting; 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.LoggerFactory; +import sonia.scm.metrics.Metrics; import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.version.Version; @@ -46,7 +49,7 @@ public class ReleaseFeedParser { public static final int DEFAULT_TIMEOUT_IN_MILLIS = 5000; private static final Logger LOG = LoggerFactory.getLogger(ReleaseFeedParser.class); - + @VisibleForTesting static final String SPAN_KIND = "Release Feed"; @@ -56,14 +59,24 @@ public class ReleaseFeedParser { private Future> updateInfoFuture; @Inject - public ReleaseFeedParser(AdvancedHttpClient client) { - this(client, DEFAULT_TIMEOUT_IN_MILLIS); + public ReleaseFeedParser(AdvancedHttpClient client, MeterRegistry registry) { + 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.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 findLatestRelease(String url) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java index a8ed757954..6a0e74f2b0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java @@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources; import com.google.common.base.Strings; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; +import io.micrometer.core.instrument.MeterRegistry; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; 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.FullScmRepositoryExporter; import sonia.scm.importexport.RepositoryImportExportEncryption; +import sonia.scm.metrics.Metrics; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; @@ -104,7 +106,10 @@ public class RepositoryExportResource { RepositoryImportExportEncryption repositoryImportExportEncryption, ExportService exportService, RepositoryExportInformationToDtoMapper informationToDtoMapper, - ExportFileExtensionResolver fileExtensionResolver, ResourceLinks resourceLinks) { + ExportFileExtensionResolver fileExtensionResolver, + ResourceLinks resourceLinks, + MeterRegistry registry + ) { this.manager = manager; this.serviceFactory = serviceFactory; this.fullScmRepositoryExporter = fullScmRepositoryExporter; @@ -113,7 +118,7 @@ public class RepositoryExportResource { this.informationToDtoMapper = informationToDtoMapper; this.fileExtensionResolver = fileExtensionResolver; 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]; } - private ExecutorService createExportHandlerPool() { - return Executors.newCachedThreadPool( + private ExecutorService createExportHandlerPool(MeterRegistry registry) { + ExecutorService executorService = Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("RepositoryExportHandler-%d") .build() ); + Metrics.executor(registry, executorService, "RepositoryExport", "cached"); + return executorService; } diff --git a/scm-webapp/src/main/java/sonia/scm/metrics/HttpMetricsFilter.java b/scm-webapp/src/main/java/sonia/scm/metrics/HttpMetricsFilter.java new file mode 100644 index 0000000000..e2189e31f1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/metrics/HttpMetricsFilter.java @@ -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 registryProvider; + private final RequestCategoryDetector detector; + + @Inject + public HttpMetricsFilter(Provider 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 tags = tagsProvider.getTags(request, response); + return Tags.concat(tags, "category", detector.detect(request).name()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/metrics/MeterRegistryProvider.java b/scm-webapp/src/main/java/sonia/scm/metrics/MeterRegistryProvider.java index 5aa9ed3eea..cc5c1c348d 100644 --- a/scm-webapp/src/main/java/sonia/scm/metrics/MeterRegistryProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/metrics/MeterRegistryProvider.java @@ -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.system.ProcessorMetrics; import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Provider; @@ -40,6 +42,8 @@ import java.util.Set; @Singleton public class MeterRegistryProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(MeterRegistryProvider.class); + private final Set providerSet; @Inject @@ -58,15 +62,20 @@ public class MeterRegistryProvider implements Provider { private MeterRegistry createRegistry() { 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(); } private CompositeMeterRegistry createCompositeRegistry() { + LOG.debug("create composite meter registry"); CompositeMeterRegistry registry = new CompositeMeterRegistry(); 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; } diff --git a/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategory.java b/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategory.java new file mode 100644 index 0000000000..eff1847ad9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategory.java @@ -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 + +} diff --git a/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategoryDetector.java b/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategoryDetector.java new file mode 100644 index 0000000000..d433f5fbba --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/metrics/RequestCategoryDetector.java @@ -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(); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index eca8c5107d..73e3ea03e3 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -26,10 +26,8 @@ package sonia.scm.repository; import com.google.common.base.Strings; import com.google.common.collect.Lists; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.apache.shiro.concurrent.SubjectAwareExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ConfigurationException; @@ -56,9 +54,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; 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.Predicate; @@ -75,10 +70,7 @@ import static sonia.scm.NotFoundException.notFound; @Singleton public class DefaultRepositoryManager extends AbstractRepositoryManager { - private static final String THREAD_NAME = "Hook-%s"; - private static final Logger logger = - LoggerFactory.getLogger(DefaultRepositoryManager.class); - private final ExecutorService executorService; + private static final Logger logger = LoggerFactory.getLogger(DefaultRepositoryManager.class); private final Map handlerMap; private final KeyGenerator keyGenerator; private final RepositoryDAO repositoryDAO; @@ -94,12 +86,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { this.repositoryDAO = repositoryDAO; this.namespaceStrategyProvider = namespaceStrategyProvider; - ThreadFactory factory = new ThreadFactoryBuilder() - .setNameFormat(THREAD_NAME).build(); - this.executorService = new SubjectAwareExecutorService( - Executors.newCachedThreadPool(factory) - ); - handlerMap = new HashMap<>(); types = new HashSet<>(); @@ -109,11 +95,8 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { managerDaoAdapter = new ManagerDaoAdapter<>(repositoryDAO); } - @Override public void close() { - executorService.shutdown(); - for (RepositoryHandler handler : handlerMap.values()) { IOUtil.close(handler); } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java index 2323eb7c99..e99e77cc50 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultSyncAsyncExecutorProvider.java @@ -21,9 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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.SyncAsyncExecutorProvider; @@ -48,8 +51,19 @@ public class DefaultSyncAsyncExecutorProvider implements SyncAsyncExecutorProvid private final int defaultMaxAsyncAbortSeconds; @Inject - public DefaultSyncAsyncExecutorProvider() { - this(Executors.newFixedThreadPool(getProperty(NUMBER_OF_THREADS_PROPERTY, DEFAULT_NUMBER_OF_THREADS))); + public DefaultSyncAsyncExecutorProvider(MeterRegistry registry) { + 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) { diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java b/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java index 1aa3a92aec..c8ab89d72f 100644 --- a/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java +++ b/scm-webapp/src/main/java/sonia/scm/schedule/CronScheduler.java @@ -21,11 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.schedule; +import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.metrics.Metrics; import javax.inject.Inject; import javax.inject.Singleton; @@ -44,14 +46,16 @@ public class CronScheduler implements Scheduler { private final CronThreadFactory threadFactory; @Inject - public CronScheduler(CronTaskFactory taskFactory) { + public CronScheduler(CronTaskFactory taskFactory, MeterRegistry registry) { this.taskFactory = taskFactory; this.threadFactory = new CronThreadFactory(); - this.executorService = createExecutor(); + this.executorService = createExecutor(registry); } - private ScheduledExecutorService createExecutor() { - return Executors.newScheduledThreadPool(2, threadFactory); + private ScheduledExecutorService createExecutor(MeterRegistry registry) { + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, threadFactory); + Metrics.executor(registry, executor, "Cron", "scheduled"); + return executor; } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java index 7365af7c8b..2ce3d92b1e 100644 --- a/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.java +++ b/scm-webapp/src/main/java/sonia/scm/template/MustacheTemplateEngine.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.template; //~--- 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.UncheckedExecutionException; import com.google.inject.Inject; +import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.Default; +import sonia.scm.metrics.Metrics; import sonia.scm.plugin.PluginLoader; +import javax.annotation.Nullable; import javax.servlet.ServletContext; import java.io.IOException; import java.io.Reader; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; //~--- JDK imports ------------------------------------------------------------ @@ -61,37 +64,43 @@ public class MustacheTemplateEngine implements TemplateEngine @Inject(optional = true) PluginLoader pluginLoader; } + /** + * Used to implement optional injection for the MeterRegistry. + * @see Optional Injection + */ + static class MeterRegistryHolder { + @Inject(optional = true) MeterRegistry registry; + } + /** Field description */ public static final TemplateType TYPE = new TemplateType("mustache", "Mustache", "mustache"); - /** Field description */ - private static final String THREAD_NAME = "Mustache-%s"; - /** * the logger for MustacheTemplateEngine */ private static final Logger logger = LoggerFactory.getLogger(MustacheTemplateEngine.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param context - * @param pluginLoaderHolder - */ @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.setExecutorService(createExecutorService(registryHolder.registry)); + } - ThreadFactory threadFactory = - new ThreadFactoryBuilder().setNameFormat(THREAD_NAME).build(); + private static ExecutorService createExecutorService(@Nullable MeterRegistry registry) { + 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) { diff --git a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java index fee5a855a8..a0258495bb 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/web/cgi/DefaultCGIExecutorFactory.java @@ -21,23 +21,24 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web.cgi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.util.concurrent.ThreadFactoryBuilder; - +import io.micrometer.core.instrument.MeterRegistry; import sonia.scm.config.ScmConfiguration; +import sonia.scm.metrics.Metrics; -//~--- JDK imports ------------------------------------------------------------ - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - +import javax.inject.Inject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; 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 ... * */ - public DefaultCGIExecutorFactory() - { - //J- - this.executor = Executors.newCachedThreadPool( - new ThreadFactoryBuilder().setNameFormat("cgi-pool-%d").build() + @Inject + public DefaultCGIExecutorFactory(MeterRegistry registry) { + this.executor = createExecutor(registry); + } + + 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 -------------------------------------------------------------- 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 1e67b74df9..8276477784 100644 --- a/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java +++ b/scm-webapp/src/test/java/sonia/scm/admin/ReleaseFeedParserTest.java @@ -25,6 +25,7 @@ package sonia.scm.admin; import com.google.common.collect.ImmutableList; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,7 +58,7 @@ class ReleaseFeedParserTest { @BeforeEach void createSut() { - releaseFeedParser = new ReleaseFeedParser(client, 500); + releaseFeedParser = new ReleaseFeedParser(client, new SimpleMeterRegistry(), 500); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 67fd42911e..09aa7f6ed8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -28,6 +28,7 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import com.google.common.io.Resources; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.Subject; import org.jboss.resteasy.mock.MockHttpRequest; @@ -178,7 +179,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); 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()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); diff --git a/scm-webapp/src/test/java/sonia/scm/metrics/HttpMetricsFilterTest.java b/scm-webapp/src/test/java/sonia/scm/metrics/HttpMetricsFilterTest.java new file mode 100644 index 0000000000..13eba69f3b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/metrics/HttpMetricsFilterTest.java @@ -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; + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/metrics/RequestCategoryDetectorTest.java b/scm-webapp/src/test/java/sonia/scm/metrics/RequestCategoryDetectorTest.java new file mode 100644 index 0000000000..fbb4dcc6a6 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/metrics/RequestCategoryDetectorTest.java @@ -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); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 509d5f5d1e..1b9479a7db 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -48,7 +48,6 @@ import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.SCMContext; import sonia.scm.ScmConstraintViolationException; -import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.api.HookContext; import sonia.scm.repository.api.HookContextFactory; diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java index 0446008cf8..19a0e0a961 100644 --- a/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/schedule/CronSchedulerTest.java @@ -21,9 +21,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.schedule; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,7 +33,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.concurrent.Future; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -56,7 +56,7 @@ class CronSchedulerTest { @Test void shouldScheduleWithClass() { when(task.hasNextRun()).thenReturn(true); - try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) { scheduler.schedule("vep", TestingRunnable.class); verify(task).setFuture(any(Future.class)); } @@ -65,7 +65,7 @@ class CronSchedulerTest { @Test void shouldScheduleWithRunnable() { when(task.hasNextRun()).thenReturn(true); - try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) { scheduler.schedule("vep", new TestingRunnable()); verify(task).setFuture(any(Future.class)); } @@ -73,7 +73,7 @@ class CronSchedulerTest { @Test void shouldSkipSchedulingWithoutNextRun(){ - try (CronScheduler scheduler = new CronScheduler(taskFactory)) { + try (CronScheduler scheduler = new CronScheduler(taskFactory, new SimpleMeterRegistry())) { scheduler.schedule("vep", new TestingRunnable()); verify(task, never()).setFuture(any(Future.class)); } diff --git a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java index 49cac28352..fd3f5638a4 100644 --- a/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/template/MustacheTemplateEngineTest.java @@ -21,12 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.template; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableMap; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.assertj.core.api.Assertions; import org.junit.Test; import sonia.scm.plugin.PluginLoader; @@ -64,10 +65,13 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase when(loader.getUberClassLoader()).thenReturn( Thread.currentThread().getContextClassLoader()); - MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); - holder.pluginLoader = loader; + MustacheTemplateEngine.PluginLoaderHolder pluginLoaderHolder = new MustacheTemplateEngine.PluginLoaderHolder(); + 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 ---------------------------------------------------------- @@ -119,14 +123,15 @@ public class MustacheTemplateEngineTest extends TemplateEngineTestBase @Test public void testCreateEngineWithoutPluginLoader() throws IOException { ServletContext context = mock(ServletContext.class); - MustacheTemplateEngine.PluginLoaderHolder holder = new MustacheTemplateEngine.PluginLoaderHolder(); - MustacheTemplateEngine engine = new MustacheTemplateEngine(context, holder); + MustacheTemplateEngine.PluginLoaderHolder pluginLoaderHolder = new MustacheTemplateEngine.PluginLoaderHolder(); + MustacheTemplateEngine.MeterRegistryHolder meterRegistryHolder = new MustacheTemplateEngine.MeterRegistryHolder(); + MustacheTemplateEngine engine = new MustacheTemplateEngine(context, pluginLoaderHolder, meterRegistryHolder); Template template = engine.getTemplate(getTemplateResource()); StringWriter writer = new StringWriter(); template.execute(writer, ImmutableMap.of("name", "World")); - Assertions.assertThat(writer.toString()).isEqualTo("Hello World!"); + Assertions.assertThat(writer).hasToString("Hello World!"); } }