/*
* Copyright 2015 Netflix, Inc.
*
* 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 com.netflix.discovery.shared.transport.decorator;
import java.util.EnumMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.netflix.discovery.EurekaClientNames;
import com.netflix.discovery.shared.resolver.EurekaEndpoint;
import com.netflix.discovery.shared.transport.EurekaHttpClient;
import com.netflix.discovery.shared.transport.EurekaHttpClientFactory;
import com.netflix.discovery.shared.transport.EurekaHttpResponse;
import com.netflix.discovery.shared.transport.TransportClientFactory;
import com.netflix.discovery.shared.transport.decorator.MetricsCollectingEurekaHttpClient.EurekaHttpClientRequestMetrics.Status;
import com.netflix.discovery.util.ExceptionsMetric;
import com.netflix.discovery.util.ServoUtil;
import com.netflix.servo.monitor.BasicCounter;
import com.netflix.servo.monitor.BasicTimer;
import com.netflix.servo.monitor.Counter;
import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.monitor.Stopwatch;
import com.netflix.servo.monitor.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Tomasz Bak
*/
public class MetricsCollectingEurekaHttpClient extends EurekaHttpClientDecorator {
private static final Logger logger = LoggerFactory.getLogger(MetricsCollectingEurekaHttpClient.class);
private final EurekaHttpClient delegate;
private final Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType;
private final ExceptionsMetric exceptionsMetric;
private final boolean shutdownMetrics;
public MetricsCollectingEurekaHttpClient(EurekaHttpClient delegate) {
this(delegate, initializeMetrics(), new ExceptionsMetric(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "exceptions"), true);
}
private MetricsCollectingEurekaHttpClient(EurekaHttpClient delegate,
Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType,
ExceptionsMetric exceptionsMetric,
boolean shutdownMetrics) {
this.delegate = delegate;
this.metricsByRequestType = metricsByRequestType;
this.exceptionsMetric = exceptionsMetric;
this.shutdownMetrics = shutdownMetrics;
}
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
EurekaHttpClientRequestMetrics requestMetrics = metricsByRequestType.get(requestExecutor.getRequestType());
Stopwatch stopwatch = requestMetrics.latencyTimer.start();
try {
EurekaHttpResponse<R> httpResponse = requestExecutor.execute(delegate);
requestMetrics.countersByStatus.get(mappedStatus(httpResponse)).increment();
return httpResponse;
} catch (Exception e) {
requestMetrics.connectionErrors.increment();
exceptionsMetric.count(e);
throw e;
} finally {
stopwatch.stop();
}
}
@Override
public void shutdown() {
if (shutdownMetrics) {
shutdownMetrics(metricsByRequestType);
exceptionsMetric.shutdown();
}
}
public static EurekaHttpClientFactory createFactory(final EurekaHttpClientFactory delegateFactory) {
final Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType = initializeMetrics();
final ExceptionsMetric exceptionMetrics = new ExceptionsMetric(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "exceptions");
return new EurekaHttpClientFactory() {
@Override
public EurekaHttpClient newClient() {
return new MetricsCollectingEurekaHttpClient(
delegateFactory.newClient(),
metricsByRequestType,
exceptionMetrics,
false
);
}
@Override
public void shutdown() {
shutdownMetrics(metricsByRequestType);
exceptionMetrics.shutdown();
}
};
}
public static TransportClientFactory createFactory(final TransportClientFactory delegateFactory) {
final Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType = initializeMetrics();
final ExceptionsMetric exceptionMetrics = new ExceptionsMetric(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "exceptions");
return new TransportClientFactory() {
@Override
public EurekaHttpClient newClient(EurekaEndpoint endpoint) {
return new MetricsCollectingEurekaHttpClient(
delegateFactory.newClient(endpoint),
metricsByRequestType,
exceptionMetrics,
false
);
}
@Override
public void shutdown() {
shutdownMetrics(metricsByRequestType);
exceptionMetrics.shutdown();
}
};
}
private static Map<RequestType, EurekaHttpClientRequestMetrics> initializeMetrics() {
Map<RequestType, EurekaHttpClientRequestMetrics> result = new EnumMap<>(RequestType.class);
try {
for (RequestType requestType : RequestType.values()) {
result.put(requestType, new EurekaHttpClientRequestMetrics(requestType.name()));
}
} catch (Exception e) {
logger.warn("Metrics initialization failure", e);
}
return result;
}
private static void shutdownMetrics(Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType) {
for (EurekaHttpClientRequestMetrics metrics : metricsByRequestType.values()) {
metrics.shutdown();
}
}
private static Status mappedStatus(EurekaHttpResponse<?> httpResponse) {
int category = httpResponse.getStatusCode() / 100;
switch (category) {
case 1:
return Status.x100;
case 2:
return Status.x200;
case 3:
return Status.x300;
case 4:
return Status.x400;
case 5:
return Status.x500;
}
return Status.Unknown;
}
static class EurekaHttpClientRequestMetrics {
enum Status {x100, x200, x300, x400, x500, Unknown}
private final Timer latencyTimer;
private final Counter connectionErrors;
private final Map<Status, Counter> countersByStatus;
EurekaHttpClientRequestMetrics(String resourceName) {
this.countersByStatus = createStatusCounters(resourceName);
latencyTimer = new BasicTimer(
MonitorConfig.builder(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "latency")
.withTag("id", resourceName)
.withTag("class", MetricsCollectingEurekaHttpClient.class.getSimpleName())
.build(),
TimeUnit.MILLISECONDS
);
ServoUtil.register(latencyTimer);
this.connectionErrors = new BasicCounter(
MonitorConfig.builder(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "connectionErrors")
.withTag("id", resourceName)
.withTag("class", MetricsCollectingEurekaHttpClient.class.getSimpleName())
.build()
);
ServoUtil.register(connectionErrors);
}
void shutdown() {
ServoUtil.unregister(latencyTimer, connectionErrors);
ServoUtil.unregister(countersByStatus.values());
}
private static Map<Status, Counter> createStatusCounters(String resourceName) {
Map<Status, Counter> result = new EnumMap<>(Status.class);
for (Status status : Status.values()) {
BasicCounter counter = new BasicCounter(
MonitorConfig.builder(EurekaClientNames.METRIC_TRANSPORT_PREFIX + "request")
.withTag("id", resourceName)
.withTag("class", MetricsCollectingEurekaHttpClient.class.getSimpleName())
.withTag("status", status.name())
.build()
);
ServoUtil.register(counter);
result.put(status, counter);
}
return result;
}
}
}