tutorial

How to Monitor Your Apache FOP PDF Generation Service (Free, Multi-Region)

Apache FOP converts XSL-FO to PDF, PostScript, and other formats — but PDF generation pipelines fail silently. Learn how to add external uptime monitoring, heartbeat checks for batch rendering jobs, and instant alerts — free.

How to Monitor Your Apache FOP PDF Generation Service (Free, Multi-Region)

Apache FOP (Formatting Objects Processor) is the industry-standard Java library for converting XSL-FO documents to PDF, PostScript, AFP, and other print-ready formats. It powers invoices, reports, contracts, and regulatory documents in enterprise Java applications. When the FOP pipeline breaks — a missing font, a corrupted XSL-FO template, or an out-of-memory error during a large document render — your PDF exports silently disappear or return corrupted files.

By the end of this guide you'll have external uptime monitoring, multi-region probes, heartbeat monitoring for batch PDF generation jobs, and instant alerts — all on the free tier.


Why FOP pipelines fail silently

PDF generation services have failure modes that are easy to miss:

Silent FOPException — a malformed XSL-FO document or a missing font metric file throws a FOPException at render time. If your application catches and swallows this, the endpoint returns 200 with an empty or partial PDF — no log entry, no alert.

Font subsystem failure — FOP relies on font configuration files (fop.xconf). A corrupt metrics file or a missing font directory reference causes rendering to fail for only some documents (those using that font), while others succeed. Intermittent failures are hard to detect without active monitoring.

Batch generation stalls — nightly invoice generation, monthly regulatory reports, and contract PDF exports run as background jobs. If they crash partway through, the incomplete batch output may sit in the output directory looking like a normal partial run — and nobody notices until a customer calls.

Memory exhaustion — rendering large, complex FO documents with embedded images is memory-intensive. Under production load, OutOfMemoryError can silently kill individual rendering threads.


Step 1: Expose a health endpoint that exercises the FOP pipeline

A health check that bypasses FOP proves nothing. Render a minimal FO document to validate the full pipeline.

With Spring Boot:

import org.apache.fop.apps.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.Map;

@RestController
public class HealthController {

    private static final String MINIMAL_FO =
        "<?xml version='1.0'?>" +
        "<fo:root xmlns:fo='http://www.w3.org/1999/XSL/Format'>" +
        "  <fo:layout-master-set>" +
        "    <fo:simple-page-master master-name='A4'>" +
        "      <fo:region-body/>" +
        "    </fo:simple-page-master>" +
        "  </fo:layout-master-set>" +
        "  <fo:page-sequence master-reference='A4'>" +
        "    <fo:flow flow-name='xsl-region-body'>" +
        "      <fo:block>Health Check</fo:block>" +
        "    </fo:flow>" +
        "  </fo:page-sequence>" +
        "</fo:root>";

    @GetMapping("/health")
    public Map<String, Object> health() throws Exception {
        FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
        FOUserAgent userAgent = fopFactory.newFOUserAgent();

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent, bos);

        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        Source src = new StreamSource(new StringReader(MINIMAL_FO));
        Result res = new SAXResult(fop.getDefaultHandler());
        transformer.transform(src, res);

        return Map.of(
            "status", "UP",
            "engine", "ok",
            "pdfBytes", bos.size()
        );
    }
}

With a plain servlet:

import org.apache.fop.apps.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;

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

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        try {
            FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF,
                fopFactory.newFOUserAgent(), bos);
            Transformer t = TransformerFactory.newInstance().newTransformer();
            t.transform(
                new StreamSource(new StringReader(MINIMAL_FO)), // MINIMAL_FO constant from above
                new SAXResult(fop.getDefaultHandler())
            );
            res.getWriter().write("{\"status\":\"UP\",\"pdfBytes\":" + bos.size() + "}");
        } catch (Exception e) {
            res.setStatus(500);
            res.getWriter().write("{\"status\":\"DOWN\",\"error\":\"" + e.getMessage() + "\"}");
        }
    }
}

The minimal FO document is tiny and fast to render — it validates the FOP classpath, the font configuration, the JAXP transformer chain, and the PDF output stream, without burning CPU on a real document.


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 calling.

Add monitors for each critical endpoint:

| Endpoint | What it catches | |---|---| | /health | FOP engine failures, font config issues | | /generate/invoice | Invoice PDF generation endpoint | | /generate/report | Report rendering endpoint |


Step 3: Heartbeat monitoring for batch PDF generation jobs

Invoice generation, regulatory reporting, and contract rendering typically run as nightly or monthly scheduled jobs — no HTTP, no external observer. Heartbeat monitoring is essential here.

import org.apache.fop.apps.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

public class MonthlyInvoiceJob {

    private final FopFactory fopFactory;
    private final String heartbeatUrl =
        System.getenv("HEARTBEAT_INVOICE_JOB_URL");

    public MonthlyInvoiceJob(FopFactory fopFactory) {
        this.fopFactory = fopFactory;
    }

    public void run() throws Exception {
        List<Invoice> invoices = getPendingInvoices();
        if (invoices.isEmpty()) {
            throw new IllegalStateException("No invoices found for this billing period");
        }

        for (Invoice invoice : invoices) {
            String fo = renderFoTemplate(invoice);
            byte[] pdf = renderPdf(fo);
            saveToStorage(invoice.getId(), pdf);
            notifyCustomer(invoice.getCustomerEmail(), pdf);
        }

        // Only ping after all invoices rendered and sent
        if (heartbeatUrl != null && !heartbeatUrl.isEmpty()) {
            pingHeartbeat(heartbeatUrl);
        }
    }

    private byte[] renderPdf(String fo) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF,
            fopFactory.newFOUserAgent(), bos);
        Transformer t = TransformerFactory.newInstance().newTransformer();
        t.transform(
            new StreamSource(new StringReader(fo)),
            new SAXResult(fop.getDefaultHandler())
        );
        return bos.toByteArray();
    }

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

    // ... getPendingInvoices(), renderFoTemplate(), saveToStorage(),
    //     notifyCustomer() omitted
}

In Vigilmon:

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

If FOPException is thrown, a PDF is corrupt and the email bounces, or the job runs out of memory halfway through, the heartbeat is never pinged, and you get an alert after one missed interval.


Step 4: Validate FOP configuration at startup

Font configuration errors and missing XSL-FO template files fail at render time, not startup. Force validation early:

import org.apache.fop.apps.*;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;

@Component
public class FopStartupValidator {

    private final FopFactory fopFactory;

    public FopStartupValidator(FopFactory fopFactory) {
        this.fopFactory = fopFactory;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void validate() throws Exception {
        // Verify the FOP engine and font subsystem work end-to-end
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF,
            fopFactory.newFOUserAgent(), bos);
        Transformer t = TransformerFactory.newInstance().newTransformer();
        t.transform(
            new StreamSource(new StringReader(MINIMAL_FO)),
            new SAXResult(fop.getDefaultHandler())
        );
        if (bos.size() == 0) {
            throw new IllegalStateException("FOP produced empty PDF — font or config issue");
        }
    }

    private static final String MINIMAL_FO =
        "<?xml version='1.0'?>" +
        "<fo:root xmlns:fo='http://www.w3.org/1999/XSL/Format'>" +
        "  <fo:layout-master-set>" +
        "    <fo:simple-page-master master-name='A4'>" +
        "      <fo:region-body/>" +
        "    </fo:simple-page-master>" +
        "  </fo:layout-master-set>" +
        "  <fo:page-sequence master-reference='A4'>" +
        "    <fo:flow flow-name='xsl-region-body'>" +
        "      <fo:block>OK</fo:block>" +
        "    </fo:flow>" +
        "  </fo:page-sequence>" +
        "</fo:root>";
}

A broken font config or a misconfigured fop.xconf now causes a startup crash — caught by your /health check immediately after deploy rather than at midnight when the first invoice job runs.


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=fop-tutorial)

What you've built

| What | How | |---|---| | External health checks | /health with live FO render + Vigilmon HTTP monitor | | Pipeline validation | Minimal FO document round-trip in health endpoint | | Batch job monitoring | Heartbeat ping after each successful invoice/report run | | Startup validation | ApplicationReadyEvent validates FOP config and fonts | | 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 FOP pipeline failures, font configuration errors, and silently stalled batch PDF jobs before customers notice missing invoices or broken 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 →