Watchdog

Project structure:
 
watchdog/
  logs/
  src/
    main/java/watchdog
      App.java
      Tools.java
    test/
  target/
  .evn
  .gitignore
  pom.xml

Dependencies

pom.xml
 
<dependencies>
  <dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
  </dependency>
  <dependency>
    <groupId>io.github.cdimascio</groupId>
    <artifactId>java-dotenv</artifactId>
    <version>5.2.2</version>
  </dependency>
</dependencies>

Environment

.evn
 
ENV=TEST

BVB_LOG=someapp/logs/wrapper.log
BVB_START=09:00
BVB_END=18:50

FROM=admin@example.com
MAIL_TO=admin@example.com
MAIL_CC=admin@example.com
ASSIGN_TO=admin@example.com
BACKUP_TO=adminBB@example.com

# Log config (1 MB, append on existing file on restart)
LOG_PATH=logs/watchdog.log
LOG_MAX_SIZE=1048576
LOG_ROTATE_COUNT=2
LOG_APPEND=true

Application

App.java
 
/**
 * WATCHDOG - MICROSERVICE
 * 
 * Purpose:
 *   Monitors a single log/data file and alerts on stale.
 *
 * Configuration:
 *   All values are taken from environment variables (OS or .env file).
 *   Precedence: OS env > .env > defaults
 * 
 * WATCH - "10/60 check algorithm"
 *  - Runs every 10 seconds.
 *  - Check if wrapper.log (configured in properties) is older than 120 seconds.
 *  - If stale or empty, sends an alert and stops.
 * 
 * Behavior:
 *   - Outside working hours and weekends: sleeps 16 hours.
 *   - Test server: Tools.sendAlert handles suppresion.
 *   - All sleeps are in seconds.
 */

package watchdog;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

import io.github.cdimascio.dotenv.Dotenv;

public class App {

    // Env variables loader (.env + OS env)
    private static Dotenv dotenv;

    // Time constants (in seconds)
    private final static int HOURS_16 = 60*60*16;
    private final static int HOURS_12 = 60*60*12;

    // Used for logging current weekday/time
    private static String weekDay;
    private static String currTime;

     public static void main(String[] args) throws InterruptedException {
        
        // Load .env
        dotenv = Dotenv.configure().load();

        // Intialize dual logging (console + file)
        Tools.initLogger(dotenv);

        // Parse working hours
        int START_TIME = Tools.time(dotenv.get("WATCH_START"));
        int END_TIME = Tools.time(dotenv.get("WATCH_END"));

        // File to monitor
        File FILE_WATCHED = new File(dotenv.get("WATCH_LOG"));
        Date fileDate; String fileTime; long diff;

        Tools.print("Watchdog starting | ENV=" + dotenv.get("ENV") +
        " | LOG_PATH=" + dotenv.get("LOG_PATH") +
        " | LIMIT=" + dotenv.get("LOG_MAX_SIZE") +
        " | ROTATE=" + dotenv.get("LOG_ROTATE_COUNT"), Tools.ANSI_CYAN);

        while(true) {
            // Tick interval
            Tools.sleep(10); 

            // Weekend skip
            weekDay = new SimpleDateFormat("EEEE").format(new Date());
            if (weekDay.equals("Saturday") || weekDay.equals("Sunday")) {
                Tools.print("WATCH / Watch scheduled not to run on weekends / " + currTime);
                Tools.sleep(HOURS_16);
                continue;
            }

            // Enforce working window
            if (Tools.now("HHmm") < START_TIME) {
                Tools.print("WATCH / watch will start at ... " + START_TIME);
                continue; 
            } else
            if (Tools.now("HHmm") >= END_TIME) {
                Tools.print("WATCH / watch is going to sleep ... 12h");
                Tools.sleep(HOURS_12);
                continue;
            }

            // Compute file age
            fileDate = new Date(FILE_WATCHED.lastModified());
            fileTime = new SimpleDateFormat("HH:mm:ss").format(fileDate);
            diff = (new Date().getTime() - FILE_WATCHED.lastModified()) / 1000;  // seconds

            Tools.print("Watch is running ... ", Tools.ANSI_YELLOW);
            
            // Stale -> alert and stop
            if ( diff > 120) {
                Tools.print("WATCH is offline /  " + fileTime + " / " +  diff + "s diff", Tools.ANSI_PURPLE);
                Tools.sendAlert(dotenv);
                break;
            }

            // OK -> keep running
            Tools.print("WATCH OK / " + fileTime + " / 120s check, " + diff + "s diff", Tools.ANSI_GREEN);
        }
    }
}
Tools.java
 
package watchdog;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import io.github.cdimascio.dotenv.Dotenv;

class Tools {
    // Env variables loader (.env + OS env)
    private static Dotenv dotenv;

    private static Logger logger;
    private static boolean loggerInitialized = false;
    private static Level logLevel = Level.INFO;
    
    public static final String ANSI_RESET = "\033[0m";
    public static final String ANSI_BLACK = "\033[30m";
    public static final String ANSI_RED = "\033[31m";
    public static final String ANSI_GREEN = "\033[32m";
    public static final String ANSI_YELLOW = "\033[33m";
    public static final String ANSI_BLUE = "\033[34m";
    public static final String ANSI_PURPLE = "\033[35m";
    public static final String ANSI_CYAN = "\033[36m";
    public static final String ANSI_WHITE = "\033[37m";

    public static synchronized void initLogger(Dotenv env) {
        if (loggerInitialized) return;
        dotenv = env;

        logger = Logger.getLogger("WatchdogLogger");
        logger.setUseParentHandlers(false);  // avoid default root console duplication

        String logPath = dotenv.get("LOG_PATH");
        int maxBytes = Integer.parseInt(dotenv.get("LOG_MAX_SIZE"));
        int count = Integer.parseInt(dotenv.get("LOG_ROTATE_COUNT"));
        boolean append = Boolean.parseBoolean(dotenv.get("LOG_APPEND"));

        try {
            FileHandler fh = new FileHandler(logPath, maxBytes, count, append);
            fh.setLevel(logLevel);
            //fh.setFormatter(new SimpleFormatter());
            fh.setFormatter(new Formatter() {
                @Override
                public String format(LogRecord record) {
                    return String.format( 
                        "%1$tF %1$tT [%2$s] %3$s%n",
                        new Date(record.getMillis()),      // timestamp
                        record.getLevel().getName(),       // INFO, WARNING, SEVERE
                        record.getMessage()                // your message
                    );
                }
            });
            logger.addHandler(fh);
        } catch (IOException e) {
            System.out.println("Failed to setup file logging: " + e.getMessage());
        }

        loggerInitialized = true;
        logger.info("Logging initialized: level=" + logLevel + ", path=" + logPath);
    }
    
    public static void print(String message) {
        print(message, ANSI_WHITE);
    }
    
    public static void print(String message, String color) {
        System.out.println(color + message + ANSI_RESET);
        logger.log(Level.INFO, message);
    }
    
    public static int time(String start) {
        return Integer.parseInt(start.replace(":", ""));
    }
    
    public static int now(String frm) { // HHmm
        return Integer.parseInt(new SimpleDateFormat(frm).format(new Date()));
    }
    
    public static File getLastFile(String dir) {

        File[] files = new File(dir).listFiles();
        Arrays.sort(files,
                Comparator.comparingLong(File::lastModified).reversed()
        );
        File lastFile = files[0];
        
        if (lastFile.isDirectory()) {
            lastFile = getLastFile(lastFile.toString());
        }
        return lastFile;
    }

    public static void sendAlert(Dotenv env) {
        dotenv = env;

        // Configure the SMTP server properties
        Properties propsMail = new Properties();
        propsMail.put("mail.smtp.host", "smtp.brd.ro");
        
        // Create a session
        Session session = Session.getDefaultInstance(propsMail, null);
        String msgText = "" +
                "Please check the server: \n " + 
                "MY_SERVER / WATCH \n\n" +
                "Assigned to: " + dotenv.get("ASSIGN_TO") + "\n" +
                "Backup: " + dotenv.get("BACKUP_TO") +
        "";

        // Set addresses
        final String from = dotenv.get("FROM");
        final String to = dotenv.get("MAIL_TO");
        final String cc = dotenv.get("MAIL_CC");
        final String classification = "C1";
        final String subject = "[" + classification + "] Watch 02P Alert - WATCH";

        try{
            // Create a MimeMessage
            Message msg = new MimeMessage(session);
            msg.setFrom(new InternetAddress(from));
            msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
            msg.setRecipient(Message.RecipientType.CC, new InternetAddress(cc));
            msg.setSubject(subject);
            msg.setText(msgText);
            msg.setHeader("X-Classification", classification);
            msg.setHeader("Sensitivity", "BRD-Restricted");

            // Send the email
            Transport.send(msg);
            System.out.println("Watch 02 Alert - sent");

        } catch (MessagingException e) {
            e.printStackTrace();
            System.err.println("Failed to send email.");
        }
    }
    
    public static void sleep(int seconds) {
        try { TimeUnit.SECONDS.sleep(seconds); } catch(InterruptedException e) {}
    }
}

Deployment

 
mvn clean
mvn compile
mvn package

mvn clean package

Serive Wrapper (Python)

 
""" WATCHDOG SERVICE - WRAPPER

python .\service\watchdog_service.py install
python .\service\watchdog_service.py start

python .\service\watchdog_service.py stop
python .\service\watchdog_service.py remove
"""

import win32serviceutil
import win32service
import win32event
import servicemanager
import subprocess
import sys
import os

JAVA_EXE   = r"C:\Program Files\Java\jdk-1.8_462\bin\java.exe"
JAR_PATH   = r"D:\bvb\watchdog\watchdog-1.3.jar"
ENV_PATH   = r"D:\bvb\watchdog\.env"
WORK_DIR   = r"D:\bvb\watchdog"

class WatchdogService(win32serviceutil.ServiceFramework):
    _svc_name_ = "WatchdogService"
    _svc_display_name_ = "Watchdog v1.3"
    _svc_description_ = "Monitors log file and alerts when stale"

    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
        self.process = None

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        if self.child:
            try:
                self.child.terminate()
                self.child.wait(timeout=10)  # wait for JVM to exit
            except Exception:
                pass
        win32event.SetEvent(self.hWaitStop)
        if self.process:
            self.process.terminate()

    def SvcDoRun(self):
        servicemanager.LogInfoMsg("Starting Watchdog Service...")

        # Ensure working dir and environment
        os.chdir(WORK_DIR)
        cmd = [JAVA_EXE, "-jar", JAR_PATH, f"-Ddotenv.file={ENV_PATH}"]

        # Start Watchdog app
        self.child = subprocess.Popen(cmd, cwd=WORK_DIR)
        win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)

if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(WatchdogService)