tutorial

How to Monitor Your Apache XMLGraphics Rendering Pipeline (Free, Multi-Region)

Apache XMLGraphics converts SVG, XSL-FO, and XML formats to PDF, PostScript, and raster images — but rendering pipelines break silently on font mismatches and memory exhaustion. Learn how to add external uptime monitoring, heartbeat checks for batch rendering jobs, and instant alerts — free.

How to Monitor Your Apache XMLGraphics Rendering Pipeline (Free, Multi-Region)

Apache XMLGraphics is the umbrella project that contains Apache Batik (SVG rendering) and the XMLGraphics Commons library (shared components for Apache FOP and Batik). It underpins SVG-to-PNG conversion pipelines, PDF report generators, vector graphics transcoders, and print-ready document export services in Java applications. When a rendering pipeline silently runs out of memory, a font is missing, or a transcoder returns a zero-byte image file, your users get broken thumbnails, blank PDF attachments, and corrupted chart exports — with no HTTP error, no log entry, and no alert.

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


Why XMLGraphics rendering pipelines fail silently

Graphics rendering pipelines have failure modes that don't map to HTTP errors:

Silent transcoder failure — Batik's TranscoderInput/TranscoderOutput pipeline can produce a zero-byte output file if the SVG contains a reference to a missing external resource. The Transcoder.transcode() call completes without exception while writing nothing to the output stream.

Font metric mismatch — XMLGraphics Commons shares font metric infrastructure with Apache FOP. A deployed batik-all JAR that doesn't match the FOP version in the same application can cause FontFamilyResolver to throw NullPointerException on specific glyph lookups — silently corrupting only documents using that font.

Memory exhaustion on complex SVG — rendering a complex SVG with embedded raster images or deeply nested <use> elements is memory-intensive. Under sustained load, individual rendering threads die with OutOfMemoryError while the JVM remains alive and healthy, causing a subset of exports to silently return null.

Batch rasterization stalls — nightly thumbnail generation, report rendering, and chart export pipelines run as background jobs. If a corrupt SVG in the input queue blocks the rendering thread, the entire batch silently stalls at that document.


Step 1: Expose a health endpoint that exercises XMLGraphics

A health check that skips the rendering pipeline proves nothing about SVG transcoding or font resolution. Render a minimal SVG to a PNG:

With Spring Boot:

import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.PNGTranscoder;
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 {

    // Minimal valid SVG for a round-trip transcoding health check
    private static final String MINIMAL_SVG =
        "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>" +
        "  <rect x='1' y='1' width='8' height='8' fill='blue'/>" +
        "</svg>";

    @GetMapping("/health")
    public Map<String, Object> health() throws TranscoderException, IOException {
        // Transcode SVG → PNG using Batik (exercises XMLGraphics Commons underneath)
        PNGTranscoder transcoder = new PNGTranscoder();
        transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, 10f);
        transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, 10f);

        byte[] svgBytes = MINIMAL_SVG.getBytes(StandardCharsets.UTF_8);
        TranscoderInput input = new TranscoderInput(new ByteArrayInputStream(svgBytes));
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        TranscoderOutput output = new TranscoderOutput(bos);

        transcoder.transcode(input, output);

        if (bos.size() == 0) {
            throw new IllegalStateException(
                "PNGTranscoder produced zero bytes — font or resource issue");
        }

        // Verify PNG header (89 50 4E 47 = \x89PNG)
        byte[] pngBytes = bos.toByteArray();
        if (pngBytes[0] != (byte) 0x89 || pngBytes[1] != (byte) 0x50) {
            throw new IllegalStateException("Output is not a valid PNG");
        }

        return Map.of(
            "status", "UP",
            "transcoder", "ok",
            "pngBytes", pngBytes.length
        );
    }
}

With a plain servlet:

import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.PNGTranscoder;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.nio.charset.StandardCharsets;

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

    private static final String MINIMAL_SVG =
        "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>" +
        "<rect x='1' y='1' width='8' height='8' fill='blue'/></svg>";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws IOException {
        res.setContentType("application/json");
        try {
            PNGTranscoder t = new PNGTranscoder();
            byte[] svgBytes = MINIMAL_SVG.getBytes(StandardCharsets.UTF_8);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            t.transcode(new TranscoderInput(new ByteArrayInputStream(svgBytes)),
                        new TranscoderOutput(bos));
            if (bos.size() == 0) throw new IllegalStateException("Zero-byte PNG");
            res.getWriter().write("{\"status\":\"UP\",\"pngBytes\":" + bos.size() + "}");
        } catch (Exception e) {
            res.setStatus(500);
            res.getWriter().write("{\"status\":\"DOWN\",\"error\":\"" + e.getMessage() + "\"}");
        }
    }
}

The minimal SVG is fast to render, exercises the full Batik transcoder pipeline (SVG parse → DOM build → PNG encode), and validates the XMLGraphics Commons font metric infrastructure without burning significant CPU.


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 broken thumbnails or blank chart exports.

Add monitors for each critical endpoint:

| Endpoint | What it catches | |---|---| | /health | SVG transcoder, XMLGraphics Commons, font resolution | | /api/render/chart | Chart SVG-to-PNG rendering endpoint | | /api/export/pdf | FOP/XMLGraphics PDF export endpoint |


Step 3: Heartbeat monitoring for batch rendering jobs

Nightly thumbnail generation, batch chart rendering, and report export pipelines run as background jobs with no HTTP endpoint. Heartbeat monitoring catches silent failures:

import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.PNGTranscoder;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;

public class NightlyThumbnailJob {

    private final Path svgInputDir = Path.of(System.getenv("SVG_INPUT_DIR"));
    private final Path pngOutputDir = Path.of(System.getenv("PNG_OUTPUT_DIR"));
    private final String heartbeatUrl = System.getenv("HEARTBEAT_THUMBNAIL_JOB_URL");

    public void run() throws Exception {
        List<Path> svgFiles = Files.list(svgInputDir)
            .filter(p -> p.toString().endsWith(".svg"))
            .toList();

        if (svgFiles.isEmpty()) {
            throw new IllegalStateException(
                "No SVG files in input directory — upstream asset pipeline may have stalled");
        }

        int rendered = 0;
        PNGTranscoder transcoder = new PNGTranscoder();
        transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, 200f);
        transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, 200f);

        for (Path svgFile : svgFiles) {
            String svgContent = Files.readString(svgFile, StandardCharsets.UTF_8);
            byte[] svgBytes = svgContent.getBytes(StandardCharsets.UTF_8);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            transcoder.transcode(
                new TranscoderInput(new ByteArrayInputStream(svgBytes)),
                new TranscoderOutput(bos)
            );

            if (bos.size() == 0) {
                throw new IllegalStateException(
                    "Zero-byte PNG for: " + svgFile.getFileName() +
                    " — SVG may reference missing external resource");
            }

            String pngName = svgFile.getFileName().toString().replace(".svg", ".png");
            Files.write(pngOutputDir.resolve(pngName), bos.toByteArray());
            rendered++;
        }

        if (rendered != svgFiles.size()) {
            throw new IllegalStateException(
                "Rendered " + rendered + "/" + svgFiles.size() + " thumbnails");
        }

        // Only ping after all thumbnails rendered and written
        if (heartbeatUrl != null && !heartbeatUrl.isBlank()) {
            pingHeartbeat(heartbeatUrl);
        }
    }

    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();
    }
}

In Vigilmon:

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

If a corrupt SVG stalls the rendering thread, memory exhaustion kills a rendering worker, or font resolution fails silently on a specific glyph, the heartbeat is never pinged and you get an alert after one missed interval.


Step 4: Validate the rendering pipeline at startup

Font metric issues and missing XMLGraphics resources fail at render time, not startup. Force validation early:

import org.apache.batik.transcoder.*;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.charset.StandardCharsets;

@Component
public class XmlGraphicsStartupValidator {

    private static final String MINIMAL_SVG =
        "<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'>" +
        "<rect x='1' y='1' width='8' height='8' fill='red'/>" +
        // Include a text element to exercise font resolution
        "<text x='1' y='9' font-size='4' fill='white'>OK</text>" +
        "</svg>";

    @EventListener(ApplicationReadyEvent.class)
    public void validate() throws Exception {
        PNGTranscoder transcoder = new PNGTranscoder();
        byte[] svgBytes = MINIMAL_SVG.getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        transcoder.transcode(
            new TranscoderInput(new ByteArrayInputStream(svgBytes)),
            new TranscoderOutput(bos)
        );

        if (bos.size() == 0) {
            throw new IllegalStateException(
                "XMLGraphics/Batik startup validation produced zero bytes. " +
                "Check font configuration and XMLGraphics Commons classpath.");
        }

        byte[] pngBytes = bos.toByteArray();
        // Verify PNG magic bytes
        if (pngBytes.length < 4 ||
                pngBytes[0] != (byte) 0x89 ||
                pngBytes[1] != (byte) 0x50) {
            throw new IllegalStateException(
                "XMLGraphics startup validation produced invalid PNG output. " +
                "Possible JAR version conflict between Batik and XMLGraphics Commons.");
        }
    }
}

The startup validator exercises the full SVG-to-PNG pipeline including text rendering — catching font metric mismatches and version conflicts at deploy time rather than at 2 AM during a batch rendering job.


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

What you've built

| What | How | |---|---| | External health checks | /health with SVG-to-PNG round-trip via Batik PNGTranscoder | | Pipeline validation | Text element in startup SVG exercises font metric resolution | | Batch job monitoring | Heartbeat ping after each successful thumbnail rendering run | | Zero-byte detection | Explicit output size check catches silent transcoder failures | | 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 Batik transcoder failures, XMLGraphics Commons font metric mismatches, zero-byte output on corrupt SVG inputs, and silently stalled nightly thumbnail generation jobs before users see broken images or blank chart 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 →