1. Introduction

The Dropwizard Metrics plugin provides support for monitoring health checks and metrics monitoring for Grails 3 applications.

2. Installation

To add the Dropwizard Metrics plugin to an application add the following dependency to the dependencies block of your build.gradle:

compile "org.grails.plugins:dropwizard-metrics:1.0.0.BUILD-SNAPSHOT"

3. Metrics

3.1. PublicMetric Beans

The plugin will discover all of the org.springframework.boot.actuate.endpoint.PublicMetrics instances that are found in the Spring application context and active them. An application may provide any number of PublicMetrics beans.

3.2. Marking Meters

A meter measures the rate of events over time (e.g., “requests per second”). In addition to the mean rate, meters also track 1-, 5-, and 15-minute moving averages.
— The Dropwizard Metrics Documentation

The Meterable trait provides a markMeter(String) method which makes it easy to access and increment a Meter. Any class may implement the trait and take advantage of this method.

class SomeClass implements Meterable {

    def someAction() {
        markMeter 'some meter'

        // ...
    }
}

The plugin provides the Metered annotation which may be applied to any method. If a method is marked with @Metered, every time the method is invoked, the meter with the specified name will automatically be incremented.

class SomeOtherClass {

    @Metered('some meter')
    void someAction() {
        // ...
    }
    @Metered(value='some other meter', useClassPrefix = true)
    void someOtherAction() {
        // ...
    }
    @Metered(value='yet another meter', useClassPrefix = false)
    void yetAnotherAction() {
        // ...
    }
}

The @Metered annotation supports an optional useClassPrefix attribute which when set to true will cause the name of the associated meter to be prefixed with the fully qualified name of the class which defines the annotated method.

3.3. Timers

A timer is basically a histogram of the duration of a type of event and a meter of the rate of its occurrence.
— The Dropwizard Metrics Documentation

The plugin provides the Timed annotation which may be applied to any method. If a method is marked with @Timed, every time the method is invoked, the timer with the specified name will automatically be started.

class SomeTimedClass {

    @Timed('some timer')
    void someAction() {
        // ...
    }

    @Timed(value='some other timer', useClassPrefix = true)
    void someOtherAction() {
        // ...
    }

    @Timed(value='yet other timer', useClassPrefix = false)
    void yetAnotherAction() {
        // ...
    }
}

The @Timed annotation supports an optional useClassPrefix attribute which when set to true will cause the name of the associated timer to be prefixed with the fully qualified name of the class which defines the annotated method.

3.4. Unit Testing

When unit testing code which uses any of the plugin’s code generation techniques (@Timed, @Metered, etc…​) a bean named metricsRegistry will need to be registed in the Spring application context. Below are examples of how to do that.

import com.codahale.metrics.MetricRegistry
import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification

@TestMixin(GrailsUnitTestMixin)
class MeterableSpec extends Specification {

    static doWithSpring = {
        metricRegistry MetricRegistry
        someBean SomeClass
    }

    void 'test markMeter method'() {
        setup:
        def registry = applicationContext.metricRegistry
        def obj = applicationContext.someBean

        when:
        obj.someAction()
        obj.someAction()
        obj.someAction()

        then:
        registry.meter('some meter').count == 3
    }
}

class SomeClass implements Meterable {

    def someAction() {
        markMeter 'some meter'

        // ...
    }
}
import com.codahale.metrics.MetricRegistry
import grails.test.mixin.TestMixin
import grails.test.mixin.support.GrailsUnitTestMixin
import spock.lang.Specification

@TestMixin(GrailsUnitTestMixin)
class MeteredAnnotationSpec extends Specification {

    static doWithSpring = {
        metricRegistry MetricRegistry
        someMeteredBean SomeOtherClass
    }

    void 'test the @Metered annotation'() {
        setup:
        def registry = applicationContext.metricRegistry
        def obj = applicationContext.someMeteredBean

        when:
        obj.someAction()
        obj.someAction()
        obj.someAction()

        then:
        registry.meter('some meter').count == 3
    }

    void 'test the @Metered annotation with useClassPrefix set to true'() {
        setup:
        def registry = applicationContext.metricRegistry
        def obj = applicationContext.someMeteredBean

        when:
        obj.someOtherAction()
        obj.someOtherAction()
        obj.someOtherAction()

        then:
        registry.meter('grails.plugin.dropwizard.metrics.meters.SomeOtherClass.some other meter').count == 3
    }

    void 'test the @Metered annotation with useClassPrefix set to false'() {
        setup:
        def registry = applicationContext.metricRegistry
        def obj = applicationContext.someMeteredBean

        when:
        obj.yetAnotherAction()
        obj.yetAnotherAction()
        obj.yetAnotherAction()

        then:
        registry.meter('yet another meter').count == 3
    }
}

class SomeOtherClass {

    @Metered('some meter')
    void someAction() {
        // ...
    }
    @Metered(value='some other meter', useClassPrefix = true)
    void someOtherAction() {
        // ...
    }
    @Metered(value='yet another meter', useClassPrefix = false)
    void yetAnotherAction() {
        // ...
    }
}

3.5. Exposing Metrics Data

The plugin supports exposing access to metrics data through a URI in the web application. By default metrics are available at /metrics. A request to /metrics will be responded to with JSON provided by all of the MetricRegistry instances that are active in the application as shown below.

{"version":"3.0.0","gauges":{"grails.dropwizard.dropwizardGarbageCollectorMetricSet.PS-MarkSweep.count":{"value":0},"grails.dropwizard.dropwizardGarbageCollectorMetricSet.PS-MarkSweep.time":{"value":0},"grails.dropwizard.dropwizardGarbageCollectorMetricSet.PS-Scavenge.count":{"value":9},"grails.dropwizard.dropwizardGarbageCollectorMetricSet.PS-Scavenge.time":{"value":468},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.heap.committed":{"value":738197504},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.heap.init":{"value":268435456},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.heap.max":{"value":3817865216},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.heap.usage":{"value":0.14876779767387158},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.heap.used":{"value":567975400},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.non-heap.committed":{"value":64880640},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.non-heap.init":{"value":24576000},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.non-heap.max":{"value":136314880},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.non-heap.usage":{"value":0.47363680326021634},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.non-heap.used":{"value":64563744},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.Code-Cache.committed":{"value":6160384},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.Code-Cache.init":{"value":2555904},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.Code-Cache.max":{"value":50331648},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.Code-Cache.usage":{"value":0.11999003092447917},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.Code-Cache.used":{"value":6039296},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Eden-Space.committed":{"value":525336576},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Eden-Space.init":{"value":67633152},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Eden-Space.max":{"value":1354760192},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Eden-Space.usage":{"value":0.3691370819375242},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Eden-Space.used":{"value":500092224},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Old-Gen.committed":{"value":178782208},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Old-Gen.init":{"value":178782208},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Old-Gen.max":{"value":2863136768},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Old-Gen.usage":{"value":0.011815351742218974},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Old-Gen.used":{"value":33828968},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Perm-Gen.committed":{"value":58720256},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Perm-Gen.init":{"value":22020096},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Perm-Gen.max":{"value":85983232},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Perm-Gen.usage":{"value":0.6806495480421113},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Perm-Gen.used":{"value":58524448},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Survivor-Space.committed":{"value":34078720},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Survivor-Space.init":{"value":11010048},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Survivor-Space.max":{"value":34078720},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Survivor-Space.usage":{"value":0.9992807241586539},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.pools.PS-Survivor-Space.used":{"value":34054208},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.total.committed":{"value":803078144},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.total.init":{"value":293011456},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.total.max":{"value":3954180096},"grails.dropwizard.dropwizardMemoryUsageGaugeSet.total.used":{"value":632539312},"grails.dropwizard.dropwizardThreadStatesGaugeSet.blocked.count":{"value":0},"grails.dropwizard.dropwizardThreadStatesGaugeSet.count":{"value":23},"grails.dropwizard.dropwizardThreadStatesGaugeSet.daemon.count":{"value":20},"grails.dropwizard.dropwizardThreadStatesGaugeSet.deadlock.count":{"value":0},"grails.dropwizard.dropwizardThreadStatesGaugeSet.deadlocks":{"value":[]},"grails.dropwizard.dropwizardThreadStatesGaugeSet.new.count":{"value":0},"grails.dropwizard.dropwizardThreadStatesGaugeSet.runnable.count":{"value":9},"grails.dropwizard.dropwizardThreadStatesGaugeSet.terminated.count":{"value":0},"grails.dropwizard.dropwizardThreadStatesGaugeSet.timed_waiting.count":{"value":9},"grails.dropwizard.dropwizardThreadStatesGaugeSet.waiting.count":{"value":5}},"counters":{},"histograms":{},"meters":{},"timers":{}}
TBD - notes on configuring the endpoint.

Metrics may also be exposed as JMX MBeans. To enable JMX support assign the value true to the grails.dropwizard.jmx.enabled config setting.

---
grails:
    dropwizard:
        jmx:
            enabled: true

4. Health Checks

4.1. HealthCheck Beans

The plugin will discover all of the org.springframework.boot.actuate.health.HealthIndicator instances that are found in the Spring application context and active them. An application may provide any number of HealthCheck beans.

4.2. Exposing Health Check Data

The plugin supports exposing access to health check data through a URI in the web application. To enable exposing the data assign a value to the grails.dropwizard.health.uri property to indicate where the data should be made available. A request to /health will be responded to with JSON provided by all of the HealthCheck instances that are active in the application as shown below.

{"dropwizardThreadDeadlockHealthCheck":{"healthy":true}}
TBD - notes on configuring the endpoint.

5. Metrics Reporting

The plugin provides support for automatically reporting metrics at a scheduled interval using one or more built-in Dropwizard ScheduledReporters: Slf4jReporter, ConsoleReporter, or CsvReporter.

A separate plug-in, dropwizard-metrics-graphite is available for doing scheduled reporting using the GraphiteReporter. It can also serve as an example of one approach for adding your own reporter.

The reporters are enabled and configured under the confuration key grails.dropwizard.metrics.

To enable scheduled reporting of metrics, first assign a value to the config property named reporterFrequency to indicate how often the reporting should occur. The value represents the reporting interval in seconds.

grails:
    dropwizard:
        metrics:
            reporterFrequency: 30 # report every 30 seconds

If reporterFrequency is missing or has a value of zero, metric reporting will not occur.

For reference as we discuss the various reporters, a SimpleService was created with a @Metered method:

package demo.dropwizard

import grails.gorm.transactions.Transactional
import grails.plugin.dropwizard.metrics.meters.Metered

@Transactional(readOnly = true)
class SimpleService {

    @Metered('sayHello meter')
    String sayHello(String name) {
        name ? "Hello ${name}!" : 'Hello Stranger!'
    }
}

5.1. Slf4jReporter

The Slf4jReporter reports metrics via a Slf4j logger named com.codahale.metrics.Slf4jReporter at the INFO level. You may want to configure the level for that logger in grails-app/conf/logback.groovy as shown below.

logger 'com.codahale.metrics.Slf4jReporter', INFO, ['STDOUT'], false

For backward compatibility, this reporter is active by default. However, adding slf4j-reporter.active configuration will explicitly enable or disable the Slf4j reporter:

grails:
    dropwizard:
        metrics:
            reporterFrequency: 10 # report every 10 seconds
            slf4j-reporter.active: false

When active, the Slf4jReporter metric reporting to the configured logger looks like this:

2017-10-07 11:06:30.820  INFO --- [rter-1-thread-1] com.codahale.metrics.Slf4jReporter : type=GAUGE, name=gauge.response.metrics, value=2.0
2017-10-07 11:06:30.820  INFO --- [rter-1-thread-1] com.codahale.metrics.Slf4jReporter : type=GAUGE, name=gauge.response.sayHello.Fred, value=3.0
2017-10-07 11:06:30.820  INFO --- [rter-1-thread-1] com.codahale.metrics.Slf4jReporter : type=COUNTER, name=counter.status.200.metrics, count=2
2017-10-07 11:06:30.820  INFO --- [rter-1-thread-1] com.codahale.metrics.Slf4jReporter : type=COUNTER, name=counter.status.200.sayHello.Fred, count=10
2017-10-07 11:06:30.820  INFO --- [rter-1-thread-1] com.codahale.metrics.Slf4jReporter : type=METER, name=sayHello meter, count=10, mean_rate=0.010176484138111742, m1=1.7528496438887441E-7, m5=0.077548415663444, m15=0.6769308502134816, rate_unit=events/second

5.2. ConsoleReporter

The ConsoleReporter reports metrics directly to the console, making it a convenient reporter for use in development and environments where console output is required instead of logging.

To configure it, simply declare grails.dropwizard.metrics.console-reporter.active: true in configuration. Below we see that the slf4j-reporter is configured as inactive and the console-reporter is active.

grails:
    dropwizard:
        metrics:
            reporterFrequency: 10 # report every 10 seconds
            slf4j-reporter.active: false
            console-reporter.active: true

However, both could be enabled as active. Recall, the slf4j-reporter is active by default for backward compatibility, so you must explicitly configure it as inactive if that’s what you want.

Here’s what the console logging output looks like:

10/7/17 11:14:29 AM ============================================================

-- Gauges ----------------------------------------------------------------------
gauge.response.sayHello.Fred
             value = 3.0

-- Counters --------------------------------------------------------------------
counter.status.200.sayHello.Fred
             count = 14

-- Meters ----------------------------------------------------------------------
sayHello meter
             count = 14
         mean rate = 3.53 events/second
     1-minute rate = 0.00 events/second
     5-minute rate = 0.00 events/second
    15-minute rate = 0.00 events/second

5.3. CsvReporter

The CsvReporter is used to report metrics to a CSV (comma-separated values) file.

To configure it, declare grails.dropwizard.metrics.csv-reporter.active: true in configuration and specify the output directory of the CSV file. Of course, the directory specified must have permissions set to allow the application to write to it. If not specified, the csv-reporter.output-dir will default to the directory from which the app is running; i.e. ./

Below we see that csv-reporter is configured as active. Since we didn’t explicitly declare the slf4j-reporter.active: false, the metrics would also be logged as well as written to the CSV file.

grails:
    dropwizard:
        metrics:
            reporterFrequency: 10 # report every 10 seconds
            csv-reporter:
                active: true
                output-dir: /tmp

Multiple files are output from the CsvReporter, one for each meter, guage and counter that had data reported. For instance, when the sayHello method was invoked with the name Fred, a sayHello meter.csv file was written into the /tmp directory with the following contents:

t,count,mean_rate,m1_rate,m5_rate,m15_rate,rate_unit
1507412611,7,4.546009,0.000000,0.000000,0.000000,events/second
1507412616,7,1.070702,1.400000,1.400000,1.400000,events/second
1507412621,7,0.606700,1.288062,1.376860,1.392244,events/second
1507412626,7,0.423267,1.185074,1.354103,1.384531,events/second
1507412631,7,0.325009,1.090321,1.331721,1.376860,events/second
1507412636,7,0.263756,1.003144,1.309710,1.369232,events/second
1507412641,7,0.221956,0.922937,1.288062,1.361646,events/second

Likewise, a /tmp/gauge.response.sayHello.Fred.csv file was written:

t,value
1507412611,4.0
1507412616,4.0
1507412621,4.0
1507412626,4.0
1507412631,4.0
1507412636,4.0
1507412641,4.0

as well as a /tmp/counter.status.200.sayHello.Fred.csv file:

t,count
1507412611,7
1507412616,7
1507412621,7
1507412626,7
1507412631,7
1507412636,7
1507412641,7

5.4. For more information

For details on Dropwizard metrics and these reporters see http://metrics.dropwizard.io/3.1.0/manual/core/

6. API Documentation

API documentation may be found here.