SLING-7031 - configurable component to write subset of metrics to log file on a recurring basis.
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1804334 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 663e5fe..d7791af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -238,6 +238,12 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>junit-addons</groupId>
+ <artifactId>junit-addons</artifactId>
+ <version>1.4</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.testing.paxexam</artifactId>
<version>0.0.4</version>
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/LogReporter.java b/src/main/java/org/apache/sling/commons/metrics/internal/LogReporter.java
new file mode 100644
index 0000000..f1f872f
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/LogReporter.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.commons.metrics.internal;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Slf4jReporter;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+@Component(service = {}, configurationPolicy = ConfigurationPolicy.REQUIRE)
+@Designate(ocd = LogReporter.Config.class, factory = true)
+public class LogReporter implements ServiceTrackerCustomizer<MetricRegistry, Slf4jReporter> {
+
+ private BundleContext context;
+ private ServiceTracker<MetricRegistry, Slf4jReporter> tracker;
+ private Config config;
+
+ @Activate
+ protected void activate(Config config, BundleContext context) {
+ this.config = config;
+ this.context = context;
+ tracker = new ServiceTracker<>(context, MetricRegistry.class, this);
+ tracker.open();
+ }
+
+ @Deactivate
+ protected void deactivate(BundleContext context) {
+ tracker.close();
+ }
+
+ //~---------------------------------------------< ServiceTracker >
+
+ @Override
+ public Slf4jReporter addingService(ServiceReference<MetricRegistry> serviceReference) {
+ MetricRegistry registry = context.getService(serviceReference);
+ String metricRegistryName = (String) serviceReference.getProperty(MetricWebConsolePlugin.METRIC_REGISTRY_NAME);
+
+ if (config.registryName() == null || config.registryName().length() == 0
+ || config.registryName().equals(metricRegistryName)) {
+ Slf4jReporter.Builder builder = Slf4jReporter.forRegistry(registry).
+ outputTo(LoggerFactory.getLogger(config.loggerName())).
+ withLoggingLevel(config.level());
+
+ if (config.prefix() != null && config.prefix().length() > 0) {
+ builder.filter(new PrefixFilter(config.prefix()));
+ } else if (config.pattern() != null && config.pattern().length() > 0) {
+ builder.filter(new PatternFilter(config.pattern()));
+ }
+
+ Slf4jReporter reporter = builder.build();
+ reporter.start(config.period(), config.timeUnit());
+ return reporter;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<MetricRegistry> serviceReference, Slf4jReporter reporter) {
+ // NO OP
+ }
+
+ @Override
+ public void removedService(ServiceReference<MetricRegistry> serviceReference, Slf4jReporter reporter) {
+ if (reporter != null) {
+ reporter.close();
+ }
+ }
+
+ private class PrefixFilter implements MetricFilter {
+ private final String prefix;
+
+ private PrefixFilter(String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public boolean matches(String s, Metric metric) {
+ return s.startsWith(prefix);
+ }
+ }
+
+ private class PatternFilter implements MetricFilter {
+ private final Pattern pattern;
+
+ private PatternFilter(String pattern) {
+ this.pattern = Pattern.compile(pattern);
+ }
+
+ @Override
+ public boolean matches(String s, Metric metric) {
+ return pattern.matcher(s).matches();
+ }
+ }
+
+ @ObjectClassDefinition(name = "Apache Sling Metrics Log Reporter Configuration")
+ @interface Config {
+
+ @AttributeDefinition(description = "Period at which the metrics data will be logged")
+ long period() default 5;
+
+ @AttributeDefinition(description = "Unit of time for evaluating the period")
+ TimeUnit timeUnit() default TimeUnit.MINUTES;
+
+ @AttributeDefinition(description = "The log level to log at.")
+ Slf4jReporter.LoggingLevel level() default Slf4jReporter.LoggingLevel.INFO;
+
+ @AttributeDefinition(description = "The logger name")
+ String loggerName() default "metrics";
+
+ @AttributeDefinition(description = "If specified, only metrics whose name starts with this value are logged. If both prefix and pattern are set, prefix is used.")
+ String prefix() default "";
+
+ @AttributeDefinition(description = "If specified, only metrics whose name matches this regular expression will be logged. If both prefix and pattern are set, prefix is used.")
+ String pattern() default "";
+
+ @AttributeDefinition(description = "Restrict the metrics logged to a specifically named registry.")
+ String registryName() default "";
+ }
+
+}
diff --git a/src/test/java/org/apache/sling/commons/metrics/internal/LogReporterTest.java b/src/test/java/org/apache/sling/commons/metrics/internal/LogReporterTest.java
new file mode 100644
index 0000000..67f1950
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/metrics/internal/LogReporterTest.java
@@ -0,0 +1,449 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.commons.metrics.internal;
+
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Slf4jReporter;
+import junitx.util.PrivateAccessor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+
+
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
+import java.lang.annotation.Annotation;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
+public class LogReporterTest {
+
+ @Mock
+ private BundleContext bundleContext;
+
+ LogReporter reporterService = new LogReporter();
+
+ @Test
+ public void testSpecificRegistryNameInclude() {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+ when(registryServiceReference.getProperty(MetricWebConsolePlugin.METRIC_REGISTRY_NAME)).thenReturn("oak");
+
+ LogReporter.Config config = createConfigWithRegistryName("oak");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNotNull(reporter);
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testSpecificRegistryNameExclude() {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+ when(registryServiceReference.getProperty(MetricWebConsolePlugin.METRIC_REGISTRY_NAME)).thenReturn("other");
+
+ LogReporter.Config config = createConfigWithRegistryName("oak");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNull(reporter);
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testSpecificRegistryNameExcludeNullName() {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+
+ LogReporter.Config config = createConfigWithRegistryName("oak");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNull(reporter);
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testLoggerName() throws Exception {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+
+ LogReporter.Config config = createConfigWithLoggerNameAndLevel("test", Slf4jReporter.LoggingLevel.WARN);
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNotNull(reporter);
+
+ Object loggerProxy = PrivateAccessor.getField(reporter, "loggerProxy");
+ assertEquals("WarnLoggerProxy", loggerProxy.getClass().getSimpleName());
+
+ Logger logger = (Logger) PrivateAccessor.getField(loggerProxy, "logger");
+ assertEquals("test", logger.getName());
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testPrefix() throws Exception {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+
+ LogReporter.Config config = createConfigWithPrefix("testPrefix");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNotNull(reporter);
+
+ MetricFilter filter = (MetricFilter) PrivateAccessor.getField(reporter, "filter");
+ assertEquals("PrefixFilter", filter.getClass().getSimpleName());
+ assertTrue(filter.matches("testPrefixedName", null));
+ assertFalse(filter.matches("testNonPrefixedName", null));
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testPattern() throws Exception {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+
+ LogReporter.Config config = createConfigWithPattern("[0-9]test.*");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNotNull(reporter);
+
+ MetricFilter filter = (MetricFilter) PrivateAccessor.getField(reporter, "filter");
+ assertEquals("PatternFilter", filter.getClass().getSimpleName());
+ assertTrue(filter.matches("5testTest", null));
+ assertFalse(filter.matches("ZtestTest", null));
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testPrefixAndPattern() throws Exception {
+ MetricRegistry registry = new MetricRegistry();
+ ServiceReference<MetricRegistry> registryServiceReference = mock(ServiceReference.class);
+ when(bundleContext.getService(registryServiceReference)).thenReturn(registry);
+
+ LogReporter.Config config = createConfigWithPrefixAndPattern("testPrefix", "[0-9]test.*");
+ reporterService.activate(config, bundleContext);
+
+ Slf4jReporter reporter = null;
+ try {
+ reporter = reporterService.addingService(registryServiceReference);
+ assertNotNull(reporter);
+
+ MetricFilter filter = (MetricFilter) PrivateAccessor.getField(reporter, "filter");
+ assertEquals("PrefixFilter", filter.getClass().getSimpleName());
+ assertTrue(filter.matches("testPrefixedName", null));
+ assertFalse(filter.matches("testNonPrefixedName", null));
+ } finally {
+ if (reporter != null) {
+ reporter.close();
+ }
+ reporterService.deactivate(bundleContext);
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ Slf4jReporter reporter = mock(Slf4jReporter.class);
+ reporterService.removedService(null, reporter);
+ verify(reporter, times(1)).close();
+ }
+
+ @Test
+ public void testNoOpCalls() {
+ // extra no-op calls for coverage
+ reporterService.removedService(null, null);
+ reporterService.modifiedService(null, null);
+ }
+
+ private LogReporter.Config createConfigWithRegistryName(final String registryName) {
+ return new LogReporter.Config() {
+ @Override
+ public long period() {
+ return 5;
+ }
+
+ @Override
+ public TimeUnit timeUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ public Slf4jReporter.LoggingLevel level() {
+ return Slf4jReporter.LoggingLevel.INFO;
+ }
+
+ @Override
+ public String loggerName() {
+ return "metrics";
+ }
+
+ @Override
+ public String prefix() {
+ return null;
+ }
+
+ @Override
+ public String pattern() {
+ return null;
+ }
+
+ @Override
+ public String registryName() {
+ return registryName;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return LogReporter.Config.class;
+ }
+ };
+ }
+
+ private LogReporter.Config createConfigWithLoggerNameAndLevel(final String loggerName, final Slf4jReporter.LoggingLevel level) {
+ return new LogReporter.Config() {
+ @Override
+ public long period() {
+ return 5;
+ }
+
+ @Override
+ public TimeUnit timeUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ public Slf4jReporter.LoggingLevel level() {
+ return level;
+ }
+
+ @Override
+ public String loggerName() {
+ return loggerName;
+ }
+
+ @Override
+ public String prefix() {
+ return null;
+ }
+
+ @Override
+ public String pattern() {
+ return null;
+ }
+
+ @Override
+ public String registryName() {
+ return "";
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return LogReporter.Config.class;
+ }
+ };
+ }
+
+ private LogReporter.Config createConfigWithPrefix(final String prefix) {
+ return new LogReporter.Config() {
+ @Override
+ public long period() {
+ return 5;
+ }
+
+ @Override
+ public TimeUnit timeUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ public Slf4jReporter.LoggingLevel level() {
+ return Slf4jReporter.LoggingLevel.INFO;
+ }
+
+ @Override
+ public String loggerName() {
+ return "metrics";
+ }
+
+ @Override
+ public String prefix() {
+ return prefix;
+ }
+
+ @Override
+ public String pattern() {
+ return null;
+ }
+
+ @Override
+ public String registryName() {
+ return null;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return LogReporter.Config.class;
+ }
+ };
+ }
+
+ private LogReporter.Config createConfigWithPattern(final String pattern) {
+ return new LogReporter.Config() {
+ @Override
+ public long period() {
+ return 5;
+ }
+
+ @Override
+ public TimeUnit timeUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ public Slf4jReporter.LoggingLevel level() {
+ return Slf4jReporter.LoggingLevel.INFO;
+ }
+
+ @Override
+ public String loggerName() {
+ return "metrics";
+ }
+
+ @Override
+ public String prefix() {
+ return null;
+ }
+
+ @Override
+ public String pattern() {
+ return pattern;
+ }
+
+ @Override
+ public String registryName() {
+ return null;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return LogReporter.Config.class;
+ }
+ };
+ }
+
+ private LogReporter.Config createConfigWithPrefixAndPattern(final String prefix, final String pattern) {
+ return new LogReporter.Config() {
+ @Override
+ public long period() {
+ return 5;
+ }
+
+ @Override
+ public TimeUnit timeUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ public Slf4jReporter.LoggingLevel level() {
+ return Slf4jReporter.LoggingLevel.INFO;
+ }
+
+ @Override
+ public String loggerName() {
+ return "metrics";
+ }
+
+ @Override
+ public String prefix() {
+ return prefix;
+ }
+
+ @Override
+ public String pattern() {
+ return pattern;
+ }
+
+ @Override
+ public String registryName() {
+ return null;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return LogReporter.Config.class;
+ }
+ };
+ }
+}