7
\$\begingroup\$

I have written a simple application to monitor whether a couple of Java EE 6 web applications deployed to Glassfish 3.1.2.2 are accessible. My intent is to demonstrate to our operations team that the applications are intermittently inaccessible.

package monitoring;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class Monitoring {

    private static final Logger logger = Logger.getLogger("MonitoringLogger");

    public static void main(String[] args) throws MalformedURLException, IOException, InterruptedException {
        configureLogger();
        URL[] urls = {
            new URL("http://alpha:11480/AlphaApplication-war/"), 
            new URL("http://beta:11680/BetaApplication-war/")
        };
        while (true) {
            for (URL url : urls) {
                URLConnection connection = url.openConnection();
                connection.setReadTimeout(5000);
                try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
                    while (reader.ready()) {
                        logger.info(reader.readLine());
                    }
                } catch (SocketTimeoutException exception) {
                    logger.log(Level.SEVERE, "Read timed out to {0}", url);
                }
            }
            Thread.sleep(TimeUnit.MINUTES.toMillis(5));
        }
    }

    private static void configureLogger() throws IOException {
        FileHandler fh;
        fh = new FileHandler("logfile.log");
        logger.addHandler(fh);
        SimpleFormatter formatter = new SimpleFormatter();
        fh.setFormatter(formatter);
        logger.info("Logger initialized");
    }
}

When the connection is successful, the reply is logged. When the connection times-out, the timeout is logged.

A Windows batch file runs this application on start-up:

java -jar "C:\Users\Me\Monitoring\dist\Monitoring.jar"
pause

I'm satisfied with the behaviour of this application, but I'm using unfamiliar APIs to accomplish an unfamiliar task, so I'm looking for broad advice.

\$\endgroup\$
5
\$\begingroup\$

The concept in your code is fundamentally good, but there are a few things I would recommend you change.

Static initializers

First up, I like static initializer functions when the static component is non-trivial. So, for example, this code:

private static final Logger logger = Logger.getLogger("MonitoringLogger");

public static void main(String[] args) throws MalformedURLException, IOException, InterruptedException {
    configureLogger();
    ......
}

private static void configureLogger() throws IOException {
    FileHandler fh;
    fh = new FileHandler("logfile.log");
    logger.addHandler(fh);
    SimpleFormatter formatter = new SimpleFormatter();
    fh.setFormatter(formatter);
    logger.info("Logger initialized");
}

I would replace with:

private static final Logger logger = configureLogger();

private static Logger configureLogger() {

    try {
        Logger logger = Logger.getLogger("MonitoringLogger");
        FileHandler fh;
        fh = new FileHandler("logfile.log");
        logger.addHandler(fh);
        SimpleFormatter formatter = new SimpleFormatter();
        fh.setFormatter(formatter);
        logger.info("Logger initialized");
        return logger;
    } catch (IOException ioe) {
        throw new IllegalStateException("Could not get logger up.", ioe);
    }
}

That's a 'trivial' change, but it puts the initialization sequence of the code in to a much more structured system.

What to log.

You don't really want to be logging what the sites are showing, you want to be logging their response times. I suggest pulling different metrics than the ones you are getting..... How long it took for the repsonse to come, and whether it was successful, or not. A log that looks like:

2015-07-22 12:23:34 [INFO] 3ms response from http://alpha:11480/AlphaApplication-war/
2015-07-22 12:26:12 [INFO] 3984ms response from http://alpha:11480/AlphaApplication-war/
2015-07-22 12:29:49 [ERROR] 5000ms TIME OUT FROM http://alpha:11480/AlphaApplication-war/

That would be much easier to process.

To do that, you will want to measure the actual times, not the responses... and you will pick up slow-to-respond servers too (perhaps a big garbage collection, over-congested network handler, or something).

So, create a function that has a response, which should be an Object:

private static final class ServerStat {
    private final boolean success;
    private final long milliseconds;

    ServerStat(boolean success, long milliseconds) {
        .....
    }
}

and then a function that tests an HTTP URL:

public static ServerStat pingServer(URL host) {
    long start = System.currentTimeMillis();

    boolean ok = false;
    try {

        URLConnection connection = url.openConnection();
        try (InputStream stream = connection.getInputStream()) {

            byte[] buffer = new byte[4096];
            while (stream.read(buffer) >= 0) {
                // throw the data away
            }
        }
        // no exceptions
        ok = true;
    } catch (Exception e) {
        // swallow any exception
    }
    return new ServerStat(ok, System.currentTimeMillis() - start);
}

Now, you have a function which times how long it takes to get a complete response (or fail) from a server.

Now, instead of having an infinite while-loop, have a non-daemon scheduled thread executor instead. Something like:

    ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
    ses.scheduleAtFixedRate(() -> pingServerList(totest), 0, 5, TimeUnit.SECONDS); 

That runs a function every 5 seconds.

I put it all together like this:

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


public class PingServer {

    private static final class ServerStat {
        private final boolean success;
        private final long milliseconds;
        private final String message;

        ServerStat(boolean success, long milliseconds, String message) {
            this.success = success;
            this.milliseconds = milliseconds;
            this.message = message;
        }
    }

    public static ServerStat pingServer(URL url) {
        long start = System.currentTimeMillis();

        boolean ok = false;
        String message = "OK";
        try {

            URLConnection connection = url.openConnection();
            connection.setReadTimeout(3000);
            try (InputStream stream = connection.getInputStream()) {

                byte[] buffer = new byte[4096];
                while (stream.read(buffer) >= 0) {
                    // throw the data away
                }
            }
            // no exceptions
            ok = true;
        } catch (Exception e) {
            message = e.getClass().getName() + ": " + e.getMessage();
        }
        return new ServerStat(ok, System.currentTimeMillis() - start, message);
    }

    public static void main(String[] args) throws IOException {
        List<URL> totest = Arrays.asList(new URL("http://localhost:3000"));
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);
        ses.scheduleAtFixedRate(() -> pingServerList(totest), 0, 5, TimeUnit.SECONDS); 

    }

    private static void pingServerList(List<URL> totest) {
        for(URL url : totest) {
            ServerStat ss = pingServer(url);
            System.out.printf("%tc [%s] %dms %s %s\n", System.currentTimeMillis(),
                    ss.success ? "INFO" : "ERROR", 
                    ss.milliseconds, url.toString(), ss.message);
        }
    }

}

and when I run it on my machine, with a test server running (then not running), I get the output:

Wed Jul 22 16:20:51 EDT 2015 [ERROR] 22ms http://localhost:3000 java.net.ConnectException: Connection refused
Wed Jul 22 16:20:56 EDT 2015 [ERROR] 1ms http://localhost:3000 java.net.ConnectException: Connection refused
Wed Jul 22 16:21:01 EDT 2015 [ERROR] 2ms http://localhost:3000 java.net.ConnectException: Connection refused
Wed Jul 22 16:21:06 EDT 2015 [INFO] 21ms http://localhost:3000 OK
Wed Jul 22 16:21:11 EDT 2015 [INFO] 8ms http://localhost:3000 OK
Wed Jul 22 16:21:16 EDT 2015 [INFO] 5ms http://localhost:3000 OK
Wed Jul 22 16:21:21 EDT 2015 [INFO] 4ms http://localhost:3000 OK
Wed Jul 22 16:21:26 EDT 2015 [ERROR] 4ms http://localhost:3000 java.net.ConnectException: Connection refused
\$\endgroup\$
  • \$\begingroup\$ This is wonderful, thank you. This is exactly what I was hoping for: a mixture of API, code structure, and problem solving advice. \$\endgroup\$ – DavidS Jul 22 '15 at 23:25
2
\$\begingroup\$

Adding onto @rolfl's excellent answer, may I suggest that you should/can also take in the list of URLs from args?

List<URL> urls = Stream.of(args).map(URL::new).collect(Collectors.toList());

This will mean that adding or changing URLs doesn't require you to rebuild your Java jar file every time. :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.