/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.event.impl.jobs.console; import java.io.IOException; import java.io.PrintWriter; import java.net.URLEncoder; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.sling.api.request.ResponseUtil; import org.apache.sling.discovery.InstanceDescription; import org.apache.sling.event.impl.jobs.JobConsumerManager; import org.apache.sling.event.impl.jobs.config.InternalQueueConfiguration; import org.apache.sling.event.impl.jobs.config.JobManagerConfiguration; import org.apache.sling.event.impl.jobs.config.TopologyCapabilities; import org.apache.sling.event.jobs.Job; import org.apache.sling.event.jobs.JobManager; import org.apache.sling.event.jobs.Queue; import org.apache.sling.event.jobs.QueueConfiguration; import org.apache.sling.event.jobs.ScheduleInfo; import org.apache.sling.event.jobs.ScheduledJobInfo; import org.apache.sling.event.jobs.Statistics; import org.apache.sling.event.jobs.TopicStatistics; import org.apache.sling.event.jobs.consumer.JobConsumer; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a web console plugin displaying the active queues, some statistics * and the configurations. * @since 3.0 */ @Component(service={javax.servlet.Servlet.class, JobConsumer.class}, property = { Constants.SERVICE_VENDOR + "=The Apache Software Foundation", "felix.webconsole.label=slingevent", "felix.webconsole.title=Jobs", "felix.webconsole.category=SLING", JobConsumer.PROPERTY_TOPICS + "=sling/webconsole/test" }) public class WebConsolePlugin extends HttpServlet implements JobConsumer { private static final String SLING_WEBCONSOLE_TEST_JOB_TOPIC = "sling/webconsole/test"; private static final long serialVersionUID = -6983227434841706385L; private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Reference private JobManager jobManager; @Reference private JobManagerConfiguration configuration; @Reference private JobConsumerManager jobConsumerManager; private static final String PAR_QUEUE = "queue"; private Queue getQueue(final HttpServletRequest req) { final String name = req.getParameter(PAR_QUEUE); if ( name != null ) { for(final Queue q : this.jobManager.getQueues()) { if ( name.equals(q.getName()) ) { return q; } } } return null; } private String getQueueErrorMessage(final HttpServletRequest req, final String command) { final String name = req.getParameter(PAR_QUEUE); if ( name == null || name.length() == 0 ) { return "Queue parameter missing for opertation " + command; } return "Queue with name '" + name + "' not found for operation " + command; } @Override protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { String msg = null; final String cmd = req.getParameter("action"); if ( "suspend".equals(cmd) ) { final Queue q = this.getQueue(req); if ( q != null ) { q.suspend(); } else { msg = this.getQueueErrorMessage(req, "suspend"); } } else if ( "resume".equals(cmd) ) { final Queue q = this.getQueue(req); if ( q != null ) { q.resume(); } else { msg = this.getQueueErrorMessage(req, "resume"); } } else if ( "reset".equals(cmd) ) { if ( req.getParameter(PAR_QUEUE) == null || req.getParameter(PAR_QUEUE).length() == 0 ) { this.jobManager.getStatistics().reset(); } else { final Queue q = this.getQueue(req); if ( q != null ) { q.getStatistics().reset(); } else { msg = this.getQueueErrorMessage(req, "reset"); } } } else if ( "test".equals(cmd) ) { this.startTestJob(); } else if ( "dropall".equals(cmd) ) { final Queue q = this.getQueue(req); if ( q != null ) { q.removeAll(); } else { msg = this.getQueueErrorMessage(req, "drop all"); } } else { msg = "Unknown command"; } final String path = req.getContextPath() + req.getServletPath() + req.getPathInfo(); final String redirectTo; if ( msg == null ) { redirectTo = path; } else { redirectTo = path + "?message=" + URLEncoder.encode(msg, "UTF-8"); } resp.sendRedirect(resp.encodeRedirectURL(redirectTo)); } private void startTestJob() { logger.info("Adding test job: {}", SLING_WEBCONSOLE_TEST_JOB_TOPIC); this.jobManager.addJob(SLING_WEBCONSOLE_TEST_JOB_TOPIC, null); } @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws ServletException, IOException { final String msg = req.getParameter("message"); final PrintWriter pw = res.getWriter(); pw.println("<form method='POST' name='eventingcmd'>" + "<input type='hidden' name='action' value=''/>"+ "<input type='hidden' name='queue' value=''/>" + "</form>"); pw.println("<script type='text/javascript'>"); pw.println("function eventingsubmit(action, queue) {" + " document.forms['eventingcmd'].action.value = action;" + " document.forms['eventingcmd'].queue.value = queue;" + " document.forms['eventingcmd'].submit();" + "} </script>"); pw.printf("<p class='statline ui-state-highlight'>Apache Sling Job Handling%s%n</p>", msg != null ? " : " + ResponseUtil.escapeXml(msg) : ""); pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>"); pw.println("<span style='float: left; margin-left: 1em'>Apache Sling Job Handling: Overall Statistics</span>"); this.printForm(pw, null, "Reset Stats", "reset"); pw.println("</div>"); pw.println("<table class='nicetable'><tbody>"); String topics = this.jobConsumerManager.getTopics(); if ( topics == null ) { topics = ""; } else { final String[] allTopics = topics.split(","); final StringBuilder sb = new StringBuilder(); boolean first = true; for(final String t : allTopics) { if ( first) { first = false; } else { sb.append("<br/>"); } sb.append(ResponseUtil.escapeXml(t)); } topics = sb.toString(); } Statistics s = this.jobManager.getStatistics(); pw.printf("<tr><td>Start Time</td><td>%s</td></tr>", formatDate(s.getStartTime())); pw.printf("<tr><td>Local topic consumers: </td><td>%s</td></tr>", topics); pw.printf("<tr><td>Last Activated</td><td>%s</td></tr>", formatDate(s.getLastActivatedJobTime())); pw.printf("<tr><td>Last Finished</td><td>%s</td></tr>", formatDate(s.getLastFinishedJobTime())); pw.printf("<tr><td>Queued Jobs</td><td>%s</td></tr>", s.getNumberOfQueuedJobs()); pw.printf("<tr><td>Active Jobs</td><td>%s</td></tr>", s.getNumberOfActiveJobs()); pw.printf("<tr><td>Jobs</td><td>%s</td></tr>", s.getNumberOfJobs()); pw.printf("<tr><td>Finished Jobs</td><td>%s</td></tr>", s.getNumberOfFinishedJobs()); pw.printf("<tr><td>Failed Jobs</td><td>%s</td></tr>", s.getNumberOfFailedJobs()); pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td></tr>", s.getNumberOfCancelledJobs()); pw.printf("<tr><td>Processed Jobs</td><td>%s</td></tr>", s.getNumberOfProcessedJobs()); pw.printf("<tr><td>Average Processing Time</td><td>%s</td></tr>", formatTime(s.getAverageProcessingTime())); pw.printf("<tr><td>Average Waiting Time</td><td>%s</td></tr>", formatTime(s.getAverageWaitingTime())); pw.println("</tbody></table>"); pw.println("<br/>"); pw.println("<table class='nicetable'><tbody>"); pw.println("<tr><th colspan='2'>Topology Capabilities</th></tr>"); final TopologyCapabilities cap = this.configuration.getTopologyCapabilities(); if ( cap == null ) { pw.print("<tr><td colspan='2'>No topology information available !</td></tr>"); } else { final Map<String, List<InstanceDescription>> instanceCaps = cap.getInstanceCapabilities(); for(final Map.Entry<String, List<InstanceDescription>> entry : instanceCaps.entrySet()) { final StringBuilder sb = new StringBuilder(); for(final InstanceDescription id : entry.getValue()) { if ( sb.length() > 0 ) { sb.append("<br/>"); } if ( id.isLocal() ) { sb.append("<b>local</b>"); } else { sb.append(ResponseUtil.escapeXml(id.getSlingId())); } } pw.printf("<tr><td>%s</td><td>%s</td></tr>", ResponseUtil.escapeXml(entry.getKey()), sb.toString()); } } pw.println("</tbody></table>"); pw.println("<br/>"); pw.println("<p class='statline'>Scheduled Jobs</p>"); pw.println("<table class='nicetable'><tbody>"); final Collection<ScheduledJobInfo> infos = this.jobManager.getScheduledJobs(); if ( infos.size() == 0 ) { pw.print("<tr><td colspan='5'>No jobs currently scheduled.</td></tr>"); } else { pw.println("<tr><th>Schedule</th><th>Job Topic</th><th>Schedules</th></tr>"); int index = 1; for(final ScheduledJobInfo info : infos) { pw.printf("<tr><td><b>%s</b></td><td>%s</td><td>", String.valueOf(index), ResponseUtil.escapeXml(info.getJobTopic())); boolean first = true; for(final ScheduleInfo si : info.getSchedules() ) { if ( !first ) { pw.print("<br/>"); } first = false; switch ( si.getType() ) { case YEARLY : pw.printf("YEARLY %s %s : %s:%s", si.getMonthOfYear(), si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour()); break; case MONTHLY : pw.printf("MONTHLY %s : %s:%s", si.getDayOfMonth(), si.getHourOfDay(), si.getMinuteOfHour()); break; case WEEKLY : pw.printf("WEEKLY %s : %s:%s", si.getDayOfWeek(), si.getHourOfDay(), si.getMinuteOfHour()); break; case DAILY : pw.printf("DAILY %s:%s", si.getHourOfDay(), si.getMinuteOfHour()); break; case HOURLY : pw.printf("HOURLY %s", si.getMinuteOfHour()); break; case CRON : pw.printf("CRON %s", ResponseUtil.escapeXml(si.getExpression())); break; default : pw.printf("AT %s", si.getAt()); } } pw.print("</td></tr>"); index++; } } pw.println("</tbody></table>"); pw.println("<br/>"); boolean isEmpty = true; for(final Queue q : this.jobManager.getQueues()) { isEmpty = false; final String queueName = q.getName(); pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>"); pw.printf("<span style='float: left; margin-left: 1em'>Active JobQueue: %s %s</span>", ResponseUtil.escapeXml(queueName), q.isSuspended() ? "(SUSPENDED)" : ""); this.printForm(pw, queueName, "Reset Stats", "reset"); if ( q.isSuspended() ) { this.printForm(pw, queueName, "Resume", "resume"); } else { this.printForm(pw, queueName, "Suspend", "suspend"); } this.printForm(pw, queueName, "Test", "test"); this.printForm(pw, queueName, "Drop All", "dropall"); pw.println("</div>"); pw.println("<table class='nicetable'><tbody>"); s = q.getStatistics(); final QueueConfiguration c = q.getConfiguration(); pw.println("<tr><th colspan='2'>Statistics</th><th colspan='2'>Configuration</th></tr>"); pw.printf("<tr><td>Start Time</td><td>%s</td><td>Type</td><td>%s</td></tr>", formatDate(s.getStartTime()), formatType(c.getType())); pw.printf("<tr><td>Last Activated</td><td>%s</td><td>Topics</td><td>%s</td></tr>", formatDate(s.getLastActivatedJobTime()), formatArray(c.getTopics())); pw.printf("<tr><td>Last Finished</td><td>%s</td><td>Max Parallel</td><td>%s</td></tr>", formatDate(s.getLastFinishedJobTime()), c.getMaxParallel()); pw.printf("<tr><td>Queued Jobs</td><td>%s</td><td>Max Retries</td><td>%s</td></tr>", s.getNumberOfQueuedJobs(), c.getMaxRetries()); pw.printf("<tr><td>Active Jobs</td><td>%s</td><td>Retry Delay</td><td>%s ms</td></tr>", s.getNumberOfActiveJobs(), c.getRetryDelayInMs()); pw.printf("<tr><td>Jobs</td><td>%s</td><td>Priority</td><td>%s</td></tr>", s.getNumberOfJobs(), c.getThreadPriority()); pw.printf("<tr><td>Finished Jobs</td><td>%s</td><td colspan='2'> </td></tr>", s.getNumberOfFinishedJobs()); pw.printf("<tr><td>Failed Jobs</td><td>%s</td><td colspan='2'> </td></tr>", s.getNumberOfFailedJobs()); pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td><td colspan='2'> </td></tr>", s.getNumberOfCancelledJobs()); pw.printf("<tr><td>Processed Jobs</td><td>%s</td><td colspan='2'> </td></tr>", s.getNumberOfProcessedJobs()); pw.printf("<tr><td>Average Processing Time</td><td>%s</td><td colspan='2'> </td></tr>", formatTime(s.getAverageProcessingTime())); pw.printf("<tr><td>Average Waiting Time</td><td>%s</td><td colspan='2'> </td></tr>", formatTime(s.getAverageWaitingTime())); pw.printf("<tr><td>Status Info</td><td colspan='3'>%s</td></tr>", ResponseUtil.escapeXml(q.getStateInfo())); pw.println("</tbody></table>"); pw.println("<br/>"); } if ( isEmpty ) { pw.println("<p>No active queues.</p>"); pw.println("<br/>"); } for(final TopicStatistics ts : this.jobManager.getTopicStatistics()) { pw.println("<table class='nicetable'><tbody>"); pw.printf("<tr><th colspan='2'>Topic Statistics: %s</th></tr>", ResponseUtil.escapeXml(ts.getTopic())); pw.printf("<tr><td>Last Activated</td><td>%s</td></tr>", formatDate(ts.getLastActivatedJobTime())); pw.printf("<tr><td>Last Finished</td><td>%s</td></tr>", formatDate(ts.getLastFinishedJobTime())); pw.printf("<tr><td>Finished Jobs</td><td>%s</td></tr>", ts.getNumberOfFinishedJobs()); pw.printf("<tr><td>Failed Jobs</td><td>%s</td></tr>", ts.getNumberOfFailedJobs()); pw.printf("<tr><td>Cancelled Jobs</td><td>%s</td></tr>", ts.getNumberOfCancelledJobs()); pw.printf("<tr><td>Processed Jobs</td><td>%s</td></tr>", ts.getNumberOfProcessedJobs()); pw.printf("<tr><td>Average Processing Time</td><td>%s</td></tr>", formatTime(ts.getAverageProcessingTime())); pw.printf("<tr><td>Average Waiting Time</td><td>%s</td></tr>", formatTime(ts.getAverageWaitingTime())); pw.println("</tbody></table>"); pw.println("<br/>"); } pw.println("<p class='statline'>Apache Sling Job Handling - Job Queue Configurations</p>"); this.printQueueConfiguration(req, pw, this.configuration.getQueueConfigurationManager().getMainQueueConfiguration()); final InternalQueueConfiguration[] configs = this.configuration.getQueueConfigurationManager().getConfigurations(); for(final InternalQueueConfiguration c : configs ) { this.printQueueConfiguration(req, pw, c); } } private void printQueueConfiguration(final HttpServletRequest req, final PrintWriter pw, final InternalQueueConfiguration c) { pw.println("<div class='ui-widget-header ui-corner-top buttonGroup'>"); pw.printf("<span style='float: left; margin-left: 1em'>Job Queue Configuration: %s</span>%n", ResponseUtil.escapeXml(c.getName())); pw.printf("<button id='edit' class='ui-state-default ui-corner-all' onclick='javascript:window.location=\"%s%s/configMgr/%s\";'>Edit</button>", req.getContextPath(), req.getServletPath(), c.getPid()); this.printForm(pw, c.getName(), "Test", "test"); pw.println("</div>"); pw.println("<table class='nicetable'><tbody>"); pw.println("<tr><th colspan='2'>Configuration</th></tr>"); pw.printf("<tr><td>Valid</td><td>%s</td></tr>", c.isValid()); pw.printf("<tr><td>Type</td><td>%s</td></tr>", formatType(c.getType())); pw.printf("<tr><td>Topics</td><td>%s</td></tr>", formatArray(c.getTopics())); pw.printf("<tr><td>Max Parallel</td><td>%s</td></tr>", c.getMaxParallel()); pw.printf("<tr><td>Max Retries</td><td>%s</td></tr>", c.getMaxRetries()); pw.printf("<tr><td>Retry Delay</td><td>%s ms</td></tr>", c.getRetryDelayInMs()); pw.printf("<tr><td>Priority</td><td>%s</td></tr>", c.getThreadPriority()); pw.printf("<tr><td>Ranking</td><td>%s</td></tr>", c.getRanking()); pw.println("</tbody></table>"); pw.println("<br/>"); } /** * Format an array for html rendering. */ private String formatArray(final String[] array) { if ( array == null || array.length == 0 ) { return ""; } final StringBuilder sb = new StringBuilder(); boolean first = true; for(final String s : array ) { if ( !first ) { sb.append('\n'); } first = false; sb.append(s); } return ResponseUtil.escapeXml(sb.toString()); } private String formatType(final QueueConfiguration.Type type) { switch ( type ) { case ORDERED : return "Ordered"; case TOPIC_ROUND_ROBIN : return "Topic Round Robin"; case UNORDERED : return "Parallel"; } return type.toString(); } /** Default date format used. */ private final DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss:SSS yyyy-MMM-dd"); /** * Format a date */ private synchronized String formatDate(final long time) { if ( time == -1 ) { return "-"; } final Date d = new Date(time); return dateFormat.format(d); } /** * Format time (= duration) */ private String formatTime(final long time) { if ( time == 0 ) { return "-"; } if ( time < 1000 ) { return time + " ms"; } else if ( time < 1000 * 60 ) { return time / 1000 + " secs"; } final long min = time / 1000 / 60; final long secs = (time - min * 1000 * 60); return min + " min " + secs / 1000 + " secs"; } private void printForm(final PrintWriter pw, final String qeueName, final String buttonLabel, final String cmd) { pw.printf("<button class='ui-state-default ui-corner-all' onclick='javascript:eventingsubmit(\"%s\", \"%s\");'>" + "%s</button>", ResponseUtil.escapeXml(cmd), (qeueName != null ? ResponseUtil.escapeXml(qeueName) : ""), ResponseUtil.escapeXml(buttonLabel)); } @Override public JobResult process(final Job job) { logger.info("Received test job {}", job.getTopic()); return JobResult.OK; } }