Apache TomEE is a Java EE certified application server built on Tomcat that adds CDI, EJB, JPA, JTA, and JAX-RS to a familiar Tomcat runtime. TomEE handles enterprise transactions, dependency injection, and persistence across your application — when the CDI Weld container fails to resolve a bean during deployment, all injection points for that bean silently become null references that only throw at invocation time; when a JTA transaction coordinator cannot reach the database, application code receives an EJBTransactionRolledbackException that looks like a business logic error; when an EJB stateful session bean pool is exhausted, callers block until timeout and TomEE logs a ConcurrentAccessTimeoutException that no external system observes. These are production incidents disguised as application errors.
Vigilmon gives you external visibility into TomEE's EJB and CDI layers through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.
Why TomEE Needs External Monitoring
TomEE's failure modes are subtle and high-impact:
- CDI bean resolution failures: a missing
@Producesmethod or a deployment descriptor conflict causes Weld to fail to resolve a bean type, resulting in null injection points that only throw aNullPointerExceptionat invocation time — not at startup - EJB container unavailability: the TomEE EJB container registers MBeans at startup; if the MBean server is unavailable, EJB deployment silently aborts while the Tomcat HTTP layer continues accepting requests on the same port
- JTA transaction rollbacks: a database connection pool exhaustion during a distributed transaction causes JTA to roll back and TomEE to throw
TransactionRolledbackException— the application returns HTTP 500 with no external signal that the transaction subsystem is the root cause - Stateful session bean timeout: TomEE passivates stateful EJBs to disk after an idle period; if the passivation directory is full or the serialization fails, the bean is discarded and subsequent calls throw
NoSuchEJBException— users see sporadic session loss - JAX-RS deployment failure: a missing provider or a content negotiation conflict causes the JAX-RS runtime to fail to deploy an endpoint silently, resulting in 404 responses for REST resources that existed before the redeployment
- JPA persistence unit initialization failure: a DataSource JNDI misconfiguration after deployment causes
EntityManagerFactorycreation to fail, throwingPersistenceExceptionon the first@PersistenceContextinjection — the Tomcat servlet continues to start but all database operations fail
External monitoring with Vigilmon adds:
- Proactive alerting when EJB or CDI health endpoints report container failures
- Application availability monitoring independent of the Tomcat servlet container state
- Heartbeat monitoring so you know when batch EJB processing or message-driven bean consumption stops completing
- Multi-region probe consensus that filters transient network blips from genuine TomEE application failures
Step 1: Build a TomEE Health Endpoint
TomEE does not ship with a built-in health endpoint. Add one as a JAX-RS resource or a dedicated servlet.
JAX-RS Health Resource
// health/HealthResource.java
package com.yourapp.health;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.*;
@Path("/health")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
public class HealthResource {
@PersistenceContext(unitName = "primaryPU")
private EntityManager em;
@Inject
private CacheService cacheService;
@GET
public Response health() {
Map<String, Object> result = new LinkedHashMap<>();
Map<String, String> checks = new LinkedHashMap<>();
boolean anyFailed = false;
// JPA/database check
try {
em.createNativeQuery("SELECT 1").getSingleResult();
checks.put("database", "ok");
} catch (Exception e) {
checks.put("database", "down");
anyFailed = true;
}
// CDI bean check
try {
boolean cacheUp = cacheService.isHealthy();
checks.put("cache", cacheUp ? "ok" : "down");
if (!cacheUp) anyFailed = true;
} catch (Exception e) {
checks.put("cache", "down");
anyFailed = true;
}
result.put("status", anyFailed ? "degraded" : "ok");
result.put("checks", checks);
return Response
.status(anyFailed ? Response.Status.SERVICE_UNAVAILABLE : Response.Status.OK)
.entity(result)
.build();
}
}
Register the JAX-RS application if you do not already have one:
// AppApplication.java
package com.yourapp;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/api")
public class AppApplication extends Application {}
The health endpoint is then available at https://your-app.example.com/api/health.
EJB MBean Health Servlet
// servlet/HealthServlet.java
package com.yourapp.servlet;
import javax.management.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.util.*;
@javax.servlet.annotation.WebServlet("/health/ejb")
public class HealthServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
boolean ejbContainerUp = false;
try {
Set<ObjectName> names = mbs.queryNames(
new ObjectName("openejb:type=Deployer,*"), null);
ejbContainerUp = !names.isEmpty();
} catch (Exception ignored) {}
resp.setContentType("application/json");
if (ejbContainerUp) {
resp.setStatus(200);
resp.getWriter().write("{\"status\":\"ok\",\"ejb_container\":\"up\"}");
} else {
resp.setStatus(503);
resp.getWriter().write("{\"status\":\"down\",\"ejb_container\":\"unavailable\"}");
}
}
}
Node.js Sidecar
// health/tomee.js
const express = require('express');
const http = require('http');
const app = express();
const APP_HOST = process.env.APP_HOST || 'localhost';
const APP_PORT = parseInt(process.env.APP_PORT || '8080');
const HEALTH_PATH = process.env.TOMEE_HEALTH_PATH || '/api/health';
function probeTomEE() {
return new Promise((resolve, reject) => {
const req = http.get(
{ host: APP_HOST, port: APP_PORT, path: HEALTH_PATH, timeout: 5000 },
res => {
let body = '';
res.on('data', chunk => { body += chunk; });
res.on('end', () => resolve({ status: res.statusCode, body }));
}
);
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
});
}
app.get('/health/tomee', async (req, res) => {
try {
const result = await probeTomEE();
const up = result.status === 200 &&
(result.body.includes('"status":"ok"') || result.body.includes('"status":"UP"'));
if (!up) {
return res.status(503).json({
status: 'down',
upstream_status: result.status,
body: result.body.substring(0, 500),
});
}
return res.status(200).json({ status: 'ok' });
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3015);
Python Health Sidecar
# health_tomee.py
import os, requests
from flask import Flask, jsonify
app = Flask(__name__)
APP_BASE = f"http://{os.environ.get('APP_HOST', 'localhost')}:{os.environ.get('APP_PORT', '8080')}"
HEALTH_PATH = os.environ.get('TOMEE_HEALTH_PATH', '/api/health')
@app.route('/health/tomee')
def health():
try:
resp = requests.get(f'{APP_BASE}{HEALTH_PATH}', timeout=5)
if resp.status_code != 200:
return jsonify({'status': 'down', 'upstream_status': resp.status_code}), 503
data = resp.json()
if data.get('status') not in ('ok', 'UP'):
return jsonify({'status': 'degraded', 'detail': data}), 503
return jsonify({'status': 'ok', 'checks': data.get('checks', {})})
except Exception as e:
return jsonify({'status': 'down', 'error': str(e)}), 503
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3015)
Step 2: Configure Vigilmon HTTP Monitor for TomEE
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your TomEE health endpoint:
https://your-app.example.com/api/health - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
3000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Add a second monitor for the EJB container check:
- URL:
https://your-app.example.com/health/ejb - Expected:
200, body contains:"ejb_container":"up" - Interval: 2 minutes
- Alert channel: P1 pager — EJB container failure means all session beans and message-driven beans are unavailable
Vigilmon's multi-region probe consensus filters transient Tomcat restart blips from genuine TomEE application failures.
Step 3: Heartbeat Monitoring for EJB Batch Jobs
Health endpoint monitoring catches EJB container and CDI failures, but not end-to-end batch processing problems. The EJB container can report healthy while a scheduled @Timer callback has been failing silently due to a JTA transaction rollback on a specific record.
Vigilmon heartbeat monitors catch these: your EJB batch job pings Vigilmon after each successful run.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
tomee-batch-processor - Set the expected interval: 10 minutes
- Set the grace period: 20 minutes
- Save — copy the heartbeat URL, e.g.
https://vigilmon.online/heartbeat/abc123xyz
Wire It Into a TomEE EJB Timer
// ejb/BatchProcessorBean.java
package com.yourapp.ejb;
import javax.ejb.*;
import java.net.http.*;
import java.net.URI;
@Singleton
@Startup
public class BatchProcessorBean {
private final HttpClient http = HttpClient.newHttpClient();
private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");
@Schedule(minute = "*/10", hour = "*", persistent = false)
public void processBatch() {
try {
doProcessBatch();
pingHeartbeat();
} catch (Exception e) {
// Log error — heartbeat lapses naturally, Vigilmon alerts
}
}
private void doProcessBatch() throws Exception {
// batch processing logic
}
private void pingHeartbeat() {
if (heartbeatUrl == null) return;
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
}
}
For message-driven beans, add the heartbeat ping at the end of onMessage() before the method returns, and suppress the ping on exceptions so Vigilmon alerts on missed heartbeats.
Step 4: Alert Routing for TomEE Failures
TomEE failures range from a single CDI injection failing to the entire EJB container being unavailable. Route alerts by severity:
| Monitor | Alert Channel | Priority |
|---|---|---|
| HTTP: /api/health JAX-RS check | Slack + PagerDuty | P1 |
| HTTP: /health/ejb EJB container check | Slack + PagerDuty | P1 |
| Heartbeat: EJB batch processor | Slack + PagerDuty | P1 |
| Heartbeat: scheduled timer job | Slack + email | P2 |
Set response time thresholds as early warning signals:
- Alert at
800msfor the JAX-RS health check (slow responses indicate JTA or DataSource pool pressure) - Alert at
2000msfor the EJB container check (EJB container slowdowns precede full session bean unavailability) - Alert at
15 minutesgap in batch processor heartbeat during business hours
For production applications, set up a status page in Vigilmon showing EJB container availability and batch job liveness — this lets on-call engineers identify TomEE application failures instantly rather than inferring them from user reports of missing transactions or session errors.
Summary
TomEE failures present as application errors, not infrastructure failures — users see missing data or error pages while the real cause is a JTA transaction rollback or a CDI injection failure. External monitoring catches these before they become an incident:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /api/health | Database, cache, and CDI bean health |
| HTTP monitor on /health/ejb | EJB container initialization and MBean state |
| Heartbeat monitor (EJB batch processor) | End-to-end batch EJB execution success |
| Heartbeat monitor (scheduled timer) | EJB timer invocation liveness |
Get started free at vigilmon.online — your first TomEE application monitor is running in under two minutes.