Files
eapim-admin/WebContent/quartzStatus.jsp
T
Rinjae ba1161be7b Quartz 스케줄러 상태 JSP 추가
- Quartz 스케줄러 상태 표시용 quartzStatus.jsp 신규 작성
- 스케줄러/잡/트리거 정보 표시 및 자동 새로고침 기능 구현
2026-08-06 16:57:39 +09:00

344 lines
18 KiB
Plaintext

<%@ 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 %>">&#9654;</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 %>">&#9654;</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>