diff --git a/spring-cloud-stream-core-docs/src/main/asciidoc/spring-cloud-stream-overview.adoc b/spring-cloud-stream-core-docs/src/main/asciidoc/spring-cloud-stream-overview.adoc index dfa369dbe..0593912e1 100644 --- a/spring-cloud-stream-core-docs/src/main/asciidoc/spring-cloud-stream-overview.adoc +++ b/spring-cloud-stream-core-docs/src/main/asciidoc/spring-cloud-stream-overview.adoc @@ -1914,49 +1914,40 @@ Spring Cloud Stream provides a health indicator for binders. It is registered under the name of `binders` and can be enabled or disabled by setting the `management.health.binders.enabled` property. == Metrics Emitter + Spring Cloud Stream provides a module called `spring-cloud-stream-metrics` that can be used to emit any available metric from https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html[Spring Boot metrics endpoint] to a named channel. This module allow operators to collect metrics from stream applications without relying on polling their endpoints. -HTTP polling can be challenging on cloud environments since applications could be on a private network or behind a Load balancer that prevents per instance access. +The module is activated when you set the destination name for metrics binding, e.g. `spring.cloud.stream.bindings.applicationMetrics.destination=`. +`applicationMetrics` can be configured in a similar fashion to any other producer binding. +The default `contentType` setting of `applicationMetrics` is `application/json`. -The module is activated when you set the destination name for its channel, `spring.cloud.stream.bindings.applicationMetricsChannel.destination=`. -`applicationMetricsChannel` can be configured in a similar fashion to any other bound output channel. -The default `contentType` setting of `applicationMetricsChannel` is `application/json`. +The following properties can be used for customizing the emission of metrics: -By default the module is configured to only send Spring Integration message channel metrics. - -Available properties for customization using the prefix `spring.cloud.stream.metrics`. - -key:: +spring.cloud.stream.metrics.key:: The name of the metric being emitted. Should be an unique value per application. + Default:: `${spring.application.name:${vcap.application.name:${spring.config.name:application}}}` + -delay-millis:: - The period in which metrics will be posted to the channel -+ -Default: `5000`. -+ -prefix:: - Prefix string to be prepended to the metrics name +spring.cloud.stream.metrics.prefix:: + Prefix string to be prepended to the metrics key. + Default: `` + -includes:: - An array of strings containing regex patterns of metrics that should be included. -+ -Default: `integration**` -+ -excludes:: - An array of strings containing regex patterns of metrics that should be excluded. -+ -Default: `` -+ -properties:: +spring.cloud.stream.metrics.properties:: Just like the `includes` option, it allows white listing application properties that will be added to the metrics payload + Default: null. + +A detailed overview of the metrics export process can be found in the https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html#production-ready-metric-writers[Spring Boot reference documentation]. +Spring Cloud Stream provides a metric exporter named `application` that can be configured via regular https://github.com/spring-projects/spring-boot/blob/1.5.x/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/TriggerProperties.java[Spring Boot metrics configuration properties]. + +The exporter can be configured either by using the global Spring Boot configuration settings for exporters, or by using exporter-specific properties. +For using the global configuration settings, the properties should be prefixed by `spring.metric.export` (e.g. `spring.metric.export.includes=integration+++**+++`). +These configuration options will apply to all exporters (unless they have been configured differently). +Alternatively, if it is intended to use configuration settings that are different from the other exporters (e.g. for restricting the number of metrics published), the Spring Cloud Stream provided metrics exporter can be configured using the prefix `spring.metrics.export.triggers.application` (e.g. `spring.metrics.export.triggers.application.includes=integration+++**+++`). + [NOTE] ==== Due to Spring Boot's https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-relaxed-binding[relaxed binding] the value of a property being included can be slightly different than the original value. diff --git a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/ApplicationMetricsProperties.java b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/ApplicationMetricsProperties.java index 77f056cc1..b4eb240c8 100644 --- a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/ApplicationMetricsProperties.java +++ b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/ApplicationMetricsProperties.java @@ -24,6 +24,7 @@ import org.springframework.boot.actuate.metrics.export.MetricExportProperties; import org.springframework.boot.actuate.metrics.export.TriggerProperties; import org.springframework.boot.bind.RelaxedNames; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.stream.metrics.config.BinderMetricsAutoConfiguration; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; @@ -49,9 +50,9 @@ public class ApplicationMetricsProperties private String[] properties; private MetricExportProperties export; - + public TriggerProperties getTrigger() { - return export.findTrigger("application"); + return export.findTrigger(BinderMetricsAutoConfiguration.APPLICATION_METRICS_EXPORTER_TRIGGER_NAME); } public ApplicationMetricsProperties(MetricExportProperties export) { diff --git a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsAutoConfiguration.java b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsAutoConfiguration.java index ed9ecd3eb..4124bf254 100644 --- a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsAutoConfiguration.java +++ b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsAutoConfiguration.java @@ -16,7 +16,18 @@ package org.springframework.cloud.stream.metrics.config; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration; import org.springframework.boot.actuate.endpoint.MetricsEndpoint; +import org.springframework.boot.actuate.metrics.export.Exporter; +import org.springframework.boot.actuate.metrics.export.MetricExporters; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -26,27 +37,68 @@ import org.springframework.cloud.stream.metrics.ApplicationMetricsExporter; import org.springframework.cloud.stream.metrics.ApplicationMetricsProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; /** + * Autoconfiguration registering an {@link Exporter} that publishes application metrics over + * the {@link Emitter#applicationMetrics()} channel. + * * @author Vinicius Carvalho + * @author Marius Bogoevici + * */ @Configuration @ConditionalOnClass(Binder.class) @EnableBinding(Emitter.class) @EnableConfigurationProperties(ApplicationMetricsProperties.class) +@AutoConfigureAfter(MetricExportAutoConfiguration.class) @ConditionalOnProperty("spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination") public class BinderMetricsAutoConfiguration { - @Bean - public ApplicationMetricsExporter aggregateMetricsExporter(MetricsEndpoint endpoint, - Emitter emitter, ApplicationMetricsProperties properties) { - return new ApplicationMetricsExporter(endpoint, emitter.applicationMetrics(), properties); - } + public static Log log = LogFactory.getLog(BinderMetricsAutoConfiguration.class); + + public static final String APPLICATION_METRICS_EXPORTER_TRIGGER_NAME = "application"; @Bean public MetricJsonSerializer metricJsonSerializer() { return new MetricJsonSerializer(); } + /** + * Postprocessor for installing the {@link ApplicationMetricsExporter} as an + * exporter under the name {@code application}. + * @param endpoint the metrics endpoint (lazy reference to prevent early initialization) + * @param emitter the emitter bound interface + * @param properties application metrics properties + * @return + */ + @Bean + public static BeanPostProcessor metricExportersBeanPostProcessor(final @Lazy MetricsEndpoint endpoint, + final Emitter emitter, final ApplicationMetricsProperties properties) { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { + if (bean instanceof MetricExporters) { + Map exporters = ((MetricExporters) bean).getExporters(); + if (!exporters.containsKey(APPLICATION_METRICS_EXPORTER_TRIGGER_NAME)) { + exporters.put(APPLICATION_METRICS_EXPORTER_TRIGGER_NAME, + new ApplicationMetricsExporter(endpoint, emitter.applicationMetrics(), properties)); + } + else { + log.warn("Could not register ApplicationMetricExporter: " + + exporters.get(APPLICATION_METRICS_EXPORTER_TRIGGER_NAME) + + " was already registered as " + APPLICATION_METRICS_EXPORTER_TRIGGER_NAME); + } + } + return bean; + } + }; + } + } diff --git a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsEnvironmentPostProcessor.java b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsEnvironmentPostProcessor.java index 7decc545b..72842747f 100644 --- a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsEnvironmentPostProcessor.java +++ b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/BinderMetricsEnvironmentPostProcessor.java @@ -33,7 +33,6 @@ public class BinderMetricsEnvironmentPostProcessor implements EnvironmentPostPro Map propertiesToAdd = new HashMap<>(); propertiesToAdd.put("spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".contentType", "application/json"); - propertiesToAdd.put("spring.metrics.export.includes", "integration**"); environment.getPropertySources() .addLast(new MapPropertySource("binderMetricsDefaultProperties", propertiesToAdd)); } diff --git a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/Emitter.java b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/Emitter.java index 6c1afcf8e..8c8e6ff95 100644 --- a/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/Emitter.java +++ b/spring-cloud-stream-metrics/src/main/java/org/springframework/cloud/stream/metrics/config/Emitter.java @@ -24,7 +24,7 @@ import org.springframework.messaging.MessageChannel; */ public interface Emitter { - String APPLICATION_METRICS = "applicationMetricsChannel"; + String APPLICATION_METRICS = "applicationMetrics"; @Output(APPLICATION_METRICS) MessageChannel applicationMetrics(); diff --git a/spring-cloud-stream-metrics/src/test/java/org/springframework/cloud/stream/metrics/ApplicationMetricsExporterTests.java b/spring-cloud-stream-metrics/src/test/java/org/springframework/cloud/stream/metrics/ApplicationMetricsExporterTests.java index 4dee3851f..b6560ccaf 100644 --- a/spring-cloud-stream-metrics/src/test/java/org/springframework/cloud/stream/metrics/ApplicationMetricsExporterTests.java +++ b/spring-cloud-stream-metrics/src/test/java/org/springframework/cloud/stream/metrics/ApplicationMetricsExporterTests.java @@ -73,20 +73,19 @@ public class ApplicationMetricsExporterTests { ConfigurableApplicationContext applicationContext = SpringApplication.run( BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo"); + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo"); Emitter emitterSource = applicationContext.getBean(Emitter.class); MessageCollector collector = applicationContext.getBean(MessageCollector.class); Message message = collector.forChannel(emitterSource.applicationMetrics()).poll(1000, TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper + ApplicationMetrics applicationMetrics = mapper .readValue((String) message.getPayload(), ApplicationMetrics.class); Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertFalse(contains("mem", applicationMetricsChannel.getMetrics())); - Assert.assertEquals("application", applicationMetricsChannel.getName()); - Assert.assertTrue(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); + applicationMetrics.getMetrics())); + Assert.assertEquals("application", applicationMetrics.getName()); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); applicationContext.close(); } @@ -95,21 +94,20 @@ public class ApplicationMetricsExporterTests { ConfigurableApplicationContext applicationContext = SpringApplication.run( BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", - "--spring.application.name=foo", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo"); + "--spring.application.name=foo", + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo"); Emitter emitterSource = applicationContext.getBean(Emitter.class); MessageCollector collector = applicationContext.getBean(MessageCollector.class); Message message = collector.forChannel(emitterSource.applicationMetrics()).poll(1000, TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper - .readValue((String) message.getPayload(), ApplicationMetrics.class); + ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(), ApplicationMetrics.class); Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertFalse(contains("mem", applicationMetricsChannel.getMetrics())); - Assert.assertEquals("foo", applicationMetricsChannel.getName()); - Assert.assertTrue(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); + applicationMetrics.getMetrics())); + Assert.assertTrue(contains("mem", applicationMetrics.getMetrics())); + Assert.assertEquals("foo", applicationMetrics.getName()); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); applicationContext.close(); } @@ -119,29 +117,29 @@ public class ApplicationMetricsExporterTests { BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", "--spring.cloud.stream.metrics.prefix=foo", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo"); + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo"); Emitter emitterSource = applicationContext.getBean(Emitter.class); MessageCollector collector = applicationContext.getBean(MessageCollector.class); Message message = collector.forChannel(emitterSource.applicationMetrics()).poll(1000, TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper + ApplicationMetrics applicationMetrics = mapper .readValue((String) message.getPayload(), ApplicationMetrics.class); Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertFalse(contains("mem", applicationMetricsChannel.getMetrics())); - Assert.assertEquals("foo.application", applicationMetricsChannel.getName()); - Assert.assertTrue(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); + applicationMetrics.getMetrics())); + Assert.assertTrue(contains("mem", applicationMetrics.getMetrics())); + Assert.assertEquals("foo.application", applicationMetrics.getName()); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); applicationContext.close(); } @Test - public void includesExcludes() throws Exception { + public void includesExcludesDefaultConfig() throws Exception { ConfigurableApplicationContext applicationContext = SpringApplication.run( BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo", + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo", "--spring.metrics.export.includes=mem**", "--spring.metrics.export.excludes=integration**"); Emitter emitterSource = applicationContext.getBean(Emitter.class); @@ -150,12 +148,35 @@ public class ApplicationMetricsExporterTests { TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper + ApplicationMetrics applicationMetrics = mapper .readValue((String) message.getPayload(), ApplicationMetrics.class); Assert.assertFalse(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertTrue(contains("mem", applicationMetricsChannel.getMetrics())); - Assert.assertTrue(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); + applicationMetrics.getMetrics())); + Assert.assertTrue(contains("mem", applicationMetrics.getMetrics())); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); + applicationContext.close(); + } + + @Test + public void includesExcludesWithApplicationMetricsConfiguration() throws Exception { + ConfigurableApplicationContext applicationContext = SpringApplication.run( + BinderExporterApplication.class, "--server.port=0", + "--spring.jmx.enabled=false", + "--spring.metrics.export.triggers.application.delay-millis=500", + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo", + "--spring.metrics.export.triggers.application.includes=mem**", + "--spring.metrics.export.triggers.application.excludes=integration**"); + Emitter emitterSource = applicationContext.getBean(Emitter.class); + MessageCollector collector = applicationContext.getBean(MessageCollector.class); + Message message = collector.forChannel(emitterSource.applicationMetrics()).poll(1000, + TimeUnit.MILLISECONDS); + Assert.assertNotNull(message); + ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); + ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(), ApplicationMetrics.class); + Assert.assertFalse(contains("integration.channel.errorChannel.errorRate.mean", + applicationMetrics.getMetrics())); + Assert.assertTrue(contains("mem", applicationMetrics.getMetrics())); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); applicationContext.close(); } @@ -164,7 +185,7 @@ public class ApplicationMetricsExporterTests { ConfigurableApplicationContext applicationContext = SpringApplication.run( BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo", + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo", "--spring.metrics.export.includes=integration**", "--spring.cloud.stream.metrics.properties=java**,spring.test.env**"); Emitter emitterSource = applicationContext.getBean(Emitter.class); @@ -173,13 +194,12 @@ public class ApplicationMetricsExporterTests { TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper - .readValue((String) message.getPayload(), ApplicationMetrics.class); - Assert.assertFalse(contains("mem", applicationMetricsChannel.getMetrics())); + ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(), ApplicationMetrics.class); + Assert.assertFalse(contains("mem", applicationMetrics.getMetrics())); Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertFalse(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); - Assert.assertTrue(applicationMetricsChannel.getProperties().get("spring.test.env.syntax") + applicationMetrics.getMetrics())); + Assert.assertFalse(CollectionUtils.isEmpty(applicationMetrics.getProperties())); + Assert.assertTrue(applicationMetrics.getProperties().get("spring.test.env.syntax") .equals("testing")); applicationContext.close(); } @@ -190,7 +210,7 @@ public class ApplicationMetricsExporterTests { BinderExporterApplication.class, "--server.port=0", "--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500", "--spring.application.name=foo", - "--spring.cloud.stream.bindings.applicationMetricsChannel.destination=foo", + "--spring.cloud.stream.bindings." + Emitter.APPLICATION_METRICS + ".destination=foo", "--spring.cloud.stream.metrics.key=foobarfoo"); Emitter emitterSource = applicationContext.getBean(Emitter.class); MessageCollector collector = applicationContext.getBean(MessageCollector.class); @@ -198,13 +218,12 @@ public class ApplicationMetricsExporterTests { TimeUnit.MILLISECONDS); Assert.assertNotNull(message); ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class); - ApplicationMetrics applicationMetricsChannel = mapper - .readValue((String) message.getPayload(), ApplicationMetrics.class); + ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(), ApplicationMetrics.class); Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", - applicationMetricsChannel.getMetrics())); - Assert.assertFalse(contains("mem", applicationMetricsChannel.getMetrics())); - Assert.assertEquals("foobarfoo", applicationMetricsChannel.getName()); - Assert.assertTrue(CollectionUtils.isEmpty(applicationMetricsChannel.getProperties())); + applicationMetrics.getMetrics())); + Assert.assertTrue(contains("mem", applicationMetrics.getMetrics())); + Assert.assertEquals("foobarfoo", applicationMetrics.getName()); + Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties())); applicationContext.close(); }