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

@@ -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<Optional<UpdateInfo>> 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<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.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;
}

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.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<MeterRegistry> {
private static final Logger LOG = LoggerFactory.getLogger(MeterRegistryProvider.class);
private final Set<MonitoringSystem> providerSet;
@Inject
@@ -58,15 +62,20 @@ public class MeterRegistryProvider implements Provider<MeterRegistry> {
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;
}

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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 <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 */
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) {

View File

@@ -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 --------------------------------------------------------------