1. Introduction
The Dropwizard Metrics plugin provides support for monitoring health checks and metrics monitoring for Grails 4 applications.
Dropwizard dependencies used in this plugin:
io.dropwizard.metrics:metrics-core:4.1.5
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:2.0.0.RC1"
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 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 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 org.grails.testing.GrailsUnitTest
import spock.lang.Specification
class MeterableSpec extends Specification implements GrailsUnitTest {
Closure 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 org.grails.testing.GrailsUnitTest
import spock.lang.Specification
class MeteredAnnotationSpec extends Specification implements GrailsUnitTest {
Closure 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.