tutorial

health_sling.py

--- title: "How to Monitor Apache Sling Web Framework Health with Vigilmon" published: false description: Apache Sling JCR repository disconnects, resource ...


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 ResourceResolver opens sessions against the JCR; when the Jackrabbit Oak persistence layer loses connectivity to its segment store or MongoDB backend, all ResourceResolver operations throw RepositoryException — every request for content fails
  • ResourceResolver pool exhaustion: Sling uses a pool of ResourceResolver sessions; 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 Scheduler service 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

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Sling health endpoint: https://your-app.example.com/bin/health/sling
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok" or "jcr_accessible":"true"
    • Response time threshold: 5000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. 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 using org.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

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: sling-content-delivery
  3. Set the expected interval: 5 minutes
  4. Set the grace period: 15 minutes
  5. 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 3000ms for the Sling health endpoint (slow JCR session creation precedes repository failure)
  • Alert at 8000ms for the Health Check report endpoint (comprehensive HC checks hit multiple JCR paths)
  • Alert at 10 minutes gap 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →