tutorial

How to Monitor Apache Tapestry Application Health with Vigilmon

Apache Tapestry component render failures, IoC container initialization errors, and background service crashes are silent until users report broken pages. Learn how to monitor Tapestry page availability, service registry health, and background job liveness with Vigilmon HTTP probes and heartbeat monitors.

Apache Tapestry is a component-based Java web framework that manages complex page rendering, dependency injection, and live class reloading inside a single IoC container. Tapestry fronts every HTTP request — when the IoC container fails to initialize a service, the entire application enters a degraded state that returns styled exception pages with HTTP 200; when a background service started via @Startup crashes silently, workflows stop running without any log entry; when live class reloading corrupts the component registry after a hot deployment, entire page families start throwing ComponentEventException while the process monitor shows the JVM as healthy. These are production incidents disguised as rendering errors.

Vigilmon gives you external visibility into Tapestry's application layer through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.


Why Tapestry Needs External Monitoring

Tapestry's failure modes are subtle and high-impact:

  • Exception pages masquerading as success: Tapestry catches unhandled exceptions and renders a styled error page that returns HTTP 200 — load balancers and process monitors see a healthy response while users see a broken application
  • IoC container partial initialization: when a @Startup service throws during container initialization, dependent services may be created in an indeterminate state; requests to pages that use those services fail with misleading ServiceLocatorException messages
  • Live class reloading corruption: Tapestry's development-mode class reloader can leave the component registry in an inconsistent state after a hot deployment, causing ComponentEventException on any page that references reloaded components
  • Symbol provider misconfiguration: Tapestry resolves configuration symbols at startup; a missing symbol provider entry causes deferred NullPointerException failures in components that load correctly during startup but fail at first render
  • Scheduled service silent failures: Tapestry services that schedule background work via ScheduledExecutorService or Quartz throw independently of the request cycle — failures leave no 5xx trace in HTTP logs
  • Asset pipeline failures: Tapestry's asset minification and aggregation pipeline can fail silently on startup, causing JavaScript and CSS assets to return 404 while HTML pages respond normally

External monitoring with Vigilmon adds:

  • Proactive alerting when page health endpoints return non-200 or contain error markers
  • IoC service availability monitoring independent of the JVM process state
  • Heartbeat monitoring so you know when background services stop completing their scheduled work
  • Multi-region probe consensus that filters transient blips from genuine application failures

Step 1: Build a Tapestry Health Endpoint

Tapestry does not ship with a dedicated health endpoint. Add one as a dedicated Tapestry page that exercises critical dependencies and returns an unambiguous HTTP status code.

Tapestry Health Page with StreamResponse

// pages/Health.java
package com.yourapp.pages;

import com.yourapp.services.DatabaseService;
import com.yourapp.services.CacheService;
import org.apache.tapestry5.StreamResponse;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Response;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class Health {

    @Inject
    private DatabaseService databaseService;

    @Inject
    private CacheService cacheService;

    @Inject
    private Response response;

    Object onActivate() {
        Map<String, String> checks = new LinkedHashMap<>();
        boolean anyFailed = false;

        checks.put("database", databaseService.isHealthy() ? "ok" : "down");
        checks.put("cache", cacheService.isHealthy() ? "ok" : "down");

        for (String v : checks.values()) {
            if ("down".equals(v)) { anyFailed = true; break; }
        }

        StringBuilder json = new StringBuilder("{");
        json.append("\"status\":\"").append(anyFailed ? "degraded" : "ok").append("\",");
        json.append("\"checks\":{");
        checks.forEach((k, v) -> json.append("\"").append(k).append("\":\"").append(v).append("\","));
        if (!checks.isEmpty()) json.deleteCharAt(json.length() - 1);
        json.append("}}");

        final String body = json.toString();
        final int code = anyFailed ? 503 : 200;

        return new StreamResponse() {
            @Override public String getContentType() { return "application/json"; }
            @Override public void prepareResponse(Response r) { r.setStatus(code); }
            @Override public InputStream getStream() {
                return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
            }
        };
    }
}

Tapestry maps this page to /health by default (convention over configuration).

Spring Boot Actuator Health Indicator (for hybrid setups)

// TapestryHealthIndicator.java
import org.apache.tapestry5.ioc.Registry;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class TapestryHealthIndicator implements HealthIndicator {

    private final Registry tapestryRegistry;

    public TapestryHealthIndicator(Registry tapestryRegistry) {
        this.tapestryRegistry = tapestryRegistry;
    }

    @Override
    public Health health() {
        try {
            Object service = tapestryRegistry.getService("ApplicationStatePersistenceStrategy");
            if (service == null) {
                return Health.down()
                    .withDetail("reason", "core_service_null")
                    .build();
            }
            return Health.up()
                .withDetail("registry", "initialized")
                .withDetail("service_count", tapestryRegistry.toString())
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("reason", "registry_unavailable")
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

Node.js Sidecar

// health/tapestry.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.TAPESTRY_HEALTH_PATH || '/health';

function probeTapestry() {
  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/tapestry', async (req, res) => {
  try {
    const result = await probeTapestry();
    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(3013);

Python Health Sidecar

# health_tapestry.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('TAPESTRY_HEALTH_PATH', '/health')

@app.route('/health/tapestry')
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=3013)

Step 2: Configure Vigilmon HTTP Monitor for Tapestry

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Tapestry health endpoint: https://your-app.example.com/health
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 3000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. Save the monitor

Add a second monitor for a critical page render check:

  • URL: https://your-app.example.com/ (or your most critical page)
  • Expected: 200, response body does NOT contain: ExceptionReport
  • Interval: 2 minutes
  • Alert channel: P1 pager — silent exception pages returning 200 are Tapestry's most dangerous failure mode

Vigilmon's multi-region probe consensus filters transient network blips from genuine Tapestry application failures.


Step 3: Heartbeat Monitoring for Background Service Completion

Health endpoint monitoring catches IoC container failures, but not background service execution problems. A Tapestry IoC container can report all services as healthy while a scheduled background service has been silently failing every run for hours.

Vigilmon heartbeat monitors catch these: your background service pings Vigilmon after each successful execution.

Set Up the Heartbeat Monitor

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: tapestry-background-service
  3. Set the expected interval: 15 minutes
  4. Set the grace period: 30 minutes
  5. Save — copy the heartbeat URL, e.g. https://vigilmon.online/heartbeat/abc123xyz

Wire It Into a Tapestry Scheduled Service

// services/impl/ReportServiceImpl.java
package com.yourapp.services.impl;

import com.yourapp.services.ReportService;
import org.apache.tapestry5.ioc.annotations.PostInjection;
import org.apache.tapestry5.ioc.services.RegistryShutdownHub;
import java.net.http.*;
import java.net.URI;
import java.util.concurrent.*;

public class ReportServiceImpl implements ReportService {

    private final ScheduledExecutorService executor =
        Executors.newSingleThreadScheduledExecutor();
    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");

    @PostInjection
    public void startup(RegistryShutdownHub hub) {
        executor.scheduleAtFixedRate(this::generateReport, 0, 15, TimeUnit.MINUTES);
        hub.addRegistryShutdownListener(executor::shutdown);
    }

    private void generateReport() {
        try {
            // ... actual report generation logic
            doGenerateReport();
            pingHeartbeat();
        } catch (Exception e) {
            // Log error — heartbeat lapses naturally, Vigilmon alerts
        }
    }

    private void doGenerateReport() {
        // business logic
    }

    private void pingHeartbeat() {
        if (heartbeatUrl == null) return;
        try {
            http.sendAsync(
                HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
            );
        } catch (Exception ignored) {}
    }
}

Register the service in your AppModule:

// AppModule.java
public static void bind(ServiceBinder binder) {
    binder.bind(ReportService.class, ReportServiceImpl.class).eagerLoad();
}

The eagerLoad() ensures the scheduler starts at container initialization, not on first use.


Step 4: Alert Routing for Tapestry Failures

Tapestry failures range from a single page rendering incorrectly to the entire IoC container being in a degraded state. Route alerts by severity:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: /health dependency check | Slack + PagerDuty | P1 | | HTTP: / page render check (no ExceptionReport) | Slack + PagerDuty | P1 | | Heartbeat: background report service | Slack + PagerDuty | P1 | | Heartbeat: asset pipeline job | Slack + email | P2 |

Set response time thresholds as early warning signals:

  • Alert at 800ms for the Tapestry health page (slow responses indicate database or cache latency)
  • Alert at 2000ms for page render checks (Tapestry render slowdowns precede full component failures)
  • Alert at 20 minutes gap in background service heartbeat

For production applications, set up a status page in Vigilmon showing page availability and background service liveness — this lets on-call engineers identify Tapestry application failures instantly rather than inferring them from user reports of broken pages.


Summary

Tapestry failures present as rendering errors, not infrastructure failures — users see exception pages or missing assets while the real cause is an IoC container service failure or a corrupted component registry. External monitoring catches these before they become an incident:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /health | Database, cache, and IoC service dependencies | | HTTP monitor on / with body check | Silent exception pages returning HTTP 200 | | Heartbeat monitor (background service) | Scheduled service execution success | | Heartbeat monitor (asset pipeline) | Asset minification and aggregation job liveness |

Get started free at vigilmon.online — your first Tapestry application 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 →