package com.mowforth.netty.util.handlers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.net.MediaType;
import com.yammer.metrics.MetricRegistry;
import com.yammer.metrics.health.HealthCheck;
import com.yammer.metrics.health.HealthCheckRegistry;
import com.yammer.metrics.json.HealthCheckModule;
import com.yammer.metrics.json.MetricsModule;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import javax.inject.Inject;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Handler for exposing metric & healthcheck data.
*
* <p>This provides equivalent functionality of an embedded
* Jetty instance to run the {@literal MetricsServlet}
* and {@literal HealthCheckServlet} servlets provided by the
* Yammer metrics library.</p>
*
* <p>The only configurable parameter is the port to listen on.
* The endpoint can only be accessed locally, i.e. it's bound
* to {@code 127.0.0.1}.</p>
*/
@ChannelHandler.Sharable
public class MonitoringHandler extends SimpleChannelInboundHandler<DefaultFullHttpRequest> {
private static final String METRICS = "metrics";
private static final String PING = "ping";
private static final String HEALTH = "healthcheck";
private static final String TEMPLATE =
"<html lang=\"en\">\n" +
"<head>\n" +
" <title>Operational Menu</title>\n" +
"</head>\n" +
"<body>\n" +
" <h1>Operational Menu</h1>\n" +
" <ul>\n" +
" <li><a href=\"/" + METRICS + "?pretty=true\">Metrics</a></li>\n" +
" <li><a href=\"/" + PING + "\">Ping</a></li>\n" +
" <li><a href=\"/" + HEALTH + "\">Healthcheck</a></li>\n" +
" </ul>\n" +
"</body>\n" +
"</html>";
private final MetricRegistry metricRegistry;
private final HealthCheckRegistry healthCheckRegistry;
private final ObjectMapper mapper;
@Inject
public MonitoringHandler(MetricRegistry metricRegistry,
HealthCheckRegistry healthCheckRegistry) {
this.mapper = new ObjectMapper()
.registerModule(new MetricsModule(TimeUnit.SECONDS, TimeUnit.SECONDS, false))
.registerModule(new HealthCheckModule());
this.metricRegistry = metricRegistry;
this.healthCheckRegistry = healthCheckRegistry;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DefaultFullHttpRequest msg) throws Exception {
if (msg.getMethod() == HttpMethod.GET) {
route(ctx, msg);
} else {
ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_IMPLEMENTED));
}
}
private void route(ChannelHandlerContext ctx, DefaultFullHttpRequest msg) throws Exception {
DefaultFullHttpResponse response;
String uri = msg.getUri();
if (uri.equals("/")) {
response = opMenu();
} else if (uri.startsWith("/" + PING)) {
response = handlePing();
} else if (uri.startsWith("/" + HEALTH)) {
response = handleHealthcheck();
} else if (uri.startsWith("/" + METRICS)) {
QueryStringDecoder decoder = new QueryStringDecoder(uri);
List<String> data = decoder.parameters().get("pretty");
boolean pretty = true;
if (data == null || data.size() < 1 || data.get(0).isEmpty()) {
pretty = false;
}
response = handleMetrics(pretty);
} else {
// 404
response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
}
ctx.writeAndFlush(response);
}
private DefaultFullHttpResponse opMenu() {
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer(TEMPLATE.getBytes()));
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, MediaType.HTML_UTF_8);
return response;
}
private DefaultFullHttpResponse handlePing() {
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.copiedBuffer("pong".getBytes()));
}
private DefaultFullHttpResponse handleHealthcheck() throws Exception {
Map<String, HealthCheck.Result> results = healthCheckRegistry.runHealthChecks();
DefaultFullHttpResponse response;
if (results.isEmpty()) {
response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.NOT_IMPLEMENTED);
} else {
String formatted = mapper.writeValueAsString(results);
HttpResponseStatus status = isAllHealthy(results) ?
HttpResponseStatus.OK : HttpResponseStatus.INTERNAL_SERVER_ERROR;
response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status,
Unpooled.copiedBuffer(formatted.getBytes()));
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
}
return response;
}
private DefaultFullHttpResponse handleMetrics(boolean pretty) throws Exception {
String formatted;
if (pretty) {
formatted = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(metricRegistry);
} else {
formatted = mapper.writeValueAsString(metricRegistry);
}
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK, Unpooled.copiedBuffer(formatted.getBytes()));
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json");
return response;
}
private static boolean isAllHealthy(Map<String, HealthCheck.Result> results) {
for (HealthCheck.Result result : results.values()) {
if (!result.isHealthy()) {
return false;
}
}
return true;
}
}