io.stallion.monitoring.HealthTracker.java Source code

Java tutorial

Introduction

Here is the source code for io.stallion.monitoring.HealthTracker.java

Source

/*
 * Stallion Core: A Modern Web Framework
 *
 * Copyright (C) 2015 - 2016 Stallion Software LLC.
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation, either version 2 of
 * the License, or (at your option) any later version. This program is distributed in the hope that
 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
 * License for more details. You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
 *
 *
 *
 */

package io.stallion.monitoring;

import com.sun.management.UnixOperatingSystemMXBean;
import io.stallion.asyncTasks.SimpleAsyncRunner;
import io.stallion.exceptions.ClientException;
import io.stallion.requests.StRequest;
import io.stallion.requests.StResponse;
import io.stallion.services.Log;
import io.stallion.settings.Settings;
import io.stallion.utils.DateUtils;
import io.stallion.utils.ProcessHelper;
import io.stallion.utils.GeneralUtils;
import org.apache.commons.collections4.queue.CircularFifoQueue;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;

import javax.net.ssl.*;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static io.stallion.utils.Literals.empty;

public class HealthTracker {
    private CircularFifoQueue<ExceptionInfo> exceptionQueue = new CircularFifoQueue(100);
    private CircularFifoQueue<MinuteInfo> response500s = new CircularFifoQueue<>(50);
    private CircularFifoQueue<MinuteInfo> response400s = new CircularFifoQueue<>(50);
    private CircularFifoQueue<MinuteInfo> response404s = new CircularFifoQueue<>(50);
    private CircularFifoQueue<MinuteInfo> responseCounts = new CircularFifoQueue<>(50);
    private ScheduledThreadPoolExecutor timedChecker;
    private RollingMetrics metrics = new RollingMetrics();
    private DailyMetrics dailyMetrics = new DailyMetrics();
    private static HealthTracker _instance = new HealthTracker();

    private HealthTracker() {
    }

    public static void start() {
        BasicThreadFactory factory = new BasicThreadFactory.Builder()
                .namingPattern("stallion-health-tracker-thread-%d").build();
        instance().timedChecker = new ScheduledThreadPoolExecutor(2, factory);
        instance().timedChecker.scheduleAtFixedRate(instance().metrics, 0, 1, TimeUnit.MINUTES);
        instance().timedChecker.scheduleAtFixedRate(instance().dailyMetrics, 0, 24 * 60, TimeUnit.MINUTES);
    }

    public static HealthTracker instance() {
        if (_instance == null) {
            _instance = new HealthTracker();
        }
        return _instance;
    }

    public static void shutdown() {
        if (_instance != null) {
            if (_instance.timedChecker != null) {
                _instance.timedChecker.shutdown();
            }
            _instance = null;
        }
    }

    public Double getAverageSystemCpuLoad() {
        int periods = 0;
        Double total = 0.0;
        for (Double usage : metrics.getSystemCpuUsage()) {
            total += usage;
            periods++;
        }
        return total / periods;
    }

    public Double getAverageAppCpuLoad() {
        int periods = 0;
        Double total = 0.0;
        for (Double usage : metrics.getAppCpuUsage()) {
            total += usage;
            periods++;
        }
        return total / periods;
    }

    public Double getSwapPages() {
        int periods = 0;
        Double total = 0.0;
        for (Long pages : metrics.getSwapRate()) {
            total += pages;
            periods++;
        }
        return total / periods;
    }

    public ZonedDateTime getSslExpires() {
        return dailyMetrics.getSslExpires();
    }

    public boolean getSslExpiresIn7() {
        return dailyMetrics.isSslExpiresWithin7();
    }

    public boolean getSslExpiresIn21() {
        return dailyMetrics.isSslExpiresWithin21();
    }

    public HttpHealthInfo getHttpHealthInfo() {
        HttpHealthInfo health = new HttpHealthInfo();
        health.setError400s(lastTenMinutesCount(response400s));
        health.setError500s(lastTenMinutesCount(response500s));
        health.setError404s(lastTenMinutesCount(response404s));
        health.setRequestCount(lastTenMinutesCount(responseCounts));
        return health;
    }

    public void logException(Throwable e) {
        if (e instanceof ClientException) {
            return;
        }
        if (e instanceof InvocationTargetException) {
            if (((InvocationTargetException) e).getTargetException() instanceof ClientException) {
                return;
            }
        }
        ExceptionInfo info = ExceptionInfo.newForException(e);
        exceptionQueue.add(info);
        if (SimpleAsyncRunner.instance() != null && Settings.instance().getEmailErrors() == true) {
            SimpleAsyncRunner.instance().submit(new ExceptionEmailRunnable(info));
        }
    }

    public int lastTenMinutesCount(CircularFifoQueue<MinuteInfo> queue) {
        ZonedDateTime tenAgo = MinuteInfo.getCurrentMinute().minusMinutes(10);
        int count = 0;
        for (MinuteInfo info : queue) {
            if (info.getMinute().isBefore(tenAgo)) {
                continue;
            }
            count += info.getCount().get();
        }
        return count;
    }

    public void logResponse(StRequest request, StResponse response) {
        incrementQueue(responseCounts);
        if (response.getStatus() >= 500) {
            // If the health endpoint is treating us as down, don't log that
            // as a 500 error or else we will be down for ever
            if (!request.getPath().startsWith("/st-internal/")) {
                incrementQueue(response500s);
            }
        } else if (response.getStatus() == 404) {
            incrementQueue(response404s);
        } else if (response.getStatus() >= 400) {
            incrementQueue(response400s);
        }
    }

    public void incrementQueue(CircularFifoQueue<MinuteInfo> queue) {
        ZonedDateTime now = MinuteInfo.getCurrentMinute();
        MinuteInfo minuteInfo = null;
        if (!queue.isEmpty()) {
            minuteInfo = queue.get(queue.size() - 1);
            //minuteInfo = queue.get(0);
            //Log.info("first: {0}", queue.get(0).getMinute());
            //Log.info("last:  {0}", queue.get(queue.size() -1).getMinute());
            //Log.info("now:   {0}", now);
            if (!minuteInfo.getMinute().equals(now)) {
                //Log.info("Minutes do not matched, prepare for new minute");
                minuteInfo = null;
            }
        }
        if (minuteInfo == null) {
            minuteInfo = new MinuteInfo();
            minuteInfo.setMinute(now);
            queue.add(minuteInfo);
        }
        //Log.info("Increment minute {0} {1}", minuteInfo.getMinute().toString(), minuteInfo.getCount().get());
        minuteInfo.getCount().incrementAndGet();
    }

    public CircularFifoQueue<ExceptionInfo> getExceptionQueue() {
        return exceptionQueue;
    }

    public static class MinuteInfo {
        private ZonedDateTime minute;
        private AtomicInteger count = new AtomicInteger(0);
        private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd-HH:mm");

        public static ZonedDateTime getCurrentMinute() {
            ZonedDateTime now = DateUtils.utcNow();
            return ZonedDateTime.of(now.getYear(), now.getMonth().getValue(), now.getDayOfMonth(), now.getHour(),
                    now.getMinute(), 0, 0, ZoneId.of("UTC"));
        }

        public AtomicInteger getCount() {
            return count;
        }

        public void setCount(AtomicInteger count) {
            this.count = count;
        }

        public ZonedDateTime getMinute() {
            return minute;
        }

        public void setMinute(ZonedDateTime minute) {
            this.minute = minute;
        }
    }

    public static class DailyMetrics implements Runnable {
        private double ntpOffset = 0;
        private ZonedDateTime sslExpires = null;
        private boolean sslExpiresWithin21 = false;
        private boolean sslExpiresWithin7 = false;

        public void run() {
            try {
                if (Settings.instance().getSiteUrl().startsWith("https")) {
                    checkSslExpiration();
                }
            } catch (Exception e) {
                Log.exception(e, "Error checking SSL");
            }

        }

        public void checkSslExpiration() throws Exception {
            // configure the SSLContext with a TrustManager
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(new KeyManager[0], new TrustManager[] { new DefaultTrustManager() }, new SecureRandom());
            SSLContext.setDefault(ctx);
            URL url = new URL(Settings.instance().getSiteUrl());
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }
            });
            System.out.println(conn.getResponseCode());
            Certificate[] certs = conn.getServerCertificates();
            Date maxDate = new Date(Long.MAX_VALUE);
            for (Certificate cert : certs) {
                X509Certificate xCert = (X509Certificate) cert;
                if (xCert.getNotAfter().before(maxDate)) {
                    maxDate = xCert.getNotAfter();
                }
            }

            setSslExpires(ZonedDateTime.ofInstant(maxDate.toInstant(), GeneralUtils.UTC));
            setSslExpiresWithin21(DateUtils.utcNow().plusDays(21).isAfter(getSslExpires()));
            setSslExpiresWithin7(DateUtils.utcNow().plusDays(7).isAfter(getSslExpires()));
            conn.disconnect();
        }

        public double getNtpOffset() {
            return ntpOffset;
        }

        public void setNtpOffset(double ntpOffset) {
            this.ntpOffset = ntpOffset;
        }

        public ZonedDateTime getSslExpires() {
            return sslExpires;
        }

        public void setSslExpires(ZonedDateTime sslExpires) {
            this.sslExpires = sslExpires;
        }

        public boolean isSslExpiresWithin21() {
            return sslExpiresWithin21;
        }

        public DailyMetrics setSslExpiresWithin21(boolean sslExpiresWithin21) {
            this.sslExpiresWithin21 = sslExpiresWithin21;
            return this;
        }

        public boolean isSslExpiresWithin7() {
            return sslExpiresWithin7;
        }

        public DailyMetrics setSslExpiresWithin7(boolean sslExpiresWithin7) {
            this.sslExpiresWithin7 = sslExpiresWithin7;
            return this;
        }
    }

    private static class DefaultTrustManager implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }

    public static class RollingMetrics implements Runnable {
        private CircularFifoQueue<Double> systemCpuUsage = new CircularFifoQueue<>(5);
        private CircularFifoQueue<Double> appCpuUsage = new CircularFifoQueue<>(5);
        private CircularFifoQueue<Long> swapRate = new CircularFifoQueue<>(5);
        private boolean vmstatAvailable = true;

        public void run() {
            OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
            if (os instanceof UnixOperatingSystemMXBean) {
                UnixOperatingSystemMXBean unixBean = (UnixOperatingSystemMXBean) os;
                getAppCpuUsage().add(unixBean.getProcessCpuLoad());
                getSystemCpuUsage().add(unixBean.getSystemLoadAverage());
            }
            if (Settings.instance().getEnv().equals("local") || Settings.instance().getDevMode()) {
                vmstatAvailable = false;
            }
            if (vmstatAvailable) {
                Runtime rt = Runtime.getRuntime();
                int exitVal = 1;
                Process proc = null;
                try {
                    proc = rt.exec("vmstat");
                    exitVal = proc.exitValue();
                } catch (IOException e) {
                }
                if (exitVal != 0) {
                    // Don't check for vmstat again
                    Log.warn("vmstat executable not found, skipping monitoring of swap rate");
                    vmstatAvailable = false;
                } else {
                    ProcessHelper.CommandResult result = ProcessHelper.run("vmstat", "4", "2");
                    if (result.succeeded()) {
                        String[] lines = result.getOut().split("\n");
                        String lastLine = lines[lines.length - 1];
                        if (empty(lastLine.trim())) {
                            lastLine = lines[lines.length - 2];
                        }
                        String[] stats = lastLine.trim().trim().replaceAll("\\s+", "\t").split("\t");
                        Long swapIn = Long.parseLong(stats[6]);
                        Long swapOut = Long.parseLong(stats[7]);
                        getSwapRate().add(swapIn + swapOut);
                    }
                }
            }

        }

        public CircularFifoQueue<Double> getSystemCpuUsage() {
            return systemCpuUsage;
        }

        public void setSystemCpuUsage(CircularFifoQueue<Double> systemCpuUsage) {
            this.systemCpuUsage = systemCpuUsage;
        }

        public CircularFifoQueue<Double> getAppCpuUsage() {
            return appCpuUsage;
        }

        public void setAppCpuUsage(CircularFifoQueue<Double> appCpuUsage) {
            this.appCpuUsage = appCpuUsage;
        }

        public CircularFifoQueue<Long> getSwapRate() {
            return swapRate;
        }

        public void setSwapRate(CircularFifoQueue<Long> swapRate) {
            this.swapRate = swapRate;
        }
    }
}