title: "How to Monitor Apache Sling Web Framework Health with Vigilmon" published: false description: Apache Sling JCR repository disconnects, resource resolver failures, and Sling servlet registration gaps are invisible to standard HTTP checks. Learn how to monitor Sling resource resolution, JCR connectivity, and request processing with Vigilmon HTTP probes and heartbeat monitors. tags: sling, jcr, java, monitoring, osgi, cms, devops cover_image:
Apache Sling is a web framework built on OSGi and the JCR (Java Content Repository) that maps HTTP requests to content nodes stored in a repository like Apache Jackrabbit Oak. Every Sling request resolves a path to a JCR node and then selects a rendering script or servlet — when the JCR repository is unreachable, every request fails; when a Sling servlet fails to register due to a bundle resolution issue, all requests for its resource type return 404 or 500 silently; when the ResourceResolver pool is exhausted under load, requests queue and eventually time out. These failures don't produce clean HTTP errors — they produce cascading timeouts that are hard to trace back to the JCR layer.
Vigilmon gives you external visibility into Sling's content delivery layer through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.
Why Sling Needs External Monitoring
Sling's JCR-backed request model introduces failure modes that generic HTTP monitoring misses:
- JCR repository disconnects: Sling's
ResourceResolveropens sessions against the JCR; when the Jackrabbit Oak persistence layer loses connectivity to its segment store or MongoDB backend, all ResourceResolver operations throwRepositoryException— every request for content fails - ResourceResolver pool exhaustion: Sling uses a pool of
ResourceResolversessions; under high load or if callers forget to close resolvers, the pool exhausts and new requests block until timeout, producing 503s after a delay that makes the root cause unclear - Sling servlet registration failures: Sling servlets register against resource types (
sling:resourceType) or request paths via SCR properties; if the bundle containing a servlet fails to activate, requests for that resource type return 404 with no obvious error log entry at WARN level - Content path mounting failures: Sling supports multiple JCR workspaces and mount points; a failed mount leaves a subtree of the content hierarchy inaccessible while the rest of the site works normally
- Scheduler and background job failures: Sling's
Schedulerservice runs periodic maintenance jobs (index compaction, version purge, replication queue drain); when jobs stop executing, the JCR grows unbounded or replication backlogs build up silently
External monitoring with Vigilmon adds:
- Proactive alerting when JCR health endpoints report repository connectivity issues
- ResourceResolver pool monitoring before exhaustion causes cascading 503s
- Heartbeat monitoring to catch silent content delivery failures
- Multi-region HTTP probing that distinguishes CDN-layer failures from JCR-layer failures
Step 1: Build a Sling Health Endpoint
Sling ships with a health check framework (org.apache.sling.hc.api) that integrates with the Sling Starter and AEM. Use it alongside a custom lightweight endpoint for Vigilmon.
Sling Health Check (sling.hc.api)
// JcrConnectivityHealthCheck.java
import org.apache.felix.hc.api.FormattingResultLog;
import org.apache.felix.hc.api.HealthCheck;
import org.apache.felix.hc.api.Result;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.*;
@Component(service = HealthCheck.class,
property = {
HealthCheck.NAME + "=JCR Connectivity",
HealthCheck.TAGS + "=jcr,sling",
HealthCheck.MBEAN_NAME + "=jcrConnectivity"
})
public class JcrConnectivityHealthCheck implements HealthCheck {
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public Result execute() {
FormattingResultLog log = new FormattingResultLog();
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(null)) {
// Probe: resolve /content to verify JCR connectivity
Resource root = resolver.getResource("/content");
if (root == null) {
log.warn("JCR /content node not found — repository may be empty or inaccessible");
} else {
log.info("JCR accessible, /content resolved to type: {}", root.getResourceType());
}
} catch (LoginException e) {
log.critical("ResourceResolver login failed: {}", e.getMessage());
} catch (Exception e) {
log.critical("JCR connectivity probe failed: {}", e.getMessage());
}
return new Result(log);
}
}
Sling Servlet Health Endpoint
// VigilmonHealthServlet.java
import org.apache.sling.api.*;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.api.resource.*;
import org.osgi.service.component.annotations.*;
import javax.servlet.Servlet;
import java.io.*;
import java.util.*;
@Component(service = Servlet.class,
property = {
"sling.servlet.paths=/bin/health/sling",
"sling.servlet.methods=GET"
})
public class VigilmonHealthServlet extends SlingSafeMethodsServlet {
@Reference
private ResourceResolverFactory resolverFactory;
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws IOException {
response.setContentType("application/json");
Map<String, Object> result = new LinkedHashMap<>();
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(null)) {
// Probe JCR connectivity
Resource probe = resolver.getResource("/content");
result.put("jcr_accessible", probe != null);
result.put("resolver_user", resolver.getUserID());
// Check ResourceResolver pool stats (if using SlingResourceResolverFactory)
result.put("status", "ok");
response.setStatus(200);
} catch (LoginException e) {
result.put("status", "down");
result.put("reason", "resolver_login_failed");
result.put("error", e.getMessage());
response.setStatus(503);
} catch (Exception e) {
result.put("status", "down");
result.put("reason", "jcr_error");
result.put("error", e.getMessage());
response.setStatus(503);
}
writeJson(response.getWriter(), result);
}
private void writeJson(PrintWriter writer, Map<String, Object> data) {
StringBuilder sb = new StringBuilder("{");
data.forEach((k, v) -> sb.append('"').append(k).append("\":\"").append(v).append("\","));
if (sb.charAt(sb.length() - 1) == ',') sb.deleteCharAt(sb.length() - 1);
sb.append('}');
writer.write(sb.toString());
}
}
Spring Boot Actuator Health Indicator (Sling + Spring)
// SlingHealthIndicator.java
import org.apache.sling.api.resource.*;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class SlingHealthIndicator implements HealthIndicator {
private final ResourceResolverFactory resolverFactory;
public SlingHealthIndicator(ResourceResolverFactory resolverFactory) {
this.resolverFactory = resolverFactory;
}
@Override
public Health health() {
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(null)) {
Resource contentRoot = resolver.getResource("/content");
return Health.up()
.withDetail("jcr_accessible", true)
.withDetail("content_root_exists", contentRoot != null)
.withDetail("resolver_user", resolver.getUserID())
.build();
} catch (LoginException e) {
return Health.down()
.withDetail("reason", "resolver_login_failed")
.withDetail("error", e.getMessage())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("reason", "jcr_connectivity_failed")
.withDetail("error", e.getMessage())
.build();
}
}
}
Node.js Sidecar
// health/sling.js
const express = require('express');
const http = require('http');
const app = express();
const SLING_HOST = process.env.SLING_HOST || 'localhost';
const SLING_PORT = parseInt(process.env.SLING_PORT || '8080');
function probe(path) {
return new Promise((resolve, reject) => {
const req = http.get(
{ host: SLING_HOST, port: SLING_PORT, path, timeout: 8000 },
res => {
let body = '';
res.on('data', c => { body += c; });
res.on('end', () => resolve({ status: res.statusCode, body }));
}
);
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
});
}
app.get('/health/sling', async (req, res) => {
try {
const health = await probe('/bin/health/sling');
if (health.status !== 200) {
return res.status(503).json({
status: 'down',
sling_status: health.status,
body: health.body.substring(0, 400),
});
}
const data = JSON.parse(health.body);
if (data.status !== 'ok') {
return res.status(503).json({ status: 'degraded', detail: data });
}
return res.status(200).json({ status: 'ok', jcr_accessible: data.jcr_accessible });
} catch (err) {
return res.status(503).json({ status: 'down', error: err.message });
}
});
app.listen(3014);
Python Health Sidecar
import os, requests
from flask import Flask, jsonify
app = Flask(__name__)
SLING_BASE = f"http://{os.environ.get('SLING_HOST', 'localhost')}:{os.environ.get('SLING_PORT', '8080')}"
@app.route('/health/sling')
def health():
try:
resp = requests.get(f'{SLING_BASE}/bin/health/sling', timeout=8)
if resp.status_code != 200:
return jsonify({'status': 'down', 'sling_status': resp.status_code, 'body': resp.text[:400]}), 503
data = resp.json()
if data.get('status') not in ('ok', 'UP'):
return jsonify({'status': 'degraded', 'detail': data}), 503
return jsonify({'status': 'ok',
'jcr_accessible': data.get('jcr_accessible', False),
'resolver_user': data.get('resolver_user', 'unknown')})
except Exception as e:
return jsonify({'status': 'down', 'error': str(e)}), 503
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3014)
Step 2: Configure Vigilmon HTTP Monitor for Sling
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your Sling health endpoint:
https://your-app.example.com/bin/health/sling - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok"or"jcr_accessible":"true" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- Save the monitor
Add a second monitor for the Sling Health Check report endpoint:
- URL:
https://your-app.example.com/system/health(Sling HC JSON report, if usingorg.apache.sling.hc.webconsole) - Expected:
200 - Interval: 3 minutes
- Alert channel: P1 pager — overall health check failure means JCR or a core Sling subsystem is down
Vigilmon's multi-region probing distinguishes a regional CDN miss from a JCR repository failure affecting the origin.
Step 3: Heartbeat Monitoring for Content Delivery Success
JCR health monitoring catches repository connectivity failures, but not silent rendering failures — a Sling servlet can register successfully and then throw on every invocation due to a null content node or a missing template, returning a 500 that the error handler converts to a 200 with a custom error page.
Vigilmon heartbeat monitors catch these by requiring your application to actively confirm successful content delivery.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
sling-content-delivery - Set the expected interval: 5 minutes
- Set the grace period: 15 minutes
- Save — copy the heartbeat URL, e.g.
https://vigilmon.online/heartbeat/abc123xyz
Wire It Into a Sling Request Filter
// VigilmonHeartbeatFilter.java
import org.apache.sling.api.*;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.*;
import javax.servlet.*;
import java.io.IOException;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.atomic.*;
@Component(service = Filter.class,
property = {
"sling.filter.scope=REQUEST",
"service.ranking:Integer=100"
})
public class VigilmonHeartbeatFilter implements Filter {
private final HttpClient http = HttpClient.newHttpClient();
private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");
private final AtomicLong lastPingMs = new AtomicLong(0);
private static final long PING_INTERVAL_MS = 60_000;
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(req, resp);
// Ping after successful requests only
if (resp instanceof SlingHttpServletResponse slingResp) {
int status = slingResp.getStatus();
if (status >= 200 && status < 400) {
long now = System.currentTimeMillis();
if (now - lastPingMs.get() > PING_INTERVAL_MS) {
lastPingMs.set(now);
pingHeartbeat();
}
}
}
}
@Override
public void init(FilterConfig config) {}
@Override
public void destroy() {}
private void pingHeartbeat() {
if (heartbeatUrl == null) return;
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
}
}
Sling Scheduler Heartbeat (for Background Jobs)
// SlingJobHeartbeat.java
import org.apache.sling.commons.scheduler.Scheduler;
import org.osgi.service.component.annotations.*;
import java.net.http.*;
import java.net.URI;
@Component(immediate = true)
public class SlingJobHeartbeat {
@Reference
private Scheduler scheduler;
private final HttpClient http = HttpClient.newHttpClient();
private final String heartbeatUrl = System.getenv("VIGILMON_JOB_HEARTBEAT_URL");
@Activate
protected void activate() {
scheduler.addPeriodicJob(
"vigilmon-heartbeat",
this::runHealthyJob,
null,
300, // every 5 minutes
false,
true
);
}
private void runHealthyJob() {
try {
// Perform a lightweight JCR operation
performJcrProbe();
pingHeartbeat();
} catch (Exception e) {
// JCR unavailable — let heartbeat lapse
}
}
private void performJcrProbe() throws Exception {
// Lightweight probe — can be a node count query or session ping
}
private void pingHeartbeat() {
if (heartbeatUrl == null) return;
try {
http.sendAsync(
HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
HttpResponse.BodyHandlers.discarding()
);
} catch (Exception ignored) {}
}
}
Step 4: Alert Routing for Sling Failures
Sling failures range from a single servlet failing to register (one resource type returns 404) to the JCR repository going offline (all content requests fail). Route alerts by severity:
| Monitor | Alert Channel | Priority |
|---|---|---|
| HTTP: /bin/health/sling | Slack + PagerDuty | P1 |
| HTTP: /system/health Sling HC report | Slack + PagerDuty | P1 |
| Heartbeat: content delivery filter | Slack + PagerDuty | P1 |
| Heartbeat: scheduled Sling jobs | Slack + email | P2 |
Set response time thresholds as early warning signals:
- Alert at
3000msfor the Sling health endpoint (slow JCR session creation precedes repository failure) - Alert at
8000msfor the Health Check report endpoint (comprehensive HC checks hit multiple JCR paths) - Alert at
10 minutesgap in the content delivery heartbeat during business hours
For production Sling deployments (including Adobe AEM, which runs on Sling), set up a status page in Vigilmon showing JCR connectivity, ResourceResolver pool health, and content delivery heartbeat — this is the fastest way to confirm a JCR storage failure rather than spending time ruling out CDN, DNS, or application-layer causes during an incident.
Summary
Sling failures cascade from the JCR layer through the resource resolution layer to the HTTP response layer — each layer can mask the failure as a different symptom (404, 500, timeout, or blank page). External monitoring from Vigilmon catches the root cause before it reaches users:
| Monitor Type | What It Covers |
|---|---|
| HTTP monitor on /bin/health/sling | JCR connectivity, ResourceResolver pool health |
| HTTP monitor on /system/health | All registered Sling Health Checks |
| Heartbeat monitor (request filter) | End-to-end content delivery success |
| Heartbeat monitor (Sling Scheduler) | Background job liveness, JCR maintenance |
Get started free at vigilmon.online — your first Sling content delivery monitor is running in under two minutes.