How to Monitor Apache UIMA Pipelines with Vigilmon
Apache UIMA (Unstructured Information Management Architecture) is the backbone of many production NLP systems — from clinical text mining at hospitals to entity extraction at scale. UIMA pipelines can process millions of documents through chains of annotators, and UIMA AS (Asynchronous Scaleout) deploys these pipelines as distributed services over ActiveMQ. But UIMA pipelines fail in ways that are invisible to standard monitoring: a downstream annotator can hang, a JMS queue can fill up, or a CAS pool can exhaust silently, leaving documents stuck and no HTTP 500 anywhere.
This tutorial wires up layered monitoring for a UIMA AS deployment:
- A REST health wrapper around your UIMA service
- HTTP uptime monitoring with Vigilmon
- Heartbeat emission tied to actual document throughput
- Queue depth alerting via a custom probe
- Slack alerts when pipeline throughput drops
Why Monitor UIMA?
| Signal | What it catches | |---|---| | Service availability | UIMA AS broker down, JVM crash | | Document throughput heartbeat | Annotator deadlocks, CAS pool exhaustion | | Queue depth | Backlog buildup, consumer failures | | Annotator error rate | Type system mismatches, NLP model failures | | Processing latency | Slow regex, overloaded models |
A TCP check on the ActiveMQ port tells you the broker is running — it says nothing about whether documents are actually flowing through your pipeline.
Step 1: Add a REST Health Wrapper
UIMA AS services communicate via JMS, not HTTP. Wrap your UIMA client with a lightweight Spring Boot health endpoint that Vigilmon can poll:
// src/main/java/com/example/uima/UimaHealthController.java
package com.example.uima;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import java.util.Map;
@RestController
public class UimaHealthController {
private final UimaPipelineService pipelineService;
public UimaHealthController(UimaPipelineService pipelineService) {
this.pipelineService = pipelineService;
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
boolean healthy = pipelineService.isHealthy();
long processed = pipelineService.getDocumentsProcessedLastMinute();
if (healthy && processed >= 0) {
return ResponseEntity.ok(Map.of(
"status", "ok",
"docsLastMinute", processed,
"casPoolAvailable", pipelineService.getCasPoolAvailable()
));
}
return ResponseEntity.status(503).body(Map.of(
"status", "degraded",
"reason", pipelineService.getLastError()
));
}
}
// src/main/java/com/example/uima/UimaPipelineService.java
package com.example.uima;
import org.apache.uima.aae.client.UimaAsynchronousEngine;
import org.apache.uima.aae.client.UimaAsBaseCallbackListener;
import org.apache.uima.cas.CAS;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicBoolean;
@Service
public class UimaPipelineService {
private UimaAsynchronousEngine uimaEngine;
private final AtomicLong docCount = new AtomicLong(0);
private final AtomicBoolean healthy = new AtomicBoolean(true);
private volatile String lastError = "";
public void sendDocument(String documentText) throws Exception {
CAS cas = uimaEngine.getCAS();
cas.setDocumentText(documentText);
uimaEngine.sendCAS(cas, new UimaAsBaseCallbackListener() {
@Override
public void entityProcessComplete(CAS aCas, EntityProcessStatus aStatus) {
if (aStatus.isException()) {
lastError = aStatus.getStatusMessage();
healthy.set(false);
} else {
docCount.incrementAndGet();
healthy.set(true);
}
}
});
}
public boolean isHealthy() { return healthy.get(); }
public long getDocumentsProcessedLastMinute() { return docCount.getAndSet(0); }
public String getLastError() { return lastError; }
public int getCasPoolAvailable() {
return uimaEngine != null ? uimaEngine.getCasPoolSize() : -1;
}
}
Test the endpoint:
curl http://localhost:8080/health
# => {"status":"ok","docsLastMinute":342,"casPoolAvailable":8}
Step 2: Emit a Throughput Heartbeat
Document throughput is the real health signal for a UIMA pipeline. Send a heartbeat to Vigilmon every minute, but only if documents are actually flowing:
// src/main/java/com/example/uima/ThroughputHeartbeatTask.java
package com.example.uima;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
@Component
public class ThroughputHeartbeatTask {
private final UimaPipelineService pipelineService;
private final HttpClient httpClient = HttpClient.newHttpClient();
@Value("${vigilmon.heartbeat.url:}")
private String heartbeatUrl;
@Value("${vigilmon.heartbeat.minDocs:1}")
private long minDocsPerMinute;
public ThroughputHeartbeatTask(UimaPipelineService pipelineService) {
this.pipelineService = pipelineService;
}
@Scheduled(fixedDelay = 60_000)
public void ping() throws Exception {
if (heartbeatUrl.isBlank()) return;
long docs = pipelineService.getDocumentsProcessedLastMinute();
boolean pipelineOk = pipelineService.isHealthy() && docs >= minDocsPerMinute;
if (pipelineOk) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(heartbeatUrl))
.GET()
.build();
httpClient.send(req, HttpResponse.BodyHandlers.discarding());
}
// If docs < minDocsPerMinute, skip the ping — Vigilmon will alert on missing heartbeat
}
}
Set the environment variable:
export VIGILMON_HEARTBEAT_URL="https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
export VIGILMON_HEARTBEAT_MINDOCS=10 # alert if fewer than 10 docs/min
Step 3: Add Vigilmon HTTP Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint URL:
http://your-uima-host:8080/health. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
This catches JVM crashes, broker outages, and CAS pool exhaustion.
Step 4: Add a Heartbeat Monitor for Throughput
- Click Add Monitor → Cron / Heartbeat.
- Set Expected ping interval to
1 minute. - Set Grace period to
3 minutes(accommodates batch load variation). - Copy the generated heartbeat URL into
VIGILMON_HEARTBEAT_URL. - Click Save.
When throughput drops below your threshold, the heartbeat stops and Vigilmon fires an alert within 4 minutes — before your SLA window closes.
Step 5: Monitor ActiveMQ Queue Depth
Queue buildup is an early warning sign before throughput drops to zero. Write a lightweight probe that exports queue depth as a Vigilmon-compatible metric:
// src/main/java/com/example/uima/QueueDepthController.java
package com.example.uima;
import org.apache.activemq.broker.jmx.QueueViewMBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;
import javax.management.*;
import java.util.Map;
@RestController
public class QueueDepthController {
@Value("${uima.queue.name:UimaInputQueue}")
private String queueName;
@Value("${uima.queue.maxDepth:10000}")
private long maxQueueDepth;
@GetMapping("/health/queue")
public ResponseEntity<Map<String, Object>> queueHealth() {
try {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName queueObject = new ObjectName(
"org.apache.activemq:type=Broker,brokerName=localhost,destinationType=Queue,destinationName=" + queueName
);
Long queueSize = (Long) mbs.getAttribute(queueObject, "QueueSize");
if (queueSize <= maxQueueDepth) {
return ResponseEntity.ok(Map.of("status", "ok", "queueDepth", queueSize));
}
return ResponseEntity.status(503).body(Map.of(
"status", "backlogged",
"queueDepth", queueSize,
"maxAllowed", maxQueueDepth
));
} catch (Exception e) {
return ResponseEntity.status(503).body(Map.of("status", "error", "detail", e.getMessage()));
}
}
}
Add a second Vigilmon monitor pointing to /health/queue with a body check for "status":"ok".
Step 6: Set Up Alerts
In Vigilmon, configure alert channels for your UIMA monitors:
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Set alert thresholds: notify after 2 consecutive failures.
- Assign the alert channel to your HTTP monitor, heartbeat monitor, and queue depth monitor.
For NLP services with SLAs, create a Status Page for internal consumers:
- Go to Status Pages → Create Page.
- Add your UIMA health monitor.
- Share the internal URL with teams that depend on the pipeline output.
Key Metrics to Watch
| Metric | Vigilmon feature | What to alert on |
|---|---|---|
| Service availability | HTTP monitor on /health | Any 5xx or timeout |
| Document throughput | Heartbeat monitor | Heartbeat missing > 4 min |
| Queue depth | HTTP monitor on /health/queue | Any non-200 (backlog threshold crossed) |
| Response time | Response time chart | P95 > 10s |
| ActiveMQ broker | HTTP monitor on broker admin port | Any 5xx or timeout |
Conclusion
UIMA pipelines fail silently — a deadlocked annotator or an exhausted CAS pool won't produce HTTP 500s. The combination of a REST health endpoint, a throughput-gated heartbeat, and a queue depth probe gives you three independent signals that triangulate exactly where the pipeline is failing. With these Vigilmon monitors in place, you'll detect NLP pipeline stalls within minutes, not hours.
Get started free at vigilmon.online.