This plugin borrowed ideas from Profile Plugin. Thanks to its developers.
Metrics Logger Plugin logs metrics information related to web requests served by Grails application in a thread-safe way. By configuring the logger appropriately metrics information can be routed to a dedicated log file, which can be indexed by tools like Splunk. The plugin can also be used to log interesting metrics outside the context of a request - for instance during application bootstrap or when batch jobs are executed. Here is a sample Metrics Log Entry:
Time=2014-02-17T15:31:00.840Z
Application=TestMetricsLoggerAppConfig
Timers:methodWithPlainAnnotation=30,CustomizedTimer=0,methodDefinedWith_def=0,StartStopTimer=2,ControllerDuration=89,ViewDuration=51,RequestDuration=175
Fields:CardUsed=Debit
Numerics:NumberOfItems=25.0
Request:Action=index,Controller=configValidation,Uri=/TestMetricsLoggerAppConfig/configValidation,Host=localhost,ServerName=localhost,RemoteAddress=127.0.0.1,X-Forwarded-For=null,X-Teros-Client-IP=null
Response:StatusCode=404
---EOE-----------------------------------------------------
Time
In case of requests this is when the request is intercepted. In case of manually created Log Entries this is when Log Entry is instantiated.Application
Grails application name. This value is extracted from app.name defined in application.properties file.Timers
List of Key-Value pairs. Keys indicate method names or custom names given to block of code that is instrumented. Values represent their duration in milliseconds.Fields
List of Key-Value pairs. Keys and values are both Strings.Numerics
List of Key-Value pairs. Keys are Strings and values are Doubles.Request
Request parameters extracted from the web request.Response
Response parameters extracted from the web request response.---EOE---
Log Entry separator
- In BuildConfig.groovy declare a compilation dependency on metrics-logger in plugins block as below:
plugins {
...
...
compile ":metrics-logger:1.1"
}
- Ensure the repositories block contains
grailsPlugins()
:
repositories {
grailsPlugins()
...
...
}
- Optionally declare a compilation dependency on apache-log4j-extras in dependencies block if your application uses log4j:
dependencies {
// specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes.
...
compile 'log4j:apache-log4j-extras:1.2.17'
}
The instructions below are applicable if the Grails App uses log4j. If it uses another logging library, set up similar configuration using that library specific classes and recommendation.
- Import the following classes in Config.groovy
import org.apache.log4j.rolling.RollingFileAppender
import org.apache.log4j.rolling.TimeBasedRollingPolicy
- Create a new Appender for MetricsLogger used by the plugin and associate the logger and appender
log4j = {
appenders {
...
appender name: 'metricLogAppender',
new RollingFileAppender(
layout:pattern(conversionPattern: '%m%n'),
rollingPolicy:new TimeBasedRollingPolicy(
activeFileName:"metric.log",
fileNamePattern:"metric.log.%d{yyyy-MM-dd-HH}.gz"
)
)
}
...
info metricLogAppender: 'MetricsLogger', additivity: false
}
Compile and run the app. It will generate an hourly rotating log file named metric.log containing basic request metrics like request timestamp, controller/view/request duration, host name, request and response parameters.
The behaviour of Metrics Logger Plugin can be controlled from the application by means of the following configuration values. These config values in Config.groovy can be used to decide the behaviour at environment level or across all environments.
grails.plugin.metricslogger.disable = true
By default the plugin is enabled to intercept requests and log metrics. Setting grails.plugin.metricslogger.disable
flag to true
ensures requests are not intercepted.
However if the application logs metrics for non requests by directly instantiating LogEntry
objects as explained later in this document, they will continue to work despite
the status of this flag.
The plugin extracts certain values from HttpServletRequest. Using the following configuration the app can specify a list of request headers that needs to be extracted for metrics logging.
grails.plugin.metricslogger.requestHeadersForLogging = ['X-Forwarded-For', 'X-Requested-With']
The above config will produce Metrics Log Entries as below:
Time=2014-02-17T15:31:00.840Z
Application=TestMetricsLoggerAppConfig
Timers:...
Fields:...
Numerics:...
Request:Action=index,Controller=configValidation,Uri=/TestMetricsLoggerAppConfig/configValidation,Host=localhost,ServerName=localhost,RemoteAddress=127.0.0.1,X-Forwarded-For=89.11.43.37,X-Requested-With=XMLHttpRequest
Response:...
---EOE-----------------------------------------------------
The plugin extracts status code of response from HttpServletResponse and surfaces that vital information as below:
Time=2014-02-17T15:31:00.840Z
Application=TestMetricsLoggerAppConfig
Timers:...
Fields:...
Numerics:...
Request:...
Response:StatusCode=404
---EOE-----------------------------------------------------
For that to work, the servlet container where the application is running should support servlet spec 3.0 or above. If that is not the case and you can't upgrade the servlet container use the following flag to disable the plugin from extracting status code. Otherwise it results in method not found exception at runtime. For efficiency reasons the plugin doesn't do try/catch in that situation.
grails.plugin.metricslogger.supportedServletSpecBelow3 = true
Methods defined in grails service beans can be timed using @LogMetrics
annotation. Blocks of code can be instrumented using MetricsLogger.startTimer("TimerName")
and a matching MetricsLogger.stopTimer("TimerName")
calls. Here is an example:
import com.mastercard.labs.metricslogger.annotation.LogMetrics
import com.mastercard.labs.metricslogger.MetricsLogger
import org.apache.log4j.Logger
class AnnotationService {
Logger logger = Logger.getLogger(AnnotationService.class);
@LogMetrics
void methodWithPlainAnnotation() {
logger.info("Method that has @LogMetrics annotation is running. There will be a timer with the name 'methodWithPlainAnnotation' in metrics log")
}
@LogMetrics(value="CustomizedTimer")
void methodWithValueAnnotation() {
logger.info("Method that has @LogMetrics annotation with a value explicitly set is running. There will be a timer named 'CustomizedTimer' in metrics log")
}
void methodWithoutAnnotation() {
logger.info("Method that is not annotated is running. There should not be any timer named 'methodWithoutAnnotation' in metrics log")
}
@LogMetrics
def methodDefinedWith_def() {
logger.info("Method defined in groovy style with 'def' is running. There should be a timer named 'methodDefinedWith_def' in metrics log")
}
void instrumentBlockOfCode() {
MetricsLogger.startTimer("BlockDuration")
try {
...
...
} finally {
MetricsLogger.stopTimer("BlockDuration")
}
}
}
This produces a Metrics Log Entry with timers as shown below. ControllerDuration, ViewDuration and RequestDuration are calculated by the plugin automatically. The other timer values come from the above code. The timer values are in milliseconds.
Time=2014-02-17T15:27:54.872Z
Application=TestMetricsLogger
Timers:methodWithPlainAnnotation=22,CustomizedTimer=1,methodDefinedWith_def=0,BlockDuration=2,ControllerDuration=88,ViewDuration=50,RequestDuration=172
Fields:...
Numerics:...
Request:...
Response:...
---EOE-----------------------------------------------------
Caveat:
For @LogMetrics annotation to work the annotated methods should be invoked from a service bean handle. Example:
annotationService.methodWithPlainAnnotation()
class AnnotationService {
Logger logger = Logger.getLogger(AnnotationService.class);
@LogMetrics
void annotatedMethod() {
logger.info("Even though this method is annotated it will not be instrumented when invoked from explainCaveat() method below")
}
void explainCaveat() {
annotatedMethod()
}
}
Field values can be created using MetricsLogger.addField(key, value)
method. The value will be overwritten if MetricsLogger.addField()
is called with the same key again
within the scope of a request.
import com.mastercard.labs.metricslogger.MetricsLogger
class FieldService {
void addFieldValues() {
MetricsLogger.addField("CardUsed", "Debit")
MetricsLogger.addField("CountryCode", "AUS")
...
MetricsLogger.addField("Merchant", "CornerShop")
MetricsLogger.addField("CountryCode", "IRL")
}
}
The above code will produce Fields as below if it gets executed while handling a request:
Time=2014-02-17T16:27:54.872Z
Application=TestMetricsLogger
Timers:...
Fields:CardUsed=Debit,CountryCode=IRL,Merchant=CornerShop
Numerics:...
Request:...
Response:...
---EOE-----------------------------------------------------
Numeric values can be created in metrics log using MetricsLogger.addNumeric(key, value)
method. The value will be summed if MetricsLogger.addNumeric()
is called with the
same key again within the scope of a request.
import com.mastercard.labs.metricslogger.MetricsLogger
class NumericService {
void createNumericValues() {
MetricsLogger.addNumeric("NumberOfItems", 25)
MetricsLogger.addNumeric("SuccessCount", 1)
...
MetricsLogger.addNumeric("OrderAmount", 123.45)
MetricsLogger.addNumeric("SuccessCount", 1)
}
}
The above code will produce Numerics as below if it gets executed while handling a request:
Time=2014-02-17T17:27:54.872Z
Application=TestMetricsLogger
Timers:...
Fields:...
Numerics:NumberOfItems=25.0,SuccessCount=2.0,OrderAmount=123.45
Request:...
Response:...
---EOE-----------------------------------------------------
Metrics Log Entries are not restricted to web requests served by grails app. They can be created outside the context of a request and flushed out when it is populated with enough metrics. For example batch/quartz jobs can be instrumented this way. Application bootstrap can create a metrics log entry and that can be used to correlate events that occur following a restart or new deployment of the app.
import com.mastercard.labs.metricslogger.LogEntry
class FakeCleanUpJob {
static triggers = {
simple repeatInterval: 60000l // execute job once a minute
}
def execute() {
dealWithStandAloneLogEntry()
}
void dealWithStandAloneLogEntry() {
LogEntry logEntry = new LogEntry();
logEntry.startTimer("dealingWithStandAloneLogEntry");
logEntry.addField("QuartzJobName", "PeriodicCleanUp");
logEntry.addNumeric("NumberOfItemsToCleanUp", 200);
logEntry.startTimer("cleanUp");
logEntry.addField("HostName", "cleanup-host");
logEntry.addNumeric("NumberOfItemsSuccessfullyCleaned", 190);
logEntry.addNumeric("NumberOfItemsFailedToBeCleaned", 10);
logEntry.addNumeric("SpaceSavedFromTheCleanUpInGB", 19.4);
logEntry.stopTimer("cleanUp");
logEntry.stopTimer("dealingWithStandAloneLogEntry");
logEntry.flush();
}
}
The above code produces Metrics Log Entries like below:
Time=2014-02-17T15:34:12.417Z
Application=TestMetricsLoggerNonRequests
Timers:cleanUp=1,dealingWithStandAloneLogEntry=2
Fields:QuartzJobName=PeriodicCleanUp,HostName=cleanup-host
Numerics:NumberOfItemsToCleanUp=200.0,SpaceSavedFromTheCleanUpInGB=19.4,NumberOfItemsSuccessfullyCleaned=190.0,NumberOfItemsFailedToBeCleaned=10.0
Request:null
Response:null
---EOE-----------------------------------------------------
Time=2014-02-17T15:35:12.214Z
Application=TestMetricsLoggerNonRequests
Timers:cleanUp=0,dealingWithStandAloneLogEntry=0
Fields:QuartzJobName=PeriodicCleanUp,HostName=cleanup-host
Numerics:NumberOfItemsToCleanUp=200.0,SpaceSavedFromTheCleanUpInGB=19.4,NumberOfItemsSuccessfullyCleaned=190.0,NumberOfItemsFailedToBeCleaned=10.0
Request:null
Response:null
---EOE-----------------------------------------------------