/*
* Copyright (c) 2014 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.werval.modules.metrics;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import io.werval.api.Application;
import io.werval.api.Config;
import io.werval.api.Mode;
import io.werval.api.Plugin;
import io.werval.api.events.ConnectionEvent;
import io.werval.api.events.Event;
import io.werval.api.events.HttpEvent;
import io.werval.api.events.Registration;
import io.werval.api.exceptions.ActivationException;
import io.werval.api.routes.Route;
import io.werval.api.routes.RouteBuilder;
import io.werval.modules.json.JSON;
import com.codahale.metrics.ConsoleReporter;
import com.codahale.metrics.CsvReporter;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Reporter;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Slf4jReporter;
import com.codahale.metrics.Timer;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.health.SharedHealthCheckRegistries;
import com.codahale.metrics.health.jvm.ThreadDeadlockHealthCheck;
import com.codahale.metrics.json.HealthCheckModule;
import com.codahale.metrics.json.MetricsModule;
import com.codahale.metrics.jvm.BufferPoolMetricSet;
import com.codahale.metrics.jvm.ClassLoadingGaugeSet;
import com.codahale.metrics.jvm.FileDescriptorRatioGauge;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
import org.slf4j.LoggerFactory;
import org.slf4j.MarkerFactory;
import static java.lang.management.ManagementFactory.getPlatformMBeanServer;
import static java.util.Arrays.asList;
import static java.util.Collections.EMPTY_LIST;
import static java.util.Locale.US;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static io.werval.api.Mode.DEV;
import static io.werval.api.http.Method.GET;
/**
* Metrics Plugin.
*/
public class MetricsPlugin
implements Plugin<Metrics>
{
/**
* Event Listener used for Connection and Http metrics.
*/
private static final class EventListener
implements Consumer<Event>
{
private final MetricRegistry metrics;
private final Map<String, Timer.Context> requestTimers;
private final boolean connections;
private final boolean requests;
private final boolean success;
private final boolean redirections;
private final boolean clientErrors;
private final boolean serverErrors;
private final boolean unknown;
private EventListener(
MetricRegistry metrics, Map<String, Timer.Context> requestTimers,
boolean connections, boolean requests,
boolean success, boolean redirections, boolean clientErrors, boolean serverErrors, boolean unknown
)
{
this.metrics = metrics;
this.requestTimers = requestTimers;
this.connections = connections;
this.requests = requests;
this.success = success;
this.redirections = redirections;
this.clientErrors = clientErrors;
this.serverErrors = serverErrors;
this.unknown = unknown;
}
@Override
public void accept( Event e )
{
if( connections && e instanceof ConnectionEvent.Opened )
{
// Increment open-connections Counter
metrics.counter( "io.werval.http.open-connections" ).inc();
}
else if( connections && e instanceof ConnectionEvent.Closed )
{
// Decrement open-connections Counter
metrics.counter( "io.werval.http.open-connections" ).dec();
}
else if( requests && e instanceof HttpEvent.RequestReceived )
{
// Start requests Timer
requestTimers.put(
( (HttpEvent.RequestReceived) e ).identity(),
metrics.timer( "io.werval.http.requests" ).time()
);
}
else if( e instanceof HttpEvent.ResponseSent )
{
if( requests )
{
// Stop requests Timer
Optional.ofNullable( requestTimers.remove( ( (HttpEvent.ResponseSent) e ).identity() ) )
.ifPresent( t -> t.close() );
}
// Mark appropriate response status class Meter
switch( ( (HttpEvent.ResponseSent) e ).status().statusClass() )
{
case SUCCESS:
if( success )
{
metrics.meter( "io.werval.http.success" ).mark();
}
break;
case REDIRECTION:
if( redirections )
{
metrics.meter( "io.werval.http.redirections" ).mark();
}
break;
case CLIENT_ERROR:
if( clientErrors )
{
metrics.meter( "io.werval.http.client-errors" ).mark();
}
break;
case SERVER_ERROR:
if( serverErrors )
{
metrics.meter( "io.werval.http.server-errors" ).mark();
}
break;
case INFORMATIONAL:
case UNKNOWN:
default:
if( unknown )
{
metrics.meter( "io.werval.http.unknown" ).mark();
}
}
}
}
}
private Map<String, Timer.Context> requestTimers;
private List<Reporter> reporters;
private Metrics api;
private Registration eventRegistration;
@Override
public Class<Metrics> apiType()
{
return Metrics.class;
}
@Override
public List<Class<?>> dependencies( Config config )
{
return Arrays.asList( JSON.class );
}
@Override
public Metrics api()
{
return api;
}
@Override
public List<Route> firstRoutes( Mode mode, RouteBuilder builder )
{
if( mode == DEV )
{
return asList(
builder.route( GET ).on( "/@metrics" ).to( Tools.class, c -> c.devShellIndex() ).build(),
builder.route( GET ).on( "/@metrics/metrics" ).to( Tools.class, c -> c.metrics() ).build(),
builder.route( GET ).on( "/@metrics/health-checks" ).to( Tools.class, c -> c.healthchecks() ).build(),
builder.route( GET ).on( "/@metrics/thread-dump" ).to( Tools.class, c -> c.threadDump() ).build()
);
}
return EMPTY_LIST;
}
@Override
public void onActivate( Application application )
throws ActivationException
{
application.plugin( JSON.class ).mapper()
.registerModule( new MetricsModule( SECONDS, MILLISECONDS, true ) )
.registerModule( new HealthCheckModule() );
MetricRegistry metrics = new MetricRegistry();
HealthCheckRegistry healthChecks = new HealthCheckRegistry();
registerMetrics( application, metrics );
registerMetricsReporters( application, metrics );
registerHealthChecks( application, healthChecks );
api = new Metrics( metrics, healthChecks );
}
@Override
public void onPassivate( Application application )
{
requestTimers.values().forEach( t -> t.stop() );
requestTimers = null;
reporters.forEach(
r ->
{
if( r instanceof ScheduledReporter )
{
( (ScheduledReporter) r ).stop();
}
else if( r instanceof JmxReporter )
{
( (JmxReporter) r ).stop();
}
}
);
reporters = null;
api = null;
eventRegistration.unregister();
eventRegistration = null;
SharedMetricRegistries.clear();
SharedHealthCheckRegistries.clear();
}
private void registerMetrics( Application application, MetricRegistry metrics )
{
Config config = application.config().atKey( "metrics" );
// JVM Meters
if( config.bool( "jvm.bufferpools.enabled" ) )
{
metrics.register( "jvm.bufferpools", new BufferPoolMetricSet( getPlatformMBeanServer() ) );
}
if( config.bool( "jvm.threadstates.enabled" ) )
{
metrics.register( "jvm.threadstates", new ThreadStatesGaugeSet() );
}
if( config.bool( "jvm.classloading.enabled" ) )
{
metrics.register( "jvm.classloading", new ClassLoadingGaugeSet() );
}
if( config.bool( "jvm.garbagecollection.enabled" ) )
{
metrics.register( "jvm.garbagecollection", new GarbageCollectorMetricSet() );
}
if( config.bool( "jvm.memory.enabled" ) )
{
metrics.register( "jvm.memory", new MemoryUsageGaugeSet() );
}
if( config.bool( "jvm.filedescriptors.enabled" ) )
{
metrics.register( "jvm.filedescriptors.ratio", new FileDescriptorRatioGauge() );
}
// Connection & HTTP Metrics
requestTimers = new ConcurrentHashMap<>();
eventRegistration = application.events().registerListener(
new EventListener(
metrics,
requestTimers,
config.bool( "http.connections.enabled" ),
config.bool( "http.requests.enabled" ),
config.bool( "http.success.enabled" ),
config.bool( "http.redirections.enabled" ),
config.bool( "http.client_errors.enabled" ),
config.bool( "http.server_errors.enabled" ),
config.bool( "http.unknown.enabled" )
)
);
}
private void registerMetricsReporters( Application application, MetricRegistry metrics )
{
Config config = application.config().atKey( "metrics" );
reporters = new ArrayList<>();
// JMX Reporter
if( config.bool( "reports.jmx.enabled" ) )
{
JmxReporter jmx = JmxReporter.forRegistry( metrics )
.convertRatesTo( TimeUnit.SECONDS )
.convertDurationsTo( TimeUnit.MILLISECONDS )
.build();
jmx.start();
reporters.add( jmx );
}
// Console Reporter
if( config.bool( "reports.console.enabled" ) )
{
ConsoleReporter console = ConsoleReporter.forRegistry( metrics )
.convertRatesTo( TimeUnit.SECONDS )
.convertDurationsTo( TimeUnit.MILLISECONDS )
.build();
console.start( config.seconds( "reports.console.periodicity" ), TimeUnit.SECONDS );
reporters.add( console );
}
// SLF4J Reporter
if( config.bool( "reports.slf4j.enabled" ) )
{
final Slf4jReporter slf4j = Slf4jReporter.forRegistry( metrics )
.outputTo( LoggerFactory.getLogger( config.string( "reports.slf4j.logger" ) ) )
.withLoggingLevel(
Slf4jReporter.LoggingLevel.valueOf( config.string( "reports.slf4j.level" ).toUpperCase( US ) )
)
.markWith( MarkerFactory.getMarker( "metrics" ) )
.convertRatesTo( TimeUnit.SECONDS )
.convertDurationsTo( TimeUnit.MILLISECONDS )
.build();
slf4j.start( config.seconds( "reports.slf4j.periodicity" ), TimeUnit.SECONDS );
reporters.add( slf4j );
}
// CSV Reporter
if( config.bool( "reports.csv.enabled" ) )
{
File csvReportDir = new File( config.string( "reports.csv.directory" ) );
csvReportDir.mkdirs();
final CsvReporter csv = CsvReporter.forRegistry( metrics )
.formatFor( Locale.forLanguageTag( config.string( "reports.csv.locale" ) ) )
.convertRatesTo( TimeUnit.SECONDS )
.convertDurationsTo( TimeUnit.MILLISECONDS )
.build( csvReportDir );
csv.start( config.seconds( "reports.csv.periodicity" ), TimeUnit.SECONDS );
reporters.add( csv );
}
}
private void registerHealthChecks( Application application, HealthCheckRegistry healthChecks )
{
Config config = application.config().atKey( "metrics" );
// JVM HealthChecks
if( config.bool( "healthchecks.deadlocks.enabled" ) )
{
healthChecks.register( "jvm.deadlocks", new ThreadDeadlockHealthCheck() );
}
}
}