org.glowroot.central.SyntheticMonitorService.java Source code

Java tutorial

Introduction

Here is the source code for org.glowroot.central.SyntheticMonitorService.java

Source

/*
 * Copyright 2017 the original author or authors.
 *
 * 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.glowroot.central;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.machinepublishers.jbrowserdriver.JBrowserDriver;
import com.machinepublishers.jbrowserdriver.RequestHeaders;
import com.machinepublishers.jbrowserdriver.Settings;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.util.CharsetUtil;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.immutables.value.Value;
import org.openqa.selenium.WebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.glowroot.agent.api.Glowroot;
import org.glowroot.agent.api.Instrumentation;
import org.glowroot.central.RollupService.AgentRollupConsumer;
import org.glowroot.central.repo.AgentDao;
import org.glowroot.central.repo.ConfigRepositoryImpl;
import org.glowroot.central.repo.SyntheticResultDao;
import org.glowroot.central.repo.TriggeredAlertDao;
import org.glowroot.common.repo.AgentRepository.AgentRollup;
import org.glowroot.common.repo.util.Compilations;
import org.glowroot.common.repo.util.Encryption;
import org.glowroot.common.util.Clock;
import org.glowroot.common.util.Styles;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.AlertConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.SyntheticMonitorConfig;

import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

class SyntheticMonitorService implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(SyntheticMonitorService.class);

    private static final Pattern encryptedPattern = Pattern.compile("\"ENCRYPTED:([^\"]*)\"");

    public static final RequestHeaders REQUEST_HEADERS;

    static {
        // this list is from com.machinepublishers.jbrowserdriver.RequestHeaders,
        // with added Glowroot-Transaction-Type header
        LinkedHashMap<String, String> headersTmp = new LinkedHashMap<>();
        headersTmp.put("Host", RequestHeaders.DYNAMIC_HEADER);
        headersTmp.put("Connection", "keep-alive");
        headersTmp.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
        headersTmp.put("Upgrade-Insecure-Requests", "1");
        headersTmp.put("User-Agent", RequestHeaders.DYNAMIC_HEADER);
        headersTmp.put("Referer", RequestHeaders.DYNAMIC_HEADER);
        headersTmp.put("Accept-Encoding", "gzip, deflate, sdch");
        headersTmp.put("Accept-Language", "en-US,en;q=0.8");
        headersTmp.put("Cookie", RequestHeaders.DYNAMIC_HEADER);
        headersTmp.put("Glowroot-Transaction-Type", "Synthetic");

        REQUEST_HEADERS = new RequestHeaders(headersTmp);
    }

    private final AgentDao agentDao;
    private final ConfigRepositoryImpl configRepository;
    private final TriggeredAlertDao triggeredAlertDao;
    private final AlertingService alertingService;

    private final SyntheticResultDao syntheticResponseDao;
    private final Ticker ticker;
    private final Clock clock;

    private final ExecutorService mainLoopExecutor;
    private final ExecutorService checkExecutor;

    private final Set<SyntheticMonitorUniqueKey> activeSyntheticMonitors = Sets.newConcurrentHashSet();

    private final ListeningExecutorService syntheticUserTestExecutor = MoreExecutors
            .listeningDecorator(Executors.newCachedThreadPool());

    private volatile boolean closed;

    SyntheticMonitorService(AgentDao agentDao, ConfigRepositoryImpl configRepository,
            TriggeredAlertDao triggeredAlertDao, AlertingService alertingService,
            SyntheticResultDao syntheticResponseDao, Ticker ticker, Clock clock) {
        this.agentDao = agentDao;
        this.configRepository = configRepository;
        this.triggeredAlertDao = triggeredAlertDao;
        this.alertingService = alertingService;
        this.syntheticResponseDao = syntheticResponseDao;
        this.ticker = ticker;
        this.clock = clock;
        mainLoopExecutor = Executors.newSingleThreadExecutor();
        checkExecutor = Executors.newCachedThreadPool();
        mainLoopExecutor.execute(castInitialized(this));
    }

    @Override
    public void run() {
        while (!closed) {
            try {
                // FIXME spread out agent checks over the minute
                Thread.sleep(60000);
                runInternal();
            } catch (InterruptedException e) {
                continue;
            } catch (Throwable t) {
                logger.error(t.getMessage(), t);
            }
        }
    }

    void close() throws InterruptedException {
        closed = true;
        // shutdownNow() is needed here to send interrupt to SyntheticMonitorService thread
        mainLoopExecutor.shutdownNow();
        if (!mainLoopExecutor.awaitTermination(10, SECONDS)) {
            throw new IllegalStateException("Could not terminate executor");
        }
    }

    @Instrumentation.Transaction(transactionType = "Background", transactionName = "Outer synthetic monitor loop", traceHeadline = "Outer synthetic monitor loop", timer = "outer synthetic monitor loop")
    private void runInternal() throws Exception {
        Glowroot.setTransactionOuter();
        for (AgentRollup agentRollup : agentDao.readAgentRollups()) {
            consumeAgentRollups(agentRollup, this::runSyntheticMonitors);
        }
    }

    private void consumeAgentRollups(AgentRollup agentRollup, AgentRollupConsumer agentRollupConsumer)
            throws Exception {
        for (AgentRollup childAgentRollup : agentRollup.children()) {
            consumeAgentRollups(childAgentRollup, agentRollupConsumer);
        }
        agentRollupConsumer.accept(agentRollup);
    }

    private void runSyntheticMonitors(AgentRollup agentRollup) throws InterruptedException {
        List<SyntheticMonitorConfig> syntheticMonitorConfigs;
        try {
            syntheticMonitorConfigs = configRepository.getSyntheticMonitorConfigs(agentRollup.id());
        } catch (Exception e) {
            logger.error("{} - {}", agentRollup.display(), e.getMessage(), e);
            return;
        }
        if (syntheticMonitorConfigs.isEmpty()) {
            return;
        }
        for (SyntheticMonitorConfig syntheticMonitorConfig : syntheticMonitorConfigs) {
            List<AlertConfig> alertConfigs;
            try {
                alertConfigs = configRepository.getAlertConfigsForSyntheticMonitorId(agentRollup.id(),
                        syntheticMonitorConfig.getId());
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                continue;
            }
            checkExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        switch (syntheticMonitorConfig.getKind()) {
                        case PING:
                            runPing(agentRollup, syntheticMonitorConfig, alertConfigs);
                            break;
                        case JAVA:
                            runJava(agentRollup, syntheticMonitorConfig, alertConfigs);
                            break;
                        default:
                            throw new IllegalStateException(
                                    "Unexpected synthetic kind: " + syntheticMonitorConfig.getKind());
                        }
                    } catch (Exception e) {
                        logger.error("{} - {}", agentRollup.display(), e.getMessage(), e);
                    }
                }
            });
        }
    }

    @Instrumentation.Transaction(transactionType = "Background", transactionName = "Synthetic monitor", traceHeadline = "Synthetic monitor: {{0.id}}", timer = "synthetic monitor")
    public void runPing(AgentRollup agentRollup, SyntheticMonitorConfig syntheticMonitorConfig,
            List<AlertConfig> alertConfigs) throws Exception {
        runSyntheticMonitor(agentRollup, syntheticMonitorConfig, alertConfigs, new Callable<ListenableFuture<?>>() {
            @Override
            public ListenableFuture<?> call() throws Exception {
                return runPing(syntheticMonitorConfig.getPingUrl());
            }
        });
    }

    @Instrumentation.Transaction(transactionType = "Background", transactionName = "Synthetic monitor", traceHeadline = "Synthetic monitor: {{0.id}}", timer = "synthetic monitor")
    public void runJava(AgentRollup agentRollup, SyntheticMonitorConfig syntheticMonitorConfig,
            List<AlertConfig> alertConfigs) throws Exception {
        Matcher matcher = encryptedPattern.matcher(syntheticMonitorConfig.getJavaSource());
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String encryptedPassword = checkNotNull(matcher.group(1));
            matcher.appendReplacement(sb,
                    "\"" + Encryption.decrypt(encryptedPassword, configRepository.getSecretKey()) + "\"");
        }
        matcher.appendTail(sb);
        runSyntheticMonitor(agentRollup, syntheticMonitorConfig, alertConfigs, new Callable<ListenableFuture<?>>() {
            @Override
            public ListenableFuture<?> call() throws Exception {
                return runJava(sb.toString());
            }
        });
    }

    private void runSyntheticMonitor(AgentRollup agentRollup, SyntheticMonitorConfig syntheticMonitorConfig,
            List<AlertConfig> alertConfigs, Callable<ListenableFuture<?>> callable) throws Exception {
        final SyntheticMonitorUniqueKey uniqueKey = ImmutableSyntheticMonitorUniqueKey.of(agentRollup.id(),
                syntheticMonitorConfig.getId());
        if (!activeSyntheticMonitors.add(uniqueKey)) {
            return;
        }
        long startTime = ticker.read();
        Stopwatch stopwatch = Stopwatch.createStarted();
        final ListenableFuture<?> future;
        try {
            future = callable.call();
        } catch (Exception e) {
            logger.debug(e.getMessage(), e);
            activeSyntheticMonitors.remove(uniqueKey);
            long durationNanos = ticker.read() - startTime;
            long captureTime = clock.currentTimeMillis();
            syntheticResponseDao.store(agentRollup.id(), syntheticMonitorConfig.getId(), captureTime, durationNanos,
                    true);
            for (AlertConfig alertConfig : alertConfigs) {
                sendPingOrSyntheticAlertIfStatusChanged(agentRollup, syntheticMonitorConfig, alertConfig, true,
                        e.getMessage());
            }
            return;
        }
        future.addListener(new Runnable() {
            @Override
            public void run() {
                // remove "lock" after completion, not just after possible timeout
                activeSyntheticMonitors.remove(uniqueKey);
                long durationNanos = ticker.read() - startTime;
                long captureTime = clock.currentTimeMillis();
                boolean error = false;
                try {
                    future.get();
                } catch (InterruptedException | ExecutionException e) {
                    error = true;
                }
                try {
                    syntheticResponseDao.store(agentRollup.id(), syntheticMonitorConfig.getId(), captureTime,
                            durationNanos, error);
                } catch (Exception e) {
                    logger.error(e.getMessage(), e);
                }
            }
        }, MoreExecutors.directExecutor());
        int maxThresholdMillis = Integer.MAX_VALUE;
        for (AlertConfig alertConfig : alertConfigs) {
            maxThresholdMillis = Math.max(maxThresholdMillis, alertConfig.getThresholdMillis().getValue());
        }
        boolean success;
        String errorMessage;
        try {
            future.get(maxThresholdMillis, MILLISECONDS);
            success = true;
            errorMessage = null;
        } catch (TimeoutException e) {
            logger.debug(e.getMessage(), e);
            success = false;
            errorMessage = null;
        } catch (ExecutionException e) {
            logger.debug(e.getMessage(), e);
            success = false;
            errorMessage = getRootCause(e).getMessage();
        }
        if (success) {
            for (AlertConfig alertConfig : alertConfigs) {
                boolean currentlyTriggered = stopwatch.elapsed(MILLISECONDS) >= alertConfig.getThresholdMillis()
                        .getValue();
                sendPingOrSyntheticAlertIfStatusChanged(agentRollup, syntheticMonitorConfig, alertConfig,
                        currentlyTriggered, null);
            }
        } else {
            for (AlertConfig alertConfig : alertConfigs) {
                sendPingOrSyntheticAlertIfStatusChanged(agentRollup, syntheticMonitorConfig, alertConfig, true,
                        errorMessage);
            }
        }
    }

    private void sendPingOrSyntheticAlertIfStatusChanged(AgentRollup agentRollup,
            SyntheticMonitorConfig syntheticMonitorConfig, AlertConfig alertConfig, boolean currentlyTriggered,
            @Nullable String errorMessage) throws Exception {
        boolean previouslyTriggered = triggeredAlertDao.exists(agentRollup.id(), alertConfig.getId());
        if (previouslyTriggered && !currentlyTriggered) {
            triggeredAlertDao.delete(agentRollup.id(), alertConfig.getId());
            sendAlert(agentRollup.display(), syntheticMonitorConfig, alertConfig, true, null);
        } else if (!previouslyTriggered && currentlyTriggered) {
            triggeredAlertDao.insert(agentRollup.id(), alertConfig.getId());
            sendAlert(agentRollup.display(), syntheticMonitorConfig, alertConfig, false, errorMessage);
        }
    }

    private ListenableFuture<?> runJava(final String javaSource) {
        return syntheticUserTestExecutor.submit(new Callable</*@Nullable*/ Void>() {
            @Override
            public @Nullable Void call() throws Exception {
                Class<?> syntheticUserTestClass = Compilations.compile(javaSource);
                // validation for default constructor and test method occurs on save
                Constructor<?> defaultConstructor = syntheticUserTestClass.getConstructor();
                Method method = syntheticUserTestClass.getMethod("test", new Class[] { WebDriver.class });
                JBrowserDriver driver = new JBrowserDriver(
                        Settings.builder().requestHeaders(REQUEST_HEADERS).build());
                try {
                    method.invoke(defaultConstructor.newInstance(), driver);
                } finally {
                    driver.quit();
                }
                return null;
            }
        });
    }

    private void sendAlert(String agentDisplay, SyntheticMonitorConfig syntheticMonitorConfig,
            AlertConfig alertConfig, boolean ok, @Nullable String errorMessage) throws Exception {
        // subject is the same between initial and ok messages so they will be threaded by gmail
        String subject = "Glowroot alert";
        if (!agentDisplay.equals("")) {
            subject += " - " + agentDisplay;
        }
        subject += " - " + syntheticMonitorConfig.getDisplay();
        StringBuilder sb = new StringBuilder();
        sb.append(syntheticMonitorConfig.getDisplay());
        if (errorMessage == null) {
            if (ok) {
                sb.append(" time has dropped back below alert threshold of ");
            } else {
                sb.append(" time exceeded alert threshold of ");
            }
            int thresholdMillis = alertConfig.getThresholdMillis().getValue();
            sb.append(thresholdMillis);
            sb.append(" millisecond");
            if (thresholdMillis != 1) {
                sb.append("s");
            }
            sb.append(".");
        } else {
            sb.append(" resulted in error: ");
            sb.append(errorMessage);
        }
        alertingService.sendNotification(alertConfig, subject, sb.toString());
    }

    private static Throwable getRootCause(Throwable t) {
        Throwable cause = t.getCause();
        if (cause == null) {
            return t;
        } else {
            return getRootCause(cause);
        }
    }

    @SuppressWarnings("return.type.incompatible")
    private static <T> /*@Initialized*/ T castInitialized(/*@UnderInitialization*/ T obj) {
        return obj;
    }

    private static ListenableFuture<HttpResponseStatus> runPing(String url) throws Exception {
        URI uri = new URI(url);
        String scheme = uri.getScheme();
        if (scheme == null) {
            throw new IllegalStateException("URI missing scheme");
        }
        final boolean ssl = uri.getScheme().equalsIgnoreCase("https");
        final String host = uri.getHost();
        if (host == null) {
            throw new IllegalStateException("URI missing host");
        }
        final int port;
        if (uri.getPort() == -1) {
            port = ssl ? 443 : 80;
        } else {
            port = uri.getPort();
        }
        final EventLoopGroup group = new NioEventLoopGroup();
        final HttpClientHandler httpClientHandler = new HttpClientHandler();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline p = ch.pipeline();
                if (ssl) {
                    SslContext sslContext = SslContextBuilder.forClient().build();
                    p.addLast(sslContext.newHandler(ch.alloc(), host, port));
                }
                p.addLast(new HttpClientCodec());
                p.addLast(new HttpObjectAggregator(1048576));
                p.addLast(httpClientHandler);
            }
        });
        final HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                uri.getRawPath());
        request.headers().set(HttpHeaderNames.HOST, host);
        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
        request.headers().set("Glowroot-Transaction-Type", "Synthetic");
        ChannelFuture future = bootstrap.connect(host, port);
        final SettableFuture<HttpResponseStatus> settableFuture = SettableFuture.create();
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                Channel ch = future.channel();
                if (future.isSuccess()) {
                    ch.writeAndFlush(request);
                }
                ch.closeFuture().addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) {
                        if (future.isSuccess()) {
                            HttpResponseStatus responseStatus = httpClientHandler.responseStatus;
                            if (HttpResponseStatus.OK.equals(responseStatus)) {
                                settableFuture.set(responseStatus);
                            } else {
                                settableFuture.setException(
                                        new Exception("Unexpected http response status: " + responseStatus));
                            }
                        } else {
                            settableFuture.setException(future.cause());
                        }
                        group.shutdownGracefully();
                    }
                });
            }
        });
        return settableFuture;
    }

    @Value.Immutable
    @Styles.AllParameters
    interface SyntheticMonitorUniqueKey {
        String agentRollupId();

        String syntheticMonitorId();
    }

    private static class HttpClientHandler extends SimpleChannelInboundHandler<HttpObject> {

        private volatile @MonotonicNonNull HttpResponseStatus responseStatus;

        @Override
        public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
            if (msg instanceof HttpResponse) {
                HttpResponse response = (HttpResponse) msg;
                responseStatus = response.status();
                if (!responseStatus.equals(HttpResponseStatus.OK) && logger.isDebugEnabled()
                        && msg instanceof HttpContent) {
                    HttpContent httpContent = (HttpContent) msg;
                    String content = httpContent.content().toString(CharsetUtil.UTF_8);
                    logger.debug("unexpected response status: {}, content: {}", responseStatus, content);
                }
            } else {
                logger.error("unexpected response: {}", msg);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            logger.error(cause.getMessage(), cause);
            ctx.close();
        }
    }
}