![JAR search and dependency download from the Maven repository](/logo.png)
io.gravitee.gateway.services.healthcheck.rule.EndpointRuleHandler Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.gateway.services.healthcheck.rule;
import static java.lang.System.currentTimeMillis;
import io.gravitee.alert.api.event.Event;
import io.gravitee.common.http.HttpHeaders;
import io.gravitee.common.http.HttpStatusCode;
import io.gravitee.common.utils.UUID;
import io.gravitee.definition.model.Endpoint;
import io.gravitee.el.TemplateEngine;
import io.gravitee.el.exceptions.ExpressionEvaluationException;
import io.gravitee.gateway.core.endpoint.EndpointException;
import io.gravitee.gateway.services.healthcheck.EndpointRule;
import io.gravitee.gateway.services.healthcheck.EndpointStatusDecorator;
import io.gravitee.gateway.services.healthcheck.eval.EvaluationException;
import io.gravitee.gateway.services.healthcheck.eval.assertion.AssertionEvaluation;
import io.gravitee.gateway.services.healthcheck.http.el.EvaluableHttpResponse;
import io.gravitee.node.api.Node;
import io.gravitee.node.api.utils.NodeUtils;
import io.gravitee.plugin.alert.AlertEventProducer;
import io.gravitee.reporter.api.common.Request;
import io.gravitee.reporter.api.common.Response;
import io.gravitee.reporter.api.health.EndpointStatus;
import io.gravitee.reporter.api.health.Step;
import io.netty.channel.ConnectTimeoutException;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.http.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Iterator;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author Azize ELAMRANI (azize.elamrani at graviteesource.com)
* @author GraviteeSource Team
*/
public abstract class EndpointRuleHandler implements Handler {
private final Logger logger = LoggerFactory.getLogger(EndpointRuleHandler.class);
// Pattern reuse for duplicate slash removal
private static final Pattern DUPLICATE_SLASH_REMOVER = Pattern.compile("(? rule;
private final Vertx vertx;
private final EndpointStatusDecorator endpointStatus;
private TemplateEngine templateEngine;
private Handler statusHandler;
private AlertEventProducer alertEventProducer;
private Node node;
public EndpointRuleHandler(Vertx vertx, EndpointRule rule, TemplateEngine templateEngine) {
this.vertx = vertx;
this.rule = rule;
endpointStatus = new EndpointStatusDecorator(rule.endpoint());
this.templateEngine = templateEngine;
}
@Override
public void handle(Long timer) {
T endpoint = rule.endpoint();
logger.debug("Running health-check for endpoint: {} [{}]", endpoint.getName(), endpoint.getTarget());
// Run request for each step
for (io.gravitee.definition.model.services.healthcheck.Step step : rule.steps()) {
runStep(endpoint, step);
}
}
protected abstract HttpClientOptions createHttpClientOptions(final URI requestUri) throws Exception;
protected HttpClient createHttpClient(final URI requestUri) throws Exception {
HttpClientOptions options = createHttpClientOptions(requestUri);
return vertx.createHttpClient(options);
}
protected Future createHttpClientRequest(
final HttpClient httpClient,
URI request,
io.gravitee.definition.model.services.healthcheck.Step step
) {
RequestOptions options = prepareHttpClientRequest(request, step);
return httpClient.request(options);
}
protected RequestOptions prepareHttpClientRequest(URI request, io.gravitee.definition.model.services.healthcheck.Step step) {
final int port = request.getPort() != -1 ? request.getPort() : (HTTPS_SCHEME.equals(request.getScheme()) ? 443 : 80);
String relativeUri = (request.getRawQuery() == null) ? request.getRawPath() : request.getRawPath() + '?' + request.getRawQuery();
// Prepare request
RequestOptions options = new RequestOptions()
.setURI(relativeUri)
.setPort(port)
.setHost(request.getHost())
.setMethod(HttpMethod.valueOf(step.getRequest().getMethod().name().toUpperCase()))
.putHeader(HttpHeaders.USER_AGENT, NodeUtils.userAgent(node))
.putHeader("X-Gravitee-Request-Id", UUID.toString(UUID.random()));
if (step.getRequest().getHeaders() != null) {
step
.getRequest()
.getHeaders()
.forEach(
httpHeader -> {
String resolvedHeader = null;
try {
resolvedHeader = templateEngine.getValue(httpHeader.getValue(), String.class);
} catch (ExpressionEvaluationException e) {
logger.warn("Expression {} cannot be evaluated", httpHeader.getValue());
}
options.putHeader(httpHeader.getName(), resolvedHeader == null ? "" : resolvedHeader);
}
);
}
return options;
}
protected URI createRequest(T endpoint, io.gravitee.definition.model.services.healthcheck.Step step) {
URI targetURI = URI.create(endpoint.getTarget());
if (step.getRequest().isFromRoot()) {
try {
targetURI = new URI(targetURI.getScheme(), targetURI.getAuthority(), null, null, null);
} catch (URISyntaxException ex) {
logger.error("Unexpected error while creating healthcheck request from target[{}]", endpoint.getTarget(), ex);
}
}
final String path = step.getRequest().getPath();
if (path == null || path.trim().isEmpty()) {
return targetURI;
}
final String uri;
if (path.startsWith("/") || path.startsWith("?")) {
uri = targetURI + path;
} else {
uri = targetURI + "/" + path;
}
return URI.create(DUPLICATE_SLASH_REMOVER.matcher(uri).replaceAll("/"));
}
protected void runStep(T endpoint, io.gravitee.definition.model.services.healthcheck.Step step) {
try {
URI hcRequestUri = createRequest(endpoint, step);
HttpClient httpClient = createHttpClient(hcRequestUri);
Future healthRequestPromise = createHttpClientRequest(httpClient, hcRequestUri, step);
healthRequestPromise.onComplete(
requestPreparationEvent -> {
HttpClientRequest healthRequest = requestPreparationEvent.result();
final EndpointStatus.Builder healthBuilder = EndpointStatus
.forEndpoint(rule.api(), endpoint.getName())
.on(currentTimeMillis());
long startTime = currentTimeMillis();
Request request = new Request();
request.setMethod(step.getRequest().getMethod());
request.setUri(hcRequestUri.toString());
if (requestPreparationEvent.failed()) {
reportThrowable(requestPreparationEvent.cause(), step, healthBuilder, startTime, request, httpClient);
} else {
healthRequest.response(
healthRequestEvent -> {
if (healthRequestEvent.succeeded()) {
HttpClientResponse response = healthRequestEvent.result();
response.bodyHandler(
buffer -> {
long endTime = currentTimeMillis();
logger.debug(
"Health-check endpoint returns a response with a {} status code",
response.statusCode()
);
String body = buffer.toString();
Step healthCheckStep = buildStep(step, startTime, endTime, request, response, body);
// Append step stepBuilder
healthBuilder.step(healthCheckStep);
report(healthBuilder.build());
// Close client
httpClient.close();
}
);
response.exceptionHandler(
throwable -> {
closeThrowable(httpClient, throwable);
}
);
} else {
closeThrowable(httpClient, healthRequestEvent.cause());
}
}
);
healthRequest.exceptionHandler(
throwable -> {
reportThrowable(throwable, step, healthBuilder, startTime, request, httpClient);
}
);
// Send request
logger.debug("Execute health-check request: {}", healthRequest);
if (step.getRequest().getBody() != null && !step.getRequest().getBody().isEmpty()) {
healthRequest.end(step.getRequest().getBody());
} else {
healthRequest.end();
}
}
}
);
} catch (EndpointException ee) {
logger.error(
"An error occurs while configuring the endpoint " + endpoint.getName() + ". Healthcheck is skipped for this endpoint.",
ee
);
} catch (Exception ex) {
logger.error("An unexpected error has occurred while configuring Healthcheck for API : {}", rule.api(), ex);
}
}
private void closeThrowable(HttpClient httpClient, Throwable throwable) {
logger.error("An error has occurred during Health check response handler", throwable);
// Close client
httpClient.close();
}
private void reportThrowable(
Throwable throwable,
io.gravitee.definition.model.services.healthcheck.Step step,
EndpointStatus.Builder healthBuilder,
long startTime,
Request request,
HttpClient httpClient
) {
long endTime = currentTimeMillis();
Step failingStep = buildFailingStep(step, startTime, endTime, request, throwable);
// Append step result
healthBuilder.step(failingStep);
report(healthBuilder.build());
try {
// Close client
httpClient.close();
} catch (IllegalStateException ise) {
// Do not take care about exception when closing client
}
}
private Step buildStep(
io.gravitee.definition.model.services.healthcheck.Step step,
long startTime,
long endTime,
Request request,
HttpClientResponse response,
String body
) {
EndpointStatus.StepBuilder stepBuilder = validateAssertions(step, new EvaluableHttpResponse(response, body));
stepBuilder.request(request);
stepBuilder.responseTime(endTime - startTime);
Response healthResponse = new Response();
healthResponse.setStatus(response.statusCode());
// If validation fail, store request and response data
if (!stepBuilder.isSuccess()) {
request.setBody(step.getRequest().getBody());
if (step.getRequest().getHeaders() != null) {
request.setHeaders(getHttpHeaders(step));
}
// Extract headers
HttpHeaders headers = new HttpHeaders();
response.headers().names().forEach(headerName -> headers.put(headerName, response.headers().getAll(headerName)));
healthResponse.setHeaders(headers);
// Store body
healthResponse.setBody(body);
}
stepBuilder.response(healthResponse);
return stepBuilder.build();
}
private Step buildFailingStep(
io.gravitee.definition.model.services.healthcheck.Step step,
long startTime,
long endTime,
Request request,
Throwable throwable
) {
EndpointStatus.StepBuilder stepBuilder = EndpointStatus.forStep(step.getName());
stepBuilder.fail(throwable.getMessage());
stepBuilder.request(request);
stepBuilder.responseTime(endTime - startTime);
Response healthResponse = new Response();
// Extract request information
request.setBody(step.getRequest().getBody());
if (step.getRequest().getHeaders() != null) {
request.setHeaders(getHttpHeaders(step));
}
if (throwable instanceof ConnectTimeoutException) {
stepBuilder.fail(throwable.getMessage());
healthResponse.setStatus(HttpStatusCode.GATEWAY_TIMEOUT_504);
} else {
stepBuilder.fail(throwable.getMessage());
healthResponse.setStatus(HttpStatusCode.BAD_GATEWAY_502);
}
stepBuilder.response(healthResponse);
return stepBuilder.build();
}
private HttpHeaders getHttpHeaders(io.gravitee.definition.model.services.healthcheck.Step step) {
HttpHeaders reqHeaders = new HttpHeaders();
step
.getRequest()
.getHeaders()
.forEach(httpHeader -> reqHeaders.put(httpHeader.getName(), Collections.singletonList(httpHeader.getValue())));
return reqHeaders;
}
private EndpointStatus.StepBuilder validateAssertions(
final io.gravitee.definition.model.services.healthcheck.Step step,
final EvaluableHttpResponse response
) {
EndpointStatus.StepBuilder stepBuilder = EndpointStatus.forStep(step.getName());
// Run assertions
if (step.getResponse().getAssertions() != null) {
Iterator assertionIterator = step.getResponse().getAssertions().iterator();
boolean success = true;
while (success && assertionIterator.hasNext()) {
try {
// TODO: assertion must be compiled only one time to preserve CPU
AssertionEvaluation evaluation = new AssertionEvaluation(assertionIterator.next());
evaluation.setVariable("response", response);
// Run validation
success = evaluation.validate();
if (success) {
stepBuilder.success();
} else {
stepBuilder.fail("Assertion not validated: " + evaluation.getAssertion());
}
} catch (EvaluationException eex) {
success = false;
stepBuilder.fail(eex.getMessage());
}
}
}
return stepBuilder;
}
private void report(final EndpointStatus endpointStatus) {
final int previousStatusCode = rule.endpoint().getStatus().code();
final String previousStatusName = rule.endpoint().getStatus().name();
this.endpointStatus.updateStatus(endpointStatus.isSuccess());
endpointStatus.setState(rule.endpoint().getStatus().code());
endpointStatus.setAvailable(!rule.endpoint().getStatus().isDown());
final long responseTime = endpointStatus.getSteps().stream().mapToLong(Step::getResponseTime).sum();
endpointStatus.setResponseTime(responseTime);
final boolean transition = previousStatusCode != rule.endpoint().getStatus().code();
endpointStatus.setTransition(transition);
if (transition && alertEventProducer != null && !alertEventProducer.isEmpty()) {
final Event event = Event
.at(currentTimeMillis())
.context(CONTEXT_NODE_ID, node.id())
.context(CONTEXT_NODE_HOSTNAME, node.hostname())
.context(CONTEXT_NODE_APPLICATION, node.application())
.type(EVENT_TYPE)
.property(PROP_API, rule.api())
.property(PROP_ENDPOINT_NAME, rule.endpoint().getName())
.property(PROP_STATUS_OLD, previousStatusName)
.property(PROP_STATUS_NEW, rule.endpoint().getStatus().name())
.property(PROP_SUCCESS, endpointStatus.isSuccess())
.property(PROP_TENANT, () -> node.metadata().get("tenant"))
.property(PROP_RESPONSE_TIME, responseTime)
.property(PROP_MESSAGE, endpointStatus.getSteps().get(0).getMessage())
.build();
alertEventProducer.send(event);
}
statusHandler.handle(endpointStatus);
}
public void setStatusHandler(Handler statusHandler) {
this.statusHandler = statusHandler;
}
public void setAlertEventProducer(AlertEventProducer alertEventProducer) {
this.alertEventProducer = alertEventProducer;
}
public void setNode(Node node) {
this.node = node;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy