tutorial

How to Monitor Apache Struts Application Health with Vigilmon

Apache Struts action dispatch failures, interceptor chain misconfigurations, and OGNL expression errors are silent until users report broken forms or missing pages. Learn how to monitor Struts action availability, interceptor stack health, and batch job liveness with Vigilmon HTTP probes and heartbeat monitors.

Apache Struts is a mature Java MVC framework that powers enterprise web applications through action-based request dispatching, interceptor chains, and OGNL-driven view rendering. Struts handles every form submission and navigation request — when an interceptor chain is misconfigured after a deployment, protected actions silently become accessible to unauthenticated users; when OGNL expression compilation fails during result rendering, Struts returns a generic 500 that gives no indication of which action triggered the failure; when the action dispatcher cannot load a class after a partial deployment, requests to that action hang until they time out. These are production incidents disguised as application errors.

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


Why Struts Needs External Monitoring

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

  • Interceptor chain misconfiguration: a bad struts.xml merge after a deployment can silently remove authentication or authorization interceptors from a stack, opening protected actions to unauthenticated requests — the application returns 200 for actions that should return 403
  • OGNL expression failures: Struts resolves view data through OGNL expressions at render time; a null value stack entry or a missing action property causes a NoSuchPropertyException that manifests as a blank field or a generic 500 with no upstream indication
  • Result type misconfiguration: Struts result types are resolved at action completion; a missing result definition causes Struts to return a MissingAction exception that looks like a 404 but is actually a configuration failure
  • Convention plugin silent failures: the Struts Convention plugin scans for action classes at startup; scan failures leave action mappings missing without any startup error, causing 404s for URLs that worked before the deployment
  • File upload silent truncation: Struts's multipart upload handling has a configurable size limit; requests that exceed it are silently discarded at the interceptor layer, causing form submissions to succeed at HTTP level while the file payload is lost
  • Session scope action bean leaks: Struts session-scoped action beans accumulate in the HTTP session; a session store near capacity causes errors that appear as authentication failures rather than infrastructure failures

External monitoring with Vigilmon adds:

  • Proactive alerting when action health endpoints report interceptor or dispatcher failures
  • Action dispatch availability monitoring independent of the servlet container state
  • Heartbeat monitoring so you know when batch processing or scheduled action invocations stop completing
  • Multi-region probe consensus that filters transient blips from genuine Struts application failures

Step 1: Build a Struts Health Endpoint

Struts does not ship with a built-in health endpoint. Add one as a dedicated health action or via Spring Boot Actuator in hybrid deployments.

Dedicated Struts Health Action

// action/HealthAction.java
package com.yourapp.action;

import com.opensymphony.xwork2.ActionSupport;
import com.yourapp.service.DatabaseService;
import com.yourapp.service.CacheService;
import org.apache.struts2.convention.annotation.*;
import org.apache.struts2.interceptor.ServletResponseAware;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@Namespace("/")
@Action(value = "health",
        results = {@Result(name = "ok",   type = "json"),
                   @Result(name = "down", type = "json")})
public class HealthAction extends ActionSupport implements ServletResponseAware {

    private DatabaseService databaseService;
    private CacheService cacheService;
    private HttpServletResponse response;
    private Map<String, Object> healthData;

    public void setDatabaseService(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }

    public void setCacheService(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    @Override
    public void setServletResponse(HttpServletResponse response) {
        this.response = response;
    }

    @Override
    public String execute() throws Exception {
        healthData = new LinkedHashMap<>();
        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; }
        }

        healthData.put("status", anyFailed ? "degraded" : "ok");
        healthData.put("checks", checks);

        response.setStatus(anyFailed ? 503 : 200);
        return anyFailed ? "down" : "ok";
    }

    public Map<String, Object> getHealthData() { return healthData; }
}

Spring Boot Actuator Health Indicator (Struts + Spring Boot)

// StrutsHealthIndicator.java
import org.apache.struts2.dispatcher.Dispatcher;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class StrutsHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        try {
            Dispatcher dispatcher = Dispatcher.getInstance();
            if (dispatcher == null) {
                return Health.down()
                    .withDetail("reason", "struts_dispatcher_null")
                    .build();
            }

            boolean initialized = dispatcher.getContainer() != null;
            if (!initialized) {
                return Health.down()
                    .withDetail("reason", "container_not_initialized")
                    .build();
            }

            return Health.up()
                .withDetail("dispatcher", dispatcher.getClass().getSimpleName())
                .withDetail("container", "initialized")
                .build();
        } catch (Exception e) {
            return Health.down()
                .withDetail("reason", "dispatcher_unavailable")
                .withDetail("error", e.getMessage())
                .build();
        }
    }
}

This exposes /actuator/health/struts automatically via Spring Boot Actuator.

Node.js Sidecar

// health/struts.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.STRUTS_HEALTH_PATH || '/health';

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

Python Health Sidecar

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

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

Step 2: Configure Vigilmon HTTP Monitor for Struts

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your Struts health action: 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" or "status":"UP"
    • Response time threshold: 3000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. Save the monitor

Add a second monitor for the Struts dispatcher check:

  • URL: https://your-app.example.com/actuator/health/struts
  • Expected: 200, body contains: "status":"UP"
  • Interval: 2 minutes
  • Alert channel: P1 pager — dispatcher failure means all action-based requests are broken

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


Step 3: Heartbeat Monitoring for Batch Action Execution

Health endpoint monitoring catches dispatcher and interceptor failures, but not end-to-end batch processing problems. A Struts dispatcher can report healthy while a scheduled batch action has been silently failing to process records due to an OGNL value stack exception on a specific input.

Vigilmon heartbeat monitors catch these: your batch job pings Vigilmon after each successful run.

Set Up the Heartbeat Monitor

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: struts-batch-processor
  3. Set the expected interval: 10 minutes
  4. Set the grace period: 20 minutes
  5. Save — copy the heartbeat URL, e.g. https://vigilmon.online/heartbeat/abc123xyz

Wire It Into a Struts Batch Processing Job

// service/BatchProcessorService.java
package com.yourapp.service;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.net.http.*;
import java.net.URI;

@Service
public class BatchProcessorService {

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

    @Scheduled(fixedDelay = 600_000)
    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 Quartz-based Struts jobs, add the heartbeat ping at the end of Job.execute() before the method returns, and suppress the ping on exceptions so Vigilmon alerts on missed heartbeats.


Step 4: Alert Routing for Struts Failures

Struts failures range from a single action being misconfigured to the entire dispatcher being uninitialized. Route alerts by severity:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: /health action check | Slack + PagerDuty | P1 | | HTTP: /actuator/health/struts | Slack + PagerDuty | P1 | | Heartbeat: batch processor | Slack + PagerDuty | P1 | | Heartbeat: scheduled report action | Slack + email | P2 |

Set response time thresholds as early warning signals:

  • Alert at 800ms for the Struts health action (slow responses indicate database or service dependency latency)
  • Alert at 2000ms for the dispatcher check (Struts dispatcher slowdowns precede full action dispatch failures)
  • Alert at 15 minutes gap in batch processor heartbeat during business hours

For production applications, set up a status page in Vigilmon showing action availability and batch job liveness — this lets on-call engineers identify Struts application failures instantly rather than inferring them from user reports of broken forms or missing pages.


Summary

Struts failures present as application errors, not infrastructure failures — users see broken forms or 500 pages while the real cause is an interceptor chain misconfiguration or an OGNL expression failure on a specific value stack entry. External monitoring catches these before they become an incident:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /health | Database, cache, and action dependency health | | HTTP monitor on /actuator/health/struts | Dispatcher initialization, container state | | Heartbeat monitor (batch processor) | End-to-end batch action execution success | | Heartbeat monitor (scheduled reports) | Scheduled action invocation liveness |

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