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:
- 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 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:
- Click New Monitor → Heartbeat
- Set the expected interval (e.g. 25 hours for a daily rendering job)
- Copy the unique ping URL
- 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:
- 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=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.