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 saturation — FileUtils.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 death — FileAlterationMonitor 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 leaks — IOUtils.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:
- Sign up at vigilmon.online — free tier, no credit card
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval (5 minutes on free tier)
- 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:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. 25 hours for a daily archive job)
- Copy the unique ping URL
- 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:
- In Vigilmon go to Notifications → New Channel
- Choose Slack or Discord, paste your webhook URL
- 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:
[](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.