-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #28
- Loading branch information
Showing
1 changed file
with
204 additions
and
0 deletions.
There are no files selected for viewing
204 changes: 204 additions & 0 deletions
204
src/main/java/org/kiwiproject/dropwizard/util/health/HttpConnectionsHealthCheck.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
package org.kiwiproject.dropwizard.util.health; | ||
|
||
import static java.util.Collections.emptyList; | ||
import static java.util.function.Function.identity; | ||
import static java.util.stream.Collectors.groupingBy; | ||
import static java.util.stream.Collectors.toMap; | ||
import static java.util.stream.Collectors.toSet; | ||
import static org.kiwiproject.base.KiwiPreconditions.checkArgument; | ||
import static org.kiwiproject.base.KiwiPreconditions.requireNotNull; | ||
import static org.kiwiproject.metrics.health.HealthCheckResults.newHealthyResult; | ||
import static org.kiwiproject.metrics.health.HealthCheckResults.newResultBuilder; | ||
|
||
import com.codahale.metrics.Gauge; | ||
import com.codahale.metrics.MetricRegistry; | ||
import com.codahale.metrics.health.HealthCheck; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.ToString; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.SortedMap; | ||
|
||
/** | ||
* Health check that checks the percent of leased connections against the maximum number of connections for | ||
* JAX-RS {@link javax.ws.rs.client.Client} instances that were created using Dropwizard's | ||
* {@code io.dropwizard.client.JerseyClientBuilder}, which creates an HTTP connection pool and registers various | ||
* connection metrics that we can query. | ||
* <p> | ||
* Please note, if using a default JAX-RS {@link javax.ws.rs.client.Client} created using the normal | ||
* {@link javax.ws.rs.client.ClientBuilder}, those clients will <em>not</em> have metrics registered and will therefore | ||
* <em>not</em> be included by this check (since it won't know about them). | ||
*/ | ||
@Slf4j | ||
public class HttpConnectionsHealthCheck extends HealthCheck { | ||
|
||
/** | ||
* Default name for the health check registration. | ||
*/ | ||
public static final String DEFAULT_NAME = "httpConnections"; | ||
|
||
/** | ||
* Default percent above which this health check will report unhealthy. | ||
*/ | ||
public static final double DEFAULT_WARNING_THRESHOLD = 50.0; | ||
|
||
private static final String HTTP_CONN_MANAGER_GAUGE_PREFIX = "org.apache.http.conn.HttpClientConnectionManager."; | ||
private static final int START_INDEX = HTTP_CONN_MANAGER_GAUGE_PREFIX.length(); | ||
|
||
private final MetricRegistry metrics; | ||
private final double warningThreshold; | ||
|
||
public HttpConnectionsHealthCheck(MetricRegistry metrics) { | ||
this(metrics, DEFAULT_WARNING_THRESHOLD); | ||
} | ||
|
||
public HttpConnectionsHealthCheck(MetricRegistry metrics, double warningThreshold) { | ||
this.metrics = requireNotNull(metrics); | ||
|
||
checkArgument(warningThreshold > 0.0 && warningThreshold < 100.0, IllegalArgumentException.class, | ||
"warningThreshold must be more than 0 and less than 100"); | ||
|
||
this.warningThreshold = warningThreshold; | ||
} | ||
|
||
@Override | ||
protected Result check() { | ||
var httpMetrics = metrics.getGauges((name, metric) -> name.startsWith(HTTP_CONN_MANAGER_GAUGE_PREFIX)); | ||
var clientNames = findHttpClientMetricNames(httpMetrics); | ||
|
||
LOG.trace("Client names: {}", clientNames); | ||
|
||
if (clientNames.isEmpty()) { | ||
return newHealthyResult("No HTTP clients found with metrics"); | ||
} | ||
|
||
var clientHealth = getClientHealth(httpMetrics, clientNames); | ||
return determineResult(clientHealth, clientNames.size()); | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
private static Set<String> findHttpClientMetricNames(SortedMap<String, Gauge> httpMetrics) { | ||
return httpMetrics.keySet().stream() | ||
.map(HttpConnectionsHealthCheck::clientNameFrom) | ||
.collect(toSet()); | ||
} | ||
|
||
private static String clientNameFrom(String gaugeName) { | ||
return gaugeName.substring(START_INDEX, gaugeName.lastIndexOf('.')); | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
private Map<ClientConnectionInfo.HealthStatus, List<ClientConnectionInfo>> getClientHealth( | ||
SortedMap<String, Gauge> httpMetrics, Set<String> clientNames) { | ||
|
||
return clientNames.stream() | ||
.map(clientName -> getClientConnectionInfo(httpMetrics, clientName)) | ||
.collect(groupingBy(ClientConnectionInfo::getHealthStatus)); | ||
} | ||
|
||
@SuppressWarnings("rawtypes") | ||
private ClientConnectionInfo getClientConnectionInfo(SortedMap<String, Gauge> httpMetrics, String clientName) { | ||
var leasedConnectionsGauge = httpMetrics.get(leasedConnectionsGaugeName(clientName)); | ||
var maxConnectionsGauge = httpMetrics.get(maxConnectionsGaugeName(clientName)); | ||
|
||
var leasedConnections = (int) leasedConnectionsGauge.getValue(); | ||
var maxConnections = (int) maxConnectionsGauge.getValue(); | ||
|
||
var connectionInfo = ClientConnectionInfo.builder() | ||
.clientName(clientName) | ||
.leased(leasedConnections) | ||
.max(maxConnections) | ||
.warningThreshold(warningThreshold) | ||
.build(); | ||
|
||
LOG.trace("{}: {} of {} leased ({}%)", clientName, leasedConnections, maxConnections, | ||
connectionInfo.percentLeased); | ||
|
||
return connectionInfo; | ||
} | ||
|
||
private Result determineResult(Map<ClientConnectionInfo.HealthStatus, List<ClientConnectionInfo>> clientHealth, | ||
int totalNumberOfClients) { | ||
|
||
var healthyClients = getConnectionInfoMap(clientHealth, ClientConnectionInfo.HealthStatus.HEALTHY); | ||
var unhealthyClients = getConnectionInfoMap(clientHealth, ClientConnectionInfo.HealthStatus.UNHEALTHY); | ||
|
||
var isHealthy = unhealthyClients.isEmpty(); | ||
|
||
var builder = newResultBuilder(isHealthy) | ||
.withDetail("healthyClients", healthyClients) | ||
.withDetail("unhealthyClients", unhealthyClients); | ||
|
||
if (isHealthy) { | ||
return builder | ||
.withMessage("%d HTTP client(s) < %4.1f%% leased connections.", | ||
totalNumberOfClients, | ||
warningThreshold) | ||
.build(); | ||
} | ||
|
||
LOG.trace("Unhealthy clients: {}", unhealthyClients); | ||
|
||
return builder | ||
.withMessage("%d of %d HTTP client(s) >= %4.1f%% leased connections.", | ||
unhealthyClients.size(), | ||
totalNumberOfClients, | ||
warningThreshold) | ||
.build(); | ||
} | ||
|
||
private Map<String, ClientConnectionInfo> getConnectionInfoMap( | ||
Map<ClientConnectionInfo.HealthStatus, List<ClientConnectionInfo>> clientHealth, | ||
ClientConnectionInfo.HealthStatus healthStatus) { | ||
|
||
return clientHealth.getOrDefault(healthStatus, emptyList()).stream() | ||
.collect(toMap(ClientConnectionInfo::getClientName, identity())); | ||
} | ||
|
||
private static String leasedConnectionsGaugeName(String clientName) { | ||
return httpConnectionGaugeName(clientName, ".leased-connections"); | ||
} | ||
|
||
private static String maxConnectionsGaugeName(String clientName) { | ||
return httpConnectionGaugeName(clientName, ".max-connections"); | ||
} | ||
|
||
private static String httpConnectionGaugeName(String clientName, String type) { | ||
return HTTP_CONN_MANAGER_GAUGE_PREFIX + clientName + type; | ||
} | ||
|
||
@Getter | ||
@ToString | ||
private static class ClientConnectionInfo { | ||
final String clientName; | ||
final int leased; | ||
final int max; | ||
final double warningThreshold; | ||
final double percentLeased; | ||
|
||
enum HealthStatus { | ||
HEALTHY, UNHEALTHY | ||
} | ||
|
||
@Builder | ||
ClientConnectionInfo(String clientName, int leased, int max, double warningThreshold) { | ||
this.clientName = clientName; | ||
this.leased = leased; | ||
this.max = max; | ||
this.warningThreshold = warningThreshold; | ||
this.percentLeased = 100.0 * (leased / (double) max); | ||
} | ||
|
||
boolean isUnhealthy() { | ||
return percentLeased >= warningThreshold; | ||
} | ||
|
||
HealthStatus getHealthStatus() { | ||
return isUnhealthy() ? HealthStatus.UNHEALTHY : HealthStatus.HEALTHY; | ||
} | ||
} | ||
} |