Quartz 스케줄러 상태 JSP 추가
- Quartz 스케줄러 상태 표시용 quartzStatus.jsp 신규 작성 - 스케줄러/잡/트리거 정보 표시 및 자동 새로고침 기능 구현
This commit is contained in:
@@ -0,0 +1,343 @@
|
|||||||
|
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %><%@
|
||||||
|
page import="org.quartz.*,
|
||||||
|
org.quartz.impl.matchers.GroupMatcher,
|
||||||
|
org.springframework.web.context.WebApplicationContext,
|
||||||
|
org.springframework.web.context.support.WebApplicationContextUtils,
|
||||||
|
java.util.*,
|
||||||
|
java.text.SimpleDateFormat"
|
||||||
|
%><%!
|
||||||
|
private String fmt(Date date) {
|
||||||
|
if (date == null) return "-";
|
||||||
|
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String relative(Date date) {
|
||||||
|
if (date == null) return "";
|
||||||
|
long diff = date.getTime() - System.currentTimeMillis();
|
||||||
|
String prefix = diff >= 0 ? "+" : "-";
|
||||||
|
long abs = Math.abs(diff) / 1000;
|
||||||
|
if (abs < 60) return prefix + abs + "s";
|
||||||
|
if (abs < 3600) return prefix + (abs / 60) + "m " + (abs % 60) + "s";
|
||||||
|
return prefix + (abs / 3600) + "h " + ((abs % 3600) / 60) + "m";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stateClass(Trigger.TriggerState state) {
|
||||||
|
if (state == Trigger.TriggerState.NORMAL) return "state-normal";
|
||||||
|
if (state == Trigger.TriggerState.PAUSED) return "state-paused";
|
||||||
|
if (state == Trigger.TriggerState.BLOCKED) return "state-blocked";
|
||||||
|
if (state == Trigger.TriggerState.ERROR) return "state-error";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildTriggerInfo(Scheduler scheduler, Trigger trigger) throws Exception {
|
||||||
|
Map<String, Object> info = new LinkedHashMap<>();
|
||||||
|
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
|
||||||
|
info.put("triggerName", trigger.getKey().getName());
|
||||||
|
info.put("triggerGroup", trigger.getKey().getGroup());
|
||||||
|
info.put("state", state.name());
|
||||||
|
info.put("stateClass", stateClass(state));
|
||||||
|
info.put("prevFire", fmt(trigger.getPreviousFireTime()));
|
||||||
|
info.put("prevFireRel", relative(trigger.getPreviousFireTime()));
|
||||||
|
info.put("nextFire", fmt(trigger.getNextFireTime()));
|
||||||
|
info.put("nextFireRel", relative(trigger.getNextFireTime()));
|
||||||
|
info.put("priority", trigger.getPriority());
|
||||||
|
if (trigger instanceof CronTrigger) {
|
||||||
|
info.put("type", "Cron");
|
||||||
|
info.put("cronExp", ((CronTrigger) trigger).getCronExpression());
|
||||||
|
} else if (trigger instanceof SimpleTrigger) {
|
||||||
|
SimpleTrigger st = (SimpleTrigger) trigger;
|
||||||
|
info.put("type", "Simple");
|
||||||
|
info.put("cronExp", "interval=" + st.getRepeatInterval() + "ms, repeat=" +
|
||||||
|
(st.getRepeatCount() == SimpleTrigger.REPEAT_INDEFINITELY ? "INF" : st.getRepeatCount()));
|
||||||
|
} else {
|
||||||
|
info.put("type", trigger.getClass().getSimpleName());
|
||||||
|
info.put("cronExp", "-");
|
||||||
|
}
|
||||||
|
if (trigger.getStartTime() != null) info.put("startTime", fmt(trigger.getStartTime()));
|
||||||
|
if (trigger.getEndTime() != null) info.put("endTime", fmt(trigger.getEndTime()));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> getJobList(Scheduler scheduler) throws Exception {
|
||||||
|
List<Map<String, Object>> jobs = new ArrayList<>();
|
||||||
|
for (String group : scheduler.getJobGroupNames()) {
|
||||||
|
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group))) {
|
||||||
|
JobDetail detail = scheduler.getJobDetail(jobKey);
|
||||||
|
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
|
||||||
|
|
||||||
|
Map<String, Object> job = new LinkedHashMap<>();
|
||||||
|
job.put("name", jobKey.getName());
|
||||||
|
job.put("group", jobKey.getGroup());
|
||||||
|
job.put("jobClass", detail.getJobClass().getSimpleName());
|
||||||
|
job.put("disallowConcurrent", detail.isConcurrentExectionDisallowed());
|
||||||
|
|
||||||
|
StringBuilder dataStr = new StringBuilder();
|
||||||
|
for (String key : detail.getJobDataMap().getKeys()) {
|
||||||
|
if (dataStr.length() > 0) dataStr.append(", ");
|
||||||
|
dataStr.append(key).append("=").append(detail.getJobDataMap().getString(key));
|
||||||
|
}
|
||||||
|
job.put("jobData", dataStr.toString());
|
||||||
|
job.put("triggerCount", triggers.size());
|
||||||
|
|
||||||
|
List<Map<String, Object>> triggerList = new ArrayList<>();
|
||||||
|
for (Trigger trigger : triggers) {
|
||||||
|
triggerList.add(buildTriggerInfo(scheduler, trigger));
|
||||||
|
}
|
||||||
|
job.put("triggerList", triggerList);
|
||||||
|
|
||||||
|
if (!triggers.isEmpty()) {
|
||||||
|
Trigger trigger = triggers.get(0);
|
||||||
|
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
|
||||||
|
job.put("state", state.name());
|
||||||
|
job.put("stateClass", stateClass(state));
|
||||||
|
job.put("prevFire", fmt(trigger.getPreviousFireTime()));
|
||||||
|
job.put("prevFireRel", relative(trigger.getPreviousFireTime()));
|
||||||
|
job.put("nextFire", fmt(trigger.getNextFireTime()));
|
||||||
|
job.put("nextFireRel", relative(trigger.getNextFireTime()));
|
||||||
|
if (trigger instanceof CronTrigger) {
|
||||||
|
job.put("cronExp", ((CronTrigger) trigger).getCronExpression());
|
||||||
|
} else {
|
||||||
|
job.put("cronExp", "-");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job.put("state", "NO_TRIGGER");
|
||||||
|
job.put("stateClass", "state-error");
|
||||||
|
job.put("prevFire", "-");
|
||||||
|
job.put("prevFireRel", "");
|
||||||
|
job.put("nextFire", "-");
|
||||||
|
job.put("nextFireRel", "");
|
||||||
|
job.put("cronExp", "-");
|
||||||
|
}
|
||||||
|
jobs.add(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs.sort(Comparator.comparing(m -> (String) m.get("name")));
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
%><%
|
||||||
|
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(application);
|
||||||
|
|
||||||
|
Scheduler scheduler = null;
|
||||||
|
Scheduler clusteredScheduler = null;
|
||||||
|
String error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
scheduler = (Scheduler) ctx.getBean("scheduler");
|
||||||
|
} catch (Exception e) {
|
||||||
|
error = "scheduler bean not found: " + e.getMessage();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
clusteredScheduler = (Scheduler) ctx.getBean("clusteredScheduler");
|
||||||
|
} catch (Exception e) {
|
||||||
|
// clusteredScheduler is optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자동 새로고침 간격 (초), 기본 10초
|
||||||
|
String refreshParam = request.getParameter("refresh");
|
||||||
|
int refreshSec = 10;
|
||||||
|
if (refreshParam != null) {
|
||||||
|
try { refreshSec = Integer.parseInt(refreshParam); } catch (NumberFormatException e) { /* ignore */ }
|
||||||
|
if (refreshSec < 1) refreshSec = 1;
|
||||||
|
}
|
||||||
|
boolean autoRefresh = refreshParam != null;
|
||||||
|
%>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<% if (autoRefresh) { %>
|
||||||
|
<meta http-equiv="refresh" content="<%= refreshSec %>;url=quartzStatus.jsp?refresh=<%= refreshSec %>">
|
||||||
|
<% } %>
|
||||||
|
<title>Quartz Scheduler Status</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; background: #f5f5f5; color: #333; }
|
||||||
|
h1 { font-size: 20px; margin-bottom: 5px; }
|
||||||
|
h2 { font-size: 16px; color: #555; margin: 20px 0 8px 0; border-bottom: 2px solid #ddd; padding-bottom: 4px; }
|
||||||
|
.info { font-size: 13px; color: #666; margin-bottom: 15px; }
|
||||||
|
table { width: 100%%; border-collapse: collapse; font-size: 13px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||||
|
th { background: #4a5568; color: #fff; padding: 8px 10px; text-align: left; font-weight: 500; }
|
||||||
|
td { padding: 7px 10px; border-bottom: 1px solid #eee; }
|
||||||
|
tr:hover td { background: #f7fafc; }
|
||||||
|
.state-normal { color: #38a169; font-weight: bold; }
|
||||||
|
.state-paused { color: #d69e2e; font-weight: bold; }
|
||||||
|
.state-blocked { color: #e53e3e; font-weight: bold; }
|
||||||
|
.state-error { color: #e53e3e; font-weight: bold; }
|
||||||
|
.mono { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; }
|
||||||
|
.rel { color: #718096; font-size: 11px; margin-left: 4px; }
|
||||||
|
.badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 11px; }
|
||||||
|
.badge-yes { background: #c6f6d5; color: #276749; }
|
||||||
|
.badge-no { background: #fed7d7; color: #9b2c2c; }
|
||||||
|
.scheduler-meta { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 10px; }
|
||||||
|
.scheduler-meta span { font-size: 13px; }
|
||||||
|
.scheduler-meta strong { color: #4a5568; }
|
||||||
|
.error { color: #e53e3e; padding: 10px; background: #fff5f5; border: 1px solid #fc8181; border-radius: 4px; }
|
||||||
|
.toolbar { font-size: 12px; color: #718096; }
|
||||||
|
.toolbar a { color: #4299e1; text-decoration: none; margin: 0 4px; }
|
||||||
|
.toolbar a:hover { text-decoration: underline; }
|
||||||
|
.toolbar .active { font-weight: bold; color: #2b6cb0; background: #ebf4ff; padding: 2px 6px; border-radius: 3px; }
|
||||||
|
.auto-badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; background: #bee3f8; color: #2a4365; margin-left: 6px; }
|
||||||
|
.trigger-toggle { cursor: pointer; user-select: none; }
|
||||||
|
.trigger-toggle:hover td { background: #edf2f7; }
|
||||||
|
.trigger-row { display: none; }
|
||||||
|
.trigger-row.open { display: table-row; }
|
||||||
|
.trigger-row td { background: #f7fafc; padding: 5px 10px 5px 30px; border-bottom: 1px solid #e2e8f0; font-size: 12px; }
|
||||||
|
.trigger-detail-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 0; }
|
||||||
|
.trigger-detail-table th { background: #e2e8f0; color: #4a5568; padding: 5px 8px; font-weight: 500; font-size: 11px; }
|
||||||
|
.trigger-detail-table td { padding: 4px 8px; border-bottom: 1px solid #e2e8f0; }
|
||||||
|
.trigger-detail-table tr:last-child td { border-bottom: none; }
|
||||||
|
.toggle-icon { display: inline-block; width: 16px; font-size: 11px; color: #718096; transition: transform 0.15s; }
|
||||||
|
.toggle-icon.open { transform: rotate(90deg); }
|
||||||
|
.trigger-count { font-size: 11px; color: #718096; font-weight: normal; margin-left: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Quartz Scheduler Status
|
||||||
|
<% if (autoRefresh) { %><span class="auto-badge">Auto <%= refreshSec %>s</span><% } %>
|
||||||
|
</h1>
|
||||||
|
<p class="info">
|
||||||
|
Instance: <strong><%= System.getProperty("inst.Name", "N/A") %></strong>
|
||||||
|
| Server Time: <strong><%= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) %></strong>
|
||||||
|
</p>
|
||||||
|
<p class="toolbar">
|
||||||
|
Auto Refresh:
|
||||||
|
<% if (!autoRefresh) { %><span class="active">OFF</span><% } else { %><a href="quartzStatus.jsp">OFF</a><% } %>
|
||||||
|
|
|
||||||
|
<% int[] intervals = {5, 10, 30, 60}; %>
|
||||||
|
<% for (int iv : intervals) { %>
|
||||||
|
<% if (autoRefresh && refreshSec == iv) { %><span class="active"><%= iv %>s</span><% } else { %><a href="quartzStatus.jsp?refresh=<%= iv %>"><%= iv %>s</a><% } %>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if (error != null) { %>
|
||||||
|
<div class="error"><%= error %></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (scheduler != null) { %>
|
||||||
|
<h2>Scheduler: <%= scheduler.getSchedulerName() %> (RAM)</h2>
|
||||||
|
<div class="scheduler-meta">
|
||||||
|
<span><strong>Status:</strong> <span class="<%= scheduler.isStarted() ? "state-normal" : "state-error" %>"><%= scheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
|
||||||
|
<span><strong>Instance ID:</strong> <%= scheduler.getSchedulerInstanceId() %></span>
|
||||||
|
<span><strong>Job Count:</strong> <%= getJobList(scheduler).size() %></span>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Job Name</th>
|
||||||
|
<th>Job Class</th>
|
||||||
|
<th>Cron</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Concurrent</th>
|
||||||
|
<th>Prev Fire</th>
|
||||||
|
<th>Next Fire</th>
|
||||||
|
<th>Job Data</th>
|
||||||
|
</tr>
|
||||||
|
<% int jobIdx = 0; for (Map<String, Object> job : getJobList(scheduler)) { String rowId = "s_" + jobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
|
||||||
|
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
|
||||||
|
<td><span class="toggle-icon" id="icon_<%= rowId %>">▶</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
|
||||||
|
<td class="mono"><%= job.get("jobClass") %></td>
|
||||||
|
<td class="mono"><%= job.get("cronExp") %></td>
|
||||||
|
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
|
||||||
|
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
|
||||||
|
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
|
||||||
|
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= job.get("jobData") %></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="trigger-row" id="triggers_<%= rowId %>">
|
||||||
|
<td colspan="8" style="padding: 6px 10px 6px 30px;">
|
||||||
|
<% if (tList.isEmpty()) { %>
|
||||||
|
<em style="color:#a0aec0;">No triggers</em>
|
||||||
|
<% } else { %>
|
||||||
|
<table class="trigger-detail-table">
|
||||||
|
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
|
||||||
|
<% for (Map<String, Object> t : tList) { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= t.get("triggerName") %></td>
|
||||||
|
<td><%= t.get("triggerGroup") %></td>
|
||||||
|
<td><%= t.get("type") %></td>
|
||||||
|
<td class="mono"><%= t.get("cronExp") %></td>
|
||||||
|
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
|
||||||
|
<td><%= t.get("priority") %></td>
|
||||||
|
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% jobIdx++; } %>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (clusteredScheduler != null) { %>
|
||||||
|
<h2>Clustered Scheduler: <%= clusteredScheduler.getSchedulerName() %> (DB)</h2>
|
||||||
|
<div class="scheduler-meta">
|
||||||
|
<span><strong>Status:</strong> <span class="<%= clusteredScheduler.isStarted() ? "state-normal" : "state-error" %>"><%= clusteredScheduler.isStarted() ? "STARTED" : "STOPPED" %></span></span>
|
||||||
|
<span><strong>Instance ID:</strong> <%= clusteredScheduler.getSchedulerInstanceId() %></span>
|
||||||
|
<span><strong>Job Count:</strong> <%= getJobList(clusteredScheduler).size() %></span>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Job Name</th>
|
||||||
|
<th>Job Class</th>
|
||||||
|
<th>Cron</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Concurrent</th>
|
||||||
|
<th>Prev Fire</th>
|
||||||
|
<th>Next Fire</th>
|
||||||
|
<th>Job Data</th>
|
||||||
|
</tr>
|
||||||
|
<% int cJobIdx = 0; for (Map<String, Object> job : getJobList(clusteredScheduler)) { String rowId = "c_" + cJobIdx; List<Map<String, Object>> tList = (List<Map<String, Object>>) job.get("triggerList"); %>
|
||||||
|
<tr class="trigger-toggle" onclick="toggleTriggers('<%= rowId %>')">
|
||||||
|
<td><span class="toggle-icon" id="icon_<%= rowId %>">▶</span><strong><%= job.get("name") %></strong><span class="trigger-count">(<%= job.get("triggerCount") %> triggers)</span></td>
|
||||||
|
<td class="mono"><%= job.get("jobClass") %></td>
|
||||||
|
<td class="mono"><%= job.get("cronExp") %></td>
|
||||||
|
<td class="<%= job.get("stateClass") %>"><%= job.get("state") %></td>
|
||||||
|
<td><span class="badge <%= (Boolean)job.get("disallowConcurrent") ? "badge-yes" : "badge-no" %>">
|
||||||
|
<%= (Boolean)job.get("disallowConcurrent") ? "NO (protected)" : "YES (allowed)" %></span></td>
|
||||||
|
<td class="mono"><%= job.get("prevFire") %><span class="rel"><%= job.get("prevFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= job.get("nextFire") %><span class="rel"><%= job.get("nextFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= job.get("jobData") %></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="trigger-row" id="triggers_<%= rowId %>">
|
||||||
|
<td colspan="8" style="padding: 6px 10px 6px 30px;">
|
||||||
|
<% if (tList.isEmpty()) { %>
|
||||||
|
<em style="color:#a0aec0;">No triggers</em>
|
||||||
|
<% } else { %>
|
||||||
|
<table class="trigger-detail-table">
|
||||||
|
<tr><th>Trigger Name</th><th>Group</th><th>Type</th><th>Expression / Interval</th><th>State</th><th>Priority</th><th>Prev Fire</th><th>Next Fire</th></tr>
|
||||||
|
<% for (Map<String, Object> t : tList) { %>
|
||||||
|
<tr>
|
||||||
|
<td><%= t.get("triggerName") %></td>
|
||||||
|
<td><%= t.get("triggerGroup") %></td>
|
||||||
|
<td><%= t.get("type") %></td>
|
||||||
|
<td class="mono"><%= t.get("cronExp") %></td>
|
||||||
|
<td class="<%= t.get("stateClass") %>"><%= t.get("state") %></td>
|
||||||
|
<td><%= t.get("priority") %></td>
|
||||||
|
<td class="mono"><%= t.get("prevFire") %><span class="rel"><%= t.get("prevFireRel") %></span></td>
|
||||||
|
<td class="mono"><%= t.get("nextFire") %><span class="rel"><%= t.get("nextFireRel") %></span></td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% cJobIdx++; } %>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleTriggers(rowId) {
|
||||||
|
var row = document.getElementById('triggers_' + rowId);
|
||||||
|
var icon = document.getElementById('icon_' + rowId);
|
||||||
|
if (row.classList.contains('open')) {
|
||||||
|
row.classList.remove('open');
|
||||||
|
icon.classList.remove('open');
|
||||||
|
} else {
|
||||||
|
row.classList.add('open');
|
||||||
|
icon.classList.add('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user