Metrics for springboot

  metrics, springboot

Class structure

clipboard.png

PublicMetricsAutoConfiguration

@Configuration
@AutoConfigureBefore(EndpointAutoConfiguration.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, CacheAutoConfiguration.class,
        MetricRepositoryAutoConfiguration.class, CacheStatisticsAutoConfiguration.class,
        IntegrationAutoConfiguration.class })
public class PublicMetricsAutoConfiguration {

    @Autowired(required = false)
    @ExportMetricReader
    private List<MetricReader> metricReaders = Collections.emptyList();

    @Bean
    public SystemPublicMetrics systemPublicMetrics() {
        return new SystemPublicMetrics();
    }

    @Bean
    public MetricReaderPublicMetrics metricReaderPublicMetrics() {
        return new MetricReaderPublicMetrics(new CompositeMetricReader(
                this.metricReaders.toArray(new MetricReader[0])));
    }

    @Bean
    @ConditionalOnBean(RichGaugeReader.class)
    public RichGaugeReaderPublicMetrics richGaugePublicMetrics(
            RichGaugeReader richGaugeReader) {
        return new RichGaugeReaderPublicMetrics(richGaugeReader);
    }

    @Configuration
    @ConditionalOnClass(DataSource.class)
    @ConditionalOnBean(DataSource.class)
    static class DataSourceMetricsConfiguration {

        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnBean(DataSourcePoolMetadataProvider.class)
        public DataSourcePublicMetrics dataSourcePublicMetrics() {
            return new DataSourcePublicMetrics();
        }

    }

    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class })
    @ConditionalOnWebApplication
    static class TomcatMetricsConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public TomcatPublicMetrics tomcatPublicMetrics() {
            return new TomcatPublicMetrics();
        }

    }

//......

}

InMemoryMetricRepository

By default, Spring-Boot-Starter-Actor will provide an InMemoryMetricRepository implementation if the user does not provide any customized MetricRepository. If we add Dropwizard’s Metrics class library to classpath as a dependency, then all metric items in Dropwizard Metrics’ Metri-cRegistry will also be exposed through Public-Metrics development.

/metrics method

@ConfigurationProperties(prefix = "endpoints.metrics")
public class MetricsEndpoint extends AbstractEndpoint<Map<String, Object>> {

    private final List<PublicMetrics> publicMetrics;

    /**
     * Create a new {@link MetricsEndpoint} instance.
     * @param publicMetrics the metrics to expose
     */
    public MetricsEndpoint(PublicMetrics publicMetrics) {
        this(Collections.singleton(publicMetrics));
    }

    /**
     * Create a new {@link MetricsEndpoint} instance.
     * @param publicMetrics the metrics to expose. The collection will be sorted using the
     * {@link AnnotationAwareOrderComparator}.
     */
    public MetricsEndpoint(Collection<PublicMetrics> publicMetrics) {
        super("metrics");
        Assert.notNull(publicMetrics, "PublicMetrics must not be null");
        this.publicMetrics = new ArrayList<PublicMetrics>(publicMetrics);
        AnnotationAwareOrderComparator.sort(this.publicMetrics);
    }

    public void registerPublicMetrics(PublicMetrics metrics) {
        this.publicMetrics.add(metrics);
        AnnotationAwareOrderComparator.sort(this.publicMetrics);
    }

    public void unregisterPublicMetrics(PublicMetrics metrics) {
        this.publicMetrics.remove(metrics);
    }

    @Override
    public Map<String, Object> invoke() {
        Map<String, Object> result = new LinkedHashMap<String, Object>();
        List<PublicMetrics> metrics = new ArrayList<PublicMetrics>(this.publicMetrics);
        for (PublicMetrics publicMetric : metrics) {
            try {
                for (Metric<?> metric : publicMetric.metrics()) {
                    result.put(metric.getName(), metric.getValue());
                }
            }
            catch (Exception ex) {
                // Could not evaluate metrics
            }
        }
        return result;
    }

}

Related configuration

{
      "name": "spring.metrics.export.aggregate.key-pattern",
      "type": "java.lang.String",
      "description": "Pattern that tells the aggregator what to do with the keys from the source\n repository. The keys in the source repository are assumed to be period\n separated, and the pattern is in the same format, e.g. \"d.d.k.d\". Here \"d\"\n means \"discard\" and \"k\" means \"keep\" the key segment in the corresponding\n position in the source.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Aggregate",
      "defaultValue": ""
    },
    {
      "name": "spring.metrics.export.aggregate.prefix",
      "type": "java.lang.String",
      "description": "Prefix for global repository if active. Should be unique for this JVM, but most\n useful if it also has the form \"a.b\" where \"a\" is unique to this logical\n process (this application) and \"b\" is unique to this physical process. If you\n set spring.application.name elsewhere, then the default will be in the right\n form.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Aggregate",
      "defaultValue": ""
    },
    {
      "name": "spring.metrics.export.delay-millis",
      "type": "java.lang.Long",
      "description": "Delay in milliseconds between export ticks. Metrics are exported to external\n sources on a schedule with this delay.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    },
    {
      "name": "spring.metrics.export.enabled",
      "type": "java.lang.Boolean",
      "description": "Flag to enable metric export (assuming a MetricWriter is available).",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    },
    {
      "name": "spring.metrics.export.excludes",
      "type": "java.lang.String[]",
      "description": "List of patterns for metric names to exclude. Applied after the includes.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    },
    {
      "name": "spring.metrics.export.includes",
      "type": "java.lang.String[]",
      "description": "List of patterns for metric names to include.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    },
    {
      "name": "spring.metrics.export.redis.key",
      "type": "java.lang.String",
      "description": "Key for redis repository export (if active). Should be globally unique for a\n system sharing a redis repository across multiple processes.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Redis",
      "defaultValue": "keys.spring.metrics"
    },
    {
      "name": "spring.metrics.export.redis.prefix",
      "type": "java.lang.String",
      "description": "Prefix for redis repository if active. Should be globally unique across all\n processes sharing the same repository.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Redis",
      "defaultValue": "spring.metrics"
    },
    {
      "name": "spring.metrics.export.send-latest",
      "type": "java.lang.Boolean",
      "description": "Flag to switch off any available optimizations based on not exporting unchanged\n metric values.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    },
    {
      "name": "spring.metrics.export.statsd.host",
      "type": "java.lang.String",
      "description": "Host of a statsd server to receive exported metrics.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Statsd"
    },
    {
      "name": "spring.metrics.export.statsd.port",
      "type": "java.lang.Integer",
      "description": "Port of a statsd server to receive exported metrics.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Statsd",
      "defaultValue": 8125
    },
    {
      "name": "spring.metrics.export.statsd.prefix",
      "type": "java.lang.String",
      "description": "Prefix for statsd exported metrics.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties$Statsd"
    },
    {
      "name": "spring.metrics.export.triggers",
      "type": "java.util.Map<java.lang.String,org.springframework.boot.actuate.metrics.export.SpecificTriggerProperties>",
      "description": "Specific trigger properties per MetricWriter bean name.",
      "sourceType": "org.springframework.boot.actuate.metrics.export.MetricExportProperties"
    }

The specific path is org/springframework/boot/spring-boot-actor/1.3.5.release/spring-boot-actor-1.3.5.release.jar! /META-INF/spring-configuration-metadata.json

Integrated statsd instance

    @Bean
    @ExportMetricWriter
    public StatsdMetricWriter statsdMetricWriter(
            @Value("${spring.metrics.export.statsd.host}") String host,
            @Value("${spring.metrics.export.statsd.port}") int port,
            @Value("${spring.metrics.export.statsd.prefix}") String prefix
    ) {
        return new StatsdMetricWriter(prefix, host, port);
    }

In fact, the above springboot-acutator is already built in, and the configuration properties can be activated.

spring.metrics.export.statsd.host=localhost
spring.metrics.export.statsd.port=8125
spring.metrics.export.statsd.prefix=metric-demo

While increasing maven dependence

<!-- https://mvnrepository.com/artifact/com.timgroup/java-statsd-client -->
        <dependency>
            <groupId>com.timgroup</groupId>
            <artifactId>java-statsd-client</artifactId>
            <version>3.1.0</version>
        </dependency>

Configure the class MetricExportProperties

@ConfigurationProperties("spring.metrics.export")
public class MetricExportProperties extends TriggerProperties {

    /**
     * Specific trigger properties per MetricWriter bean name.
     */
    private Map<String, SpecificTriggerProperties> triggers = new LinkedHashMap<String, SpecificTriggerProperties>();

    private Aggregate aggregate = new Aggregate();

    private Redis redis = new Redis();

    private Statsd statsd = new Statsd();

    @PostConstruct
    public void setUpDefaults() {
        TriggerProperties defaults = this;
        for (Entry<String, SpecificTriggerProperties> entry : this.triggers.entrySet()) {
            String key = entry.getKey();
            SpecificTriggerProperties value = entry.getValue();
            if (value.getNames() == null || value.getNames().length == 0) {
                value.setNames(new String[] { key });
            }
        }
        if (defaults.isSendLatest() == null) {
            defaults.setSendLatest(true);
        }
        if (defaults.getDelayMillis() == null) {
            defaults.setDelayMillis(5000);
        }
        for (TriggerProperties value : this.triggers.values()) {
            if (value.isSendLatest() == null) {
                value.setSendLatest(defaults.isSendLatest());
            }
            if (value.getDelayMillis() == null) {
                value.setDelayMillis(defaults.getDelayMillis());
            }
        }
    }

    /**
     * Configuration for triggers on individual named writers. Each value can individually
     * specify a name pattern explicitly, or else the map key will be used if the name is
     * not set.
     * @return the writers
     */
    public Map<String, SpecificTriggerProperties> getTriggers() {
        return this.triggers;
    }

    public Aggregate getAggregate() {
        return this.aggregate;
    }

    public void setAggregate(Aggregate aggregate) {
        this.aggregate = aggregate;
    }

    public Redis getRedis() {
        return this.redis;
    }

    public void setRedis(Redis redis) {
        this.redis = redis;
    }

    public Statsd getStatsd() {
        return this.statsd;
    }

    public void setStatsd(Statsd statsd) {
        this.statsd = statsd;
    }

    /**
     * Find a matching trigger configuration.
     * @param name the bean name to match
     * @return a matching configuration if there is one
     */
    public TriggerProperties findTrigger(String name) {
        for (SpecificTriggerProperties value : this.triggers.values()) {
            if (PatternMatchUtils.simpleMatch(value.getNames(), name)) {
                return value;
            }
        }
        return this;
    }

    /**
     * Aggregate properties.
     */
    public static class Aggregate {

        /**
         * Prefix for global repository if active. Should be unique for this JVM, but most
         * useful if it also has the form "a.b" where "a" is unique to this logical
         * process (this application) and "b" is unique to this physical process. If you
         * set spring.application.name elsewhere, then the default will be in the right
         * form.
         */
        private String prefix = "";

        /**
         * Pattern that tells the aggregator what to do with the keys from the source
         * repository. The keys in the source repository are assumed to be period
         * separated, and the pattern is in the same format, e.g. "d.d.k.d". Here "d"
         * means "discard" and "k" means "keep" the key segment in the corresponding
         * position in the source.
         */
        private String keyPattern = "";

        public String getPrefix() {
            return this.prefix;
        }

        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

        public String getKeyPattern() {
            return this.keyPattern;
        }

        public void setKeyPattern(String keyPattern) {
            this.keyPattern = keyPattern;
        }

    }

    /**
     * Redis properties.
     */
    public static class Redis {

        /**
         * Prefix for redis repository if active. Should be globally unique across all
         * processes sharing the same repository.
         */
        private String prefix = "spring.metrics";

        /**
         * Key for redis repository export (if active). Should be globally unique for a
         * system sharing a redis repository across multiple processes.
         */
        private String key = "keys.spring.metrics";

        public String getPrefix() {
            return this.prefix;
        }

        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

        public String getKey() {
            return this.key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        public String getAggregatePrefix() {
            // The common case including a standalone aggregator would have a prefix that
            // starts with the end of the key, so strip that bit off and call it the
            // aggregate prefix.
            if (this.key.startsWith("keys.")) {
                String candidate = this.key.substring("keys.".length());
                if (this.prefix.startsWith(candidate)) {
                    return candidate;
                }
                return candidate;
            }
            // If the user went off piste, choose something that is safe (not empty) but
            // not the whole prefix (on the assumption that it contains dimension keys)
            if (this.prefix.contains(".")
                    && this.prefix.indexOf(".") < this.prefix.length() - 1) {
                return this.prefix.substring(this.prefix.indexOf(".") + 1);
            }
            return this.prefix;
        }

    }

    /**
     * Statsd properties.
     */
    public static class Statsd {

        /**
         * Host of a statsd server to receive exported metrics.
         */
        private String host;

        /**
         * Port of a statsd server to receive exported metrics.
         */
        private int port = 8125;

        /**
         * Prefix for statsd exported metrics.
         */
        private String prefix;

        public String getHost() {
            return this.host;
        }

        public void setHost(String host) {
            this.host = host;
        }

        public int getPort() {
            return this.port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public String getPrefix() {
            return this.prefix;
        }

        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }

    }

}

MetricExportAutoConfiguration

@Configuration
@EnableScheduling
@ConditionalOnProperty(value = "spring.metrics.export.enabled", matchIfMissing = true)
@EnableConfigurationProperties
public class MetricExportAutoConfiguration {

    @Autowired
    private MetricExportProperties properties;

    @Autowired(required = false)
    private MetricsEndpointMetricReader endpointReader;

    @Autowired(required = false)
    @ExportMetricReader
    private List<MetricReader> readers;

    @Autowired(required = false)
    @ExportMetricWriter
    private Map<String, GaugeWriter> writers = Collections.emptyMap();

    @Autowired(required = false)
    private Map<String, Exporter> exporters = Collections.emptyMap();

    @Bean
    @ConditionalOnMissingBean(name = "metricWritersMetricExporter")
    public SchedulingConfigurer metricWritersMetricExporter() {
        Map<String, GaugeWriter> writers = new HashMap<String, GaugeWriter>();
        MetricReader reader = this.endpointReader;
        if (reader == null && !CollectionUtils.isEmpty(this.readers)) {
            reader = new CompositeMetricReader(
                    this.readers.toArray(new MetricReader[this.readers.size()]));
        }
        if (reader == null && this.exporters.isEmpty()) {
            return new NoOpSchedulingConfigurer();
        }
        MetricExporters exporters = new MetricExporters(this.properties);
        if (reader != null) {
            writers.putAll(this.writers);
            exporters.setReader(reader);
            exporters.setWriters(writers);
        }
        exporters.setExporters(this.exporters);
        return exporters;
    }

    @Bean
    @ExportMetricWriter
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "spring.metrics.export.statsd", name = "host")
    public StatsdMetricWriter statsdMetricWriter() {
        MetricExportProperties.Statsd statsdProperties = this.properties.getStatsd();
        return new StatsdMetricWriter(statsdProperties.getPrefix(),
                statsdProperties.getHost(), statsdProperties.getPort());
    }

    @Configuration
    protected static class MetricExportPropertiesConfiguration {

        @Value("${spring.application.name:application}.${random.value:0000}")
        private String prefix = "";

        private String aggregateKeyPattern = "k.d";

        @Bean(name = "spring.metrics.export.CONFIGURATION_PROPERTIES")
        @ConditionalOnMissingBean
        public MetricExportProperties metricExportProperties() {
            MetricExportProperties export = new MetricExportProperties();
            export.getRedis().setPrefix("spring.metrics"
                    + (this.prefix.length() > 0 ? "." : "") + this.prefix);
            export.getAggregate().setPrefix(this.prefix);
            export.getAggregate().setKeyPattern(this.aggregateKeyPattern);
            return export;
        }

    }

    private static class NoOpSchedulingConfigurer implements SchedulingConfigurer {

        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        }

    }

}

MetricExporters timed export data

public class MetricExporters implements SchedulingConfigurer, Closeable {

    private MetricReader reader;

    private Map<String, GaugeWriter> writers = new HashMap<String, GaugeWriter>();

    private final MetricExportProperties properties;

    private final Map<String, Exporter> exporters = new HashMap<String, Exporter>();

    private final Set<String> closeables = new HashSet<String>();

    public MetricExporters(MetricExportProperties properties) {
        this.properties = properties;
    }

    public void setReader(MetricReader reader) {
        this.reader = reader;
    }

    public void setWriters(Map<String, GaugeWriter> writers) {
        this.writers.putAll(writers);
    }

    public void setExporters(Map<String, Exporter> exporters) {
        this.exporters.putAll(exporters);
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        for (Entry<String, Exporter> entry : this.exporters.entrySet()) {
            String name = entry.getKey();
            Exporter exporter = entry.getValue();
            TriggerProperties trigger = this.properties.findTrigger(name);
            if (trigger != null) {
                ExportRunner runner = new ExportRunner(exporter);
                IntervalTask task = new IntervalTask(runner, trigger.getDelayMillis(),
                        trigger.getDelayMillis());
                taskRegistrar.addFixedDelayTask(task);
            }
        }
        for (Entry<String, GaugeWriter> entry : this.writers.entrySet()) {
            String name = entry.getKey();
            GaugeWriter writer = entry.getValue();
            TriggerProperties trigger = this.properties.findTrigger(name);
            if (trigger != null) {
                MetricCopyExporter exporter = getExporter(writer, trigger);
                this.exporters.put(name, exporter);
                this.closeables.add(name);
                ExportRunner runner = new ExportRunner(exporter);
                IntervalTask task = new IntervalTask(runner, trigger.getDelayMillis(),
                        trigger.getDelayMillis());
                taskRegistrar.addFixedDelayTask(task);
            }
        }
    }

    private MetricCopyExporter getExporter(GaugeWriter writer,
            TriggerProperties trigger) {
        MetricCopyExporter exporter = new MetricCopyExporter(this.reader, writer);
        exporter.setIncludes(trigger.getIncludes());
        exporter.setExcludes(trigger.getExcludes());
        exporter.setSendLatest(trigger.isSendLatest());
        return exporter;
    }

    public Map<String, Exporter> getExporters() {
        return this.exporters;
    }

    @Override
    public void close() throws IOException {
        for (String name : this.closeables) {
            Exporter exporter = this.exporters.get(name);
            if (exporter instanceof Closeable) {
                ((Closeable) exporter).close();
            }
        }
    }

    private static class ExportRunner implements Runnable {

        private final Exporter exporter;

        ExportRunner(Exporter exporter) {
            this.exporter = exporter;
        }

        @Override
        public void run() {
            this.exporter.export();
        }

    }

}

Configuration parameter instance

spring.metrics.export.enabled=true
spring.metrics.export.send-latest=true
spring.metrics.export.delay-millis=10000
spring.metrics.export.statsd.host=localhost
spring.metrics.export.statsd.port=8125
spring.metrics.export.statsd.prefix=metric-demo

docs