tutorial

How to Monitor Your Apache Commons IO Applications (Free, Multi-Region)

Apache Commons IO handles file operations, stream copying, and directory watching across enterprise Java — but disk saturation, permission errors, and watchdog hangs fail silently. Learn how to add external uptime monitoring, heartbeat checks for file processing jobs, and instant alerts — free.

How to Monitor Your Apache Commons IO Applications (Free, Multi-Region)

Apache Commons IO is the go-to Java library for file manipulation, stream utilities, directory monitoring, and I/O operations that the standard JDK makes inconvenient. FileUtils.copyDirectory(), IOUtils.toString(), FilenameUtils.getExtension(), and DirectoryWatcher underpin file ingestion pipelines, document processing systems, and archive management tools across enterprise Java. When the underlying filesystem fills up, permissions change after a deployment, or a DirectoryWatcher thread silently dies, your file processing pipeline stops — but your application keeps returning 200.

By the end of this guide you'll have external uptime monitoring, multi-region health probes, heartbeat monitoring for file processing jobs using Commons IO, and instant alerts — all on the free tier.


Why Commons IO failures are hard to catch

File I/O failures have unique failure modes that don't propagate to HTTP responses:

Disk saturationFileUtils.writeByteArrayToFile() and FileUtils.copyFile() throw IOException when the target disk is full. If error handling only logs these, your document archive silently stops ingesting files without any HTTP 500 response.

Permission drift — a deployment that changes file ownership or a cron that resets directory permissions causes FileUtils.openOutputStream() to throw java.io.FileNotFoundException: Permission denied at runtime, hours or days after a clean deploy.

DirectoryWatcher thread deathFileAlterationMonitor runs a background thread. An uncaught exception in an FileAlterationListener implementation kills this thread permanently. The directory watcher appears to be running (no visible error, JVM still alive) but stops processing new files.

Stream resource leaksIOUtils.copy() called without proper try-with-resources leaves file handles open. Under sustained load this exhausts the OS file descriptor limit, causing all subsequent File.exists() and Files.open() calls to fail with Too many open files.


Step 1: Expose a health endpoint that exercises Commons IO

A health endpoint that skips the filesystem proves nothing. Test actual read/write operations:

With Spring Boot:

import org.apache.commons.io.*;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@RestController
public class HealthController {

    private final File workDir = FileUtils.getTempDirectory();

    @GetMapping("/health")
    public Map<String, Object> health() throws IOException {
        // Exercise FileUtils read/write round-trip
        File probe = new File(workDir, "health-probe-" + Thread.currentThread().getId() + ".txt");
        try {
            FileUtils.writeStringToFile(probe, "vigilmon-probe", StandardCharsets.UTF_8);
            String content = FileUtils.readFileToString(probe, StandardCharsets.UTF_8);
            if (!"vigilmon-probe".equals(content)) {
                throw new IllegalStateException("FileUtils round-trip content mismatch");
            }

            // Exercise IOUtils stream copy
            byte[] bytes = "stream-probe".getBytes(StandardCharsets.UTF_8);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            IOUtils.copy(new ByteArrayInputStream(bytes), bos);
            if (bos.size() != bytes.length) {
                throw new IllegalStateException("IOUtils.copy produced wrong byte count");
            }

            // Exercise FilenameUtils
            String ext = FilenameUtils.getExtension("report.pdf");
            if (!"pdf".equals(ext)) {
                throw new IllegalStateException("FilenameUtils.getExtension returned: " + ext);
            }

            // Check available disk space
            long freeBytes = FileSystemUtils.freeSpaceKb(workDir.getAbsolutePath()) * 1024L;
            if (freeBytes < 100 * 1024 * 1024L) { // warn if less than 100 MB
                throw new IllegalStateException("Low disk space: " + freeBytes / (1024 * 1024) + " MB free");
            }

            return Map.of(
                "status", "UP",
                "fileUtils", "ok",
                "ioUtils", "ok",
                "filenameUtils", "ok",
                "freeSpaceMb", freeBytes / (1024 * 1024)
            );
        } finally {
            FileUtils.deleteQuietly(probe);
        }
    }
}

With a plain servlet:

import org.apache.commons.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.nio.charset.StandardCharsets;

@WebServlet("/health")
public class HealthServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        File probe = new File(FileUtils.getTempDirectory(), "health-" + System.nanoTime() + ".txt");
        try {
            FileUtils.writeStringToFile(probe, "ok", StandardCharsets.UTF_8);
            String content = FileUtils.readFileToString(probe, StandardCharsets.UTF_8);
            if (!"ok".equals(content)) {
                throw new IllegalStateException("FileUtils round-trip failed");
            }
            long freeKb = FileSystemUtils.freeSpaceKb(FileUtils.getTempDirectoryPath());
            res.getWriter().write("{\"status\":\"UP\",\"freeKb\":" + freeKb + "}");
        } catch (Exception e) {
            res.setStatus(500);
            res.getWriter().write("{\"status\":\"DOWN\",\"error\":\"" + e.getMessage() + "\"}");
        } finally {
            FileUtils.deleteQuietly(probe);
        }
    }
}

The health endpoint validates filesystem write access, IOUtils stream copying, FilenameUtils parsing, and disk space — four independent failure modes, one check.


Step 2: Set up external monitoring with Vigilmon

With /health live, point Vigilmon at it:

  1. Sign up at vigilmon.online — free tier, no credit card
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval (5 minutes on free tier)
  5. Save

Vigilmon checks from multiple geographic regions. A non-2xx response or timeout opens an incident and sends you an alert before users start seeing missing files or failed exports.

Add monitors for each critical endpoint:

| Endpoint | What it catches | |---|---| | /health | Filesystem write access, disk space, IOUtils, FilenameUtils | | /api/upload | Inbound file ingestion endpoint | | /api/export | Document export using FileUtils |


Step 3: Heartbeat monitoring for file processing jobs

File ingestion pipelines, nightly archive compression, and document transformation jobs have no HTTP endpoint. Heartbeat monitoring catches silent failures:

import org.apache.commons.io.*;
import org.apache.commons.io.filefilter.AgeFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;

public class NightlyArchiveJob {

    private final File inboxDir = new File(System.getenv("INBOX_DIR"));
    private final File archiveDir = new File(System.getenv("ARCHIVE_DIR"));
    private final String heartbeatUrl = System.getenv("HEARTBEAT_ARCHIVE_JOB_URL");

    public void run() throws Exception {
        // Find files older than 24 hours
        Date cutoff = new Date(System.currentTimeMillis() - 24L * 60 * 60 * 1000);
        Collection<File> toArchive = FileUtils.listFiles(
            inboxDir,
            new AgeFileFilter(cutoff),
            TrueFileFilter.INSTANCE
        );

        if (toArchive.isEmpty()) {
            // Nothing to archive tonight — still ping to confirm job ran
            pingHeartbeat(heartbeatUrl);
            return;
        }

        int archived = 0;
        for (File file : toArchive) {
            String relativePath = FilenameUtils.getName(file.getAbsolutePath());
            File target = new File(archiveDir, relativePath);
            FileUtils.moveFile(file, target);
            archived++;
        }

        if (archived != toArchive.size()) {
            throw new IllegalStateException(
                "Archived " + archived + "/" + toArchive.size() + " files");
        }

        // Only ping after all files archived
        if (IOUtils.resourceToString("/version.txt", StandardCharsets.UTF_8) == null) {
            throw new IllegalStateException("Version resource missing — deployment may be incomplete");
        }

        pingHeartbeat(heartbeatUrl);
    }

    private void pingHeartbeat(String url) throws Exception {
        if (url == null || url.isBlank()) return;
        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5_000);
        conn.getResponseCode();
        conn.disconnect();
    }
}

In Vigilmon:

  1. Click New Monitor → Heartbeat
  2. Set the expected interval (e.g. 25 hours for a daily archive job)
  3. Copy the unique ping URL
  4. Set the environment variable: HEARTBEAT_ARCHIVE_JOB_URL=https://vigilmon.online/api/heartbeat/your-unique-token

If disk fills mid-archive, permissions change on the target directory, or a FileUtils.moveFile() fails, the heartbeat is never pinged and you get an alert after one missed interval.


Step 4: Monitor the DirectoryWatcher thread

FileAlterationMonitor runs a background thread that silently dies on uncaught exceptions. Add a watchdog:

import org.apache.commons.io.monitor.*;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.concurrent.atomic.AtomicLong;

@Component
public class InboxDirectoryWatcher {

    private FileAlterationMonitor monitor;
    private final AtomicLong lastAlive = new AtomicLong(System.currentTimeMillis());
    private final File inboxDir = new File(System.getenv("INBOX_DIR"));

    @EventListener(ApplicationReadyEvent.class)
    public void start() throws Exception {
        FileAlterationObserver observer = new FileAlterationObserver(inboxDir);
        observer.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileCreate(File file) {
                lastAlive.set(System.currentTimeMillis());
                processIncomingFile(file);
            }
        });

        monitor = new FileAlterationMonitor(5_000L, observer);
        monitor.start();
    }

    // Expose watcher liveness to the health endpoint
    public boolean isAlive() {
        // If no event in 10 minutes but files are present, thread may be dead
        long silenceDuration = System.currentTimeMillis() - lastAlive.get();
        return silenceDuration < 10 * 60 * 1000L || !inboxDir.exists();
    }

    private void processIncomingFile(File file) {
        // ... process the file
    }
}

Expose watcher liveness in /health by injecting InboxDirectoryWatcher and calling isAlive(). A dead watcher thread now returns 500 and pages you within 5 minutes.


Step 5: Alerts and badge embed

Slack/Discord alerts:

  1. In Vigilmon go to Notifications → New Channel
  2. Choose Slack or Discord, paste your webhook URL
  3. Enable it on your monitors

You get an instant alert when a monitor trips and a recovery notification when it's back.

Add an uptime badge to your README:

[![Uptime](https://vigilmon.online/badge/your-monitor-id.svg)](https://vigilmon.online?utm_source=devto&utm_medium=article&utm_campaign=commons-io-tutorial)

What you've built

| What | How | |---|---| | External health checks | /health with FileUtils round-trip, disk space, IOUtils, FilenameUtils | | Batch job monitoring | Heartbeat ping after each successful archive run | | Watcher thread liveness | FileAlterationMonitor health flag in /health | | Permission drift detection | Startup write test catches deployment permission changes | | Instant alerts | Slack/Discord notifications | | README status badge | Vigilmon badge embed |

The whole setup runs on the free tier in under 30 minutes. You'll catch filesystem permission drift, disk saturation, silently dead DirectoryWatcher threads, and stalled nightly archive jobs before users start seeing missing files or failed document exports.


Get started free at vigilmon.online — monitors running in under a minute, no credit card required.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →