/**
* Copyright 2015-2016 Red Hat, Inc, and individual contributors.
*
* 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 org.wildfly.swarm.monitor.runtime;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.enterprise.inject.Vetoed;
import javax.naming.NamingException;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientResponse;
import io.undertow.server.Connectors;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.ServerConnection;
import io.undertow.server.protocol.http.HttpServerConnection;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Protocols;
import io.undertow.util.StringReadChannelListener;
import org.jboss.logging.Logger;
import org.wildfly.swarm.monitor.HealthMetaData;
import org.xnio.ChannelListeners;
import org.xnio.IoUtils;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.xnio.channels.StreamSinkChannel;
/**
* The actual monitoring HTTP endpoints. These are wrapped by {@link SecureHttpContexts}.
*
* @author Heiko Braun
*/
@Vetoed
class HttpContexts implements HttpHandler {
protected ThreadLocal<CountDownLatch> dispatched = new ThreadLocal<>();
private AttachmentKey<List> RESPONSES = AttachmentKey.create(List.class);
static AttachmentKey<String> TOKEN = AttachmentKey.create(String.class);
public HttpContexts(HttpHandler next) {
try {
this.worker = Xnio.getInstance().createWorker(
OptionMap.builder()
.set(Options.WORKER_IO_THREADS, 5)
.set(Options.WORKER_TASK_CORE_THREADS, 5)
.set(Options.WORKER_TASK_MAX_THREADS, 10)
.set(Options.TCP_NODELAY, true)
.getMap()
);
} catch (IOException e) {
throw new IllegalStateException("Failed to create worker pool");
}
this.next = next;
try {
this.monitor = Monitor.lookup();
} catch (NamingException e) {
throw new RuntimeException("Failed to lookup monitor", e);
}
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
//System.out.println(exchange.getRequestPath() +" on "+Thread.currentThread());
if (dispatched.get() != null && dispatched.get().getCount() == 1) {
next.handleRequest(exchange);
dispatched.set(null);
return;
}
if (NODE.equals(exchange.getRequestPath())) {
nodeInfo(exchange);
return;
} else if (HEAP.equals(exchange.getRequestPath())) {
heap(exchange);
return;
} else if (THREADS.equals(exchange.getRequestPath())) {
threads(exchange);
return;
} else if (HEALTH.equals(exchange.getRequestPath())) {
proxyRequests(exchange);
return;
}
next.handleRequest(exchange);
}
private void proxyRequests(HttpServerExchange exchange) {
if (monitor.getHealthURIs().isEmpty()) {
noHealthEndpoints(exchange);
} else {
try {
final List<InVMResponse> responses = new CopyOnWriteArrayList<>();
CountDownLatch latch = new CountDownLatch(monitor.getHealthURIs().size());
dispatched.set(latch);
for (HealthMetaData healthCheck : monitor.getHealthURIs()) {
invokeHealthInVM(exchange, healthCheck, responses, latch);
}
latch.await(10, TimeUnit.SECONDS);
if (latch.getCount() > 0) {
throw new Exception("Probe timed out");
}
boolean failed = false;
if (!responses.isEmpty()) {
if (responses.size() != monitor.getHealthURIs().size()) {
throw new RuntimeException("The number of responses does not match!");
}
StringBuffer sb = new StringBuffer("{");
sb.append("\"checks\": [\n");
int i = 0;
for (InVMResponse resp : responses) {
sb.append(resp.getPayload());
if (!failed) {
failed = resp.getStatus() != 200;
}
if (i < responses.size() - 1) {
sb.append(",\n");
}
i++;
}
sb.append("],\n");
String outcome = failed ? "DOWN" : "UP"; // we don't have policies yet, so keep it simple
sb.append("\"outcome\": \"" + outcome + "\"\n");
sb.append("}\n");
// send a response
if (failed) {
exchange.setStatusCode(503);
}
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
exchange.getResponseSender().send(sb.toString());
} else {
new RuntimeException("Responses should not be empty").printStackTrace();
exchange.setStatusCode(500);
}
exchange.endExchange();
} catch (Throwable t) {
LOG.error("Health check failed", t);
if (!exchange.isResponseStarted()) {
exchange.setStatusCode(500);
}
exchange.endExchange();
}
}
}
private void invokeHealthInVM(final HttpServerExchange exchange, HealthMetaData healthCheck, List<InVMResponse> responses, CountDownLatch latch) {
try {
String delegateContext = healthCheck.getWebContext();
final InVMConnection connection = new InVMConnection(
worker,
exchange.getConnection().getLocalAddress(InetSocketAddress.class).getPort()
);
final HttpServerExchange mockExchange = new HttpServerExchange(connection);
mockExchange.setRequestScheme("http");
mockExchange.setRequestMethod(new HttpString("GET"));
mockExchange.setProtocol(Protocols.HTTP_1_0);
mockExchange.setRequestURI(delegateContext);
mockExchange.setRequestPath(delegateContext);
mockExchange.setRelativePath(delegateContext);
mockExchange.getRequestHeaders().add(Headers.HOST, exchange.getRequestHeaders().get(Headers.HOST).getFirst());
mockExchange.putAttachment(TOKEN, EPHEMERAL_TOKEN);
mockExchange.putAttachment(RESPONSES, responses);
connection.addCloseListener(new ServerConnection.CloseListener() {
@Override
public void closed(ServerConnection connection) {
LOG.trace("Mock connection closed");
StringBuffer sb = new StringBuffer();
((InVMConnection) connection).flushTo(sb);
LOG.trace("Response payload: " + sb.toString());
if ("application/json".equals(mockExchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE))) {
responses.add(new InVMResponse(mockExchange.getStatusCode(), sb.toString()));
} else {
StringBuffer json = new StringBuffer("{");
json.append("\"id\"").append(":\"").append(mockExchange.getRelativePath()).append("\",");
json.append("\"result\"").append(":\"").append("DOWN").append("\",");
json.append("\"data\"").append(":").append("{");
json.append("\"status-code\"").append(":").append(mockExchange.getStatusCode());
json.append("}");
json.append("}");
responses.add(new InVMResponse(mockExchange.getStatusCode(), json.toString()));
}
mockExchange.removeAttachment(RESPONSES);
IoUtils.safeClose(connection);
latch.countDown();
}
});
HttpServerConnection httpConnection = (HttpServerConnection) exchange.getConnection();
mockExchange.startBlocking();
Connectors.executeRootHandler(httpConnection.getRootHandler(), mockExchange);
} catch (Throwable t) {
LOG.error("Health check failed", t);
latch.countDown();
}
}
private static final AttachmentKey<String> RESPONSE_BODY = AttachmentKey.create(String.class);
private ClientCallback<ClientExchange> createClientCallback(final List<ClientResponse> responses, CountDownLatch latch) {
return new ClientCallback<ClientExchange>() {
@Override
public void completed(final ClientExchange result) {
result.setResponseListener(new ClientCallback<ClientExchange>() {
@Override
public void completed(final ClientExchange result) {
responses.add(result.getResponse());
new StringReadChannelListener(result.getConnection().getBufferPool()) {
@Override
protected void stringDone(String string) {
result.getResponse().putAttachment(RESPONSE_BODY, string);
latch.countDown();
}
@Override
protected void error(IOException e) {
LOG.error("Failed to read response", e);
latch.countDown();
}
}.setup(result.getResponseChannel());
}
@Override
public void failed(IOException e) {
LOG.error("Failed to read response", e);
latch.countDown();
}
});
try {
result.getRequestChannel().shutdownWrites();
if (!result.getRequestChannel().flush()) {
result.getRequestChannel().getWriteSetter().set(ChannelListeners.<StreamSinkChannel>flushingChannelListener(null, null));
result.getRequestChannel().resumeWrites();
}
} catch (IOException e) {
LOG.error("Failed to read response", e);
latch.countDown();
}
}
@Override
public void failed(IOException e) {
LOG.error("Probe invocation failed", e);
latch.countDown();
}
};
}
private void noHealthEndpoints(HttpServerExchange exchange) {
exchange.setStatusCode(204);
exchange.setReasonPhrase("No health endpoints configured!");
}
private void nodeInfo(HttpServerExchange exchange) {
exchange.getResponseSender().send(monitor.getNodeInfo().toJSONString(false));
}
private void heap(HttpServerExchange exchange) {
exchange.getResponseSender().send(monitor.heap().toJSONString(false));
}
private void threads(HttpServerExchange exchange) {
exchange.getResponseSender().send(monitor.threads().toJSONString(false));
}
public static List<String> getDefaultContextNames() {
return Arrays.asList(NODE, HEAP, HEALTH, THREADS);
}
private static Logger LOG = Logger.getLogger("org.wildfly.swarm.monitor.health");
public static final String NODE = "/node";
public static final String HEAP = "/heap";
public static final String THREADS = "/threads";
public static final String HEALTH = "/health";
static final String EPHEMERAL_TOKEN = UUID.randomUUID().toString();
private final Monitor monitor;
private final HttpHandler next;
private XnioWorker worker;
class InVMResponse {
private int status;
private String payload;
public InVMResponse(int status, String payload) {
this.status = status;
this.payload = payload;
}
public int getStatus() {
return status;
}
public String getPayload() {
return payload;
}
}
}