Commits

Zemian Deng committed 16408c4

Issue#16: Added EventHistory and updated DataStore API to record events.

  • Participants
  • Parent commits ca75346

Comments (0)

Files changed (14)

File timemachine-hibernate/src/main/java/timemachine/scheduler/hibernate/HibernateDataStore.java

 import timemachine.scheduler.CoreServices;
 import timemachine.scheduler.CoreServicesListener;
 import timemachine.scheduler.Data;
+import timemachine.scheduler.EventHistory;
 import timemachine.scheduler.JobDef;
 import timemachine.scheduler.Schedule;
 import timemachine.scheduler.SchedulerData;
 			}
 		});
 	}
+	
+	// Event History
+	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	@Override
+	public void createEventHistory(EventHistory eventHistory) {
+		create(eventHistory);
+	}
+	@Override
+	public EventHistory getEventHistory(Long schedulerNodeId, Long eventHistoryId) {
+		return get(eventHistoryId, EventHistory.class);
+	}
+	@Override
+	public List<EventHistory> findEventHistories(final Long schedulerNodeId) {
+		return hbmSessionTemplate.withSessionResult(new SessionResultAction() {
+			@Override
+			public Object onSession(Session session) {
+				String query = "select e from EventHistory e where e.schedulerNodeId = :schedulerNodeId " + 
+						" order by e.createTime desc, e.type asc, e.name asc";
+				List<?> list = session.createQuery(query)
+						.setParameter("schedulerNodeId", schedulerNodeId).list();
+				return list;
+			}
+		});
+	}
+	@Override
+	public List<EventHistory> findEventHistories(final Long schedulerNodeId, final Date fromTime, final Date toTime) {
+		return hbmSessionTemplate.withSessionResult(new SessionResultAction() {
+			@Override
+			public Object onSession(Session session) {
+				String query = "select e from EventHistory e where e.schedulerNodeId = :schedulerNodeId " + 
+						" and e.createTime between :fromTime and :toTime " +
+						" order by e.createTime desc, e.type asc, e.name asc";
+				List<?> list = session.createQuery(query)
+						.setParameter("schedulerNodeId", schedulerNodeId)
+						.setParameter("fromTime", fromTime)
+						.setParameter("toTime", toTime).list();
+				return list;
+			}
+		});
+	}
+	@Override
+	public void deleteEventHistories(final Long schedulerNodeId, final Date olderThanTime) {
+		hbmSessionTemplate.withSession(new SessionAction() {
+			@Override
+			public void onSession(Session session) {
+				String query = "delete EventHistory e where e.schedulerNodeId = :schedulerNodeId " + 
+						" and e.createTime < :olderThanTime " +
+						" order by e.createTime desc, e.type asc, e.name asc";
+				int result = session.createQuery(query)
+						.setParameter("schedulerNodeId", schedulerNodeId)
+						.setParameter("olderThanTime", olderThanTime).executeUpdate();
+				logger.debug("Removed {} EventHistory records with olderThanTime={}.", result, olderThanTime);
+			}
+		});
+	}
 }

File timemachine-hibernate/src/main/resources/timemachine/scheduler/hibernate/EventHistory.hbm.xml

+<?xml version="1.0"?>
+<!-- 
+ * Copyright 2012 Zemian Deng
+ * 
+ * Licensed 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.
+-->
+<!DOCTYPE hibernate-mapping PUBLIC
+        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
+        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
+
+<hibernate-mapping>
+
+	<class name="timemachine.scheduler.EventHistory" table="EVENT_HISTORY">
+		<id name="id" column="ID" type="long">
+			<generator class="native" />
+		</id>
+		<property name="schedulerNodeId" type="long" column="SCHEDULER_NODE_ID" access="field" index="EVENT_HISTORY_SCHEDULER_NODE_ID"/>
+		<property name="type" type="string" column="TYPE" access="field" index="EVENT_HISTORY_TYPE" length="511"/>
+		<property name="name" type="string" column="NAME" access="field" index="EVENT_HISTORY_NAME" length="255"/>
+		<property name="createTime" type="timestamp" column="CREATE_TIME" access="field" index="EVENT_HISTORY_CREATE_TIME"/>
+		<property name="info1" type="string" column="INFO1" access="field" length="1023" not-null="false"/>
+		<property name="info2" type="string" column="INFO2" access="field" length="1023" not-null="false"/>
+		<property name="info3" type="string" column="INFO3" access="field" length="1023" not-null="false"/>
+		<property name="info4" type="string" column="INFO4" access="field" length="1023" not-null="false"/>
+		<property name="info5" type="string" column="INFO5" access="field" length="1023" not-null="false"/>
+	</class>
+
+</hibernate-mapping>

File timemachine-hibernate/src/main/resources/timemachine/scheduler/hibernate/hibernate.cfg.xml

         <mapping resource="timemachine/scheduler/hibernate/SchedulerNode.hbm.xml"/>
         <mapping resource="timemachine/scheduler/hibernate/Schedule.hbm.xml"/>
         <mapping resource="timemachine/scheduler/hibernate/JobDef.hbm.xml"/>
+        <mapping resource="timemachine/scheduler/hibernate/EventHistory.hbm.xml"/>
 
     </session-factory>
 

File timemachine-scheduler/src/main/java/timemachine/scheduler/EventHistory.java

+/*
+ * Copyright 2012 Zemian Deng
+ * 
+ * Licensed 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 timemachine.scheduler;
+
+import java.util.Date;
+
+import timemachine.scheduler.support.AbstractData;
+
+/**
+ * A scheduler event data record.
+ * @author Zemian Deng
+ */
+public class EventHistory extends AbstractData {
+	// inherited id and name already.
+	private Long schedulerNodeId;
+	private String type;
+	private Date createTime;
+	private String info1;
+	private String info2;
+	private String info3;
+	private String info4;
+	private String info5;
+	
+	public Long getSchedulerNodeId() {
+		return schedulerNodeId;
+	}
+	public void setSchedulerNodeId(Long schedulerNodeId) {
+		this.schedulerNodeId = schedulerNodeId;
+	}
+	public String getType() {
+		return type;
+	}
+	public void setType(String type) {
+		this.type = type;
+	}
+	public Date getCreateTime() {
+		return createTime;
+	}
+	public void setCreateTime(Date createTime) {
+		this.createTime = createTime;
+	}
+	public String getInfo1() {
+		return info1;
+	}
+	public void setInfo1(String info1) {
+		this.info1 = info1;
+	}
+	public String getInfo2() {
+		return info2;
+	}
+	public void setInfo2(String info2) {
+		this.info2 = info2;
+	}
+	public String getInfo3() {
+		return info3;
+	}
+	public void setInfo3(String info3) {
+		this.info3 = info3;
+	}
+	public String getInfo4() {
+		return info4;
+	}
+	public void setInfo4(String info4) {
+		this.info4 = info4;
+	}
+	public String getInfo5() {
+		return info5;
+	}
+	public void setInfo5(String info5) {
+		this.info5 = info5;
+	}
+}

File timemachine-scheduler/src/main/java/timemachine/scheduler/SchedulerFactory.java

 import timemachine.scheduler.service.ClassLoaderService;
 import timemachine.scheduler.service.ConfigPropsService;
 import timemachine.scheduler.service.DataStore;
+import timemachine.scheduler.service.EventHistoryService;
 import timemachine.scheduler.service.JobListenerNotifier;
 import timemachine.scheduler.service.JobTaskFactory;
 import timemachine.scheduler.service.JobTaskPoolNameResolver;
 			SchedulerNodeService schedulerNodeService = createSchedulerNodeService(systemServiceContainer);
 			systemServiceContainer.addSchedulerNodeService(schedulerNodeService);
 
+			// Add event history service if enable
+			if (!configProps.containsKey("timemachine.scheduler.eventHistory.disable")) {
+				EventHistoryService eventHistoryService = createSystemService("timemachine.scheduler.eventHistory.class",
+						configProps);
+				systemServiceContainer.addService(eventHistoryService);
+			}
+			
 			afterSystemServiceSetup(systemServiceContainer);
 
 			// Create user services

File timemachine-scheduler/src/main/java/timemachine/scheduler/service/DataStore.java

 import java.util.List;
 
 import timemachine.scheduler.Data;
+import timemachine.scheduler.EventHistory;
 import timemachine.scheduler.JobDef;
 import timemachine.scheduler.Schedule;
 import timemachine.scheduler.SchedulerData;
 	public void updateMissedRunSchedules(Long schedulerId, int maxCount, Date fromTime);
 	public Tuple<Schedule, JobDef> getScheduleWithJobDef(Long schedulerId, Long scheduleId, Long jobDefId);
 	public void deleteDeadSchedules(Long schedulerId, int maxCount);
-	
+
+	// Event History
+	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	public void createEventHistory(EventHistory eventHistory);
+	public EventHistory getEventHistory(Long schedulerNodeId, Long eventHistoryId);
+	public List<EventHistory> findEventHistories(Long schedulerNodeId);
+	public List<EventHistory> findEventHistories(Long schedulerNodeId, Date fromTime, Date toTime);
+	public void deleteEventHistories(Long schedulerNodeId, Date olderThanTime);
 }

File timemachine-scheduler/src/main/java/timemachine/scheduler/service/EventHistoryService.java

+package timemachine.scheduler.service;
+
+import java.util.Date;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.apache.commons.lang.exception.ExceptionUtils;
+
+import timemachine.scheduler.ConfigPropsListener;
+import timemachine.scheduler.CoreServices;
+import timemachine.scheduler.CoreServicesListener;
+import timemachine.scheduler.EventHistory;
+import timemachine.scheduler.JobContext;
+import timemachine.scheduler.JobDef;
+import timemachine.scheduler.JobListener;
+import timemachine.scheduler.Schedule;
+import timemachine.scheduler.Scheduler;
+import timemachine.scheduler.SchedulerException;
+import timemachine.scheduler.support.AbstractService;
+import timemachine.scheduler.support.Props;
+
+public class EventHistoryService extends AbstractService implements JobListener, CoreServicesListener, 
+	ConfigPropsListener, SystemService {
+	
+	public static final String REMOVE_INTERVAL_KEY = "timemachine.scheduler.eventHistory.removeInterval";
+	private Long schedulerNodeId;
+	private DataStore dataStore;
+	private long removeInterval; // in millis
+	private Props configProps;
+	private Timer timer;
+	
+	private EventHistory createEventHistory(String type, String name, String ... infos) {
+		if (infos.length > 5)
+			throw new SchedulerException("Only max of 5 infos data is allowed.");
+		
+		EventHistory result = new EventHistory();
+		result.setSchedulerNodeId(schedulerNodeId);
+		result.setCreateTime(new Date());
+		result.setType(type);
+		result.setName(name);
+		if (infos.length >= 1)
+			result.setInfo1(infos[0]);
+		if (infos.length >= 2)
+			result.setInfo1(infos[1]);
+		if (infos.length >= 3)
+			result.setInfo1(infos[2]);
+		if (infos.length >= 4)
+			result.setInfo1(infos[3]);
+		if (infos.length >= 5)
+			result.setInfo1(infos[4]);
+		return result;
+	}
+	
+	@Override
+	protected void initService() {
+		removeInterval = configProps.getLong(REMOVE_INTERVAL_KEY);
+		
+		// Setup timer to remove old histories
+		if (removeInterval > 0) {
+			timer = new Timer(getClass().getSimpleName());
+			TimerTask task = new RemoveEventHistoriesTask();
+			Date startTime = new Date(System.currentTimeMillis() + removeInterval);
+			timer.schedule(task, startTime, removeInterval);
+			logger.debug("Added timer to remove old histories older than {} ms.", removeInterval);
+		}
+		
+		// Start by recording init event.
+		logger.debug("Scheduler is configured to record event histories.");
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "init"));
+	}
+	
+	@Override
+	protected void startService() {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "start"));
+	}
+	
+	@Override
+	protected void stopService() {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "stop"));
+	}
+	
+	@Override
+	protected void destroyService() {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "destroy"));
+	}
+
+	@Override
+	public void onJobDefAdded(JobDef jobDef) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "jobDefAdded", "jobDef.id=" + jobDef.getId()));
+	}
+
+	@Override
+	public void onJobDefDeleted(JobDef jobDef) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "jobDefDeleted", "jobDef.id=" + jobDef.getId()));
+	}
+
+	@Override
+	public void onScheduleAdded(JobDef jobDef, Schedule schedule) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "scheduleAdded", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onScheduleDeleted(JobDef jobDef, Schedule schedule) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "scheduleDeleted", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onSchedulePaused(Schedule schedule) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "schedulePaused", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onScheduleResumed(Schedule schedule) {
+		dataStore.createEventHistory(createEventHistory("SCHEDULER", "scheduleResumed", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onJobRunBefore(JobContext jobContext) {
+		Schedule schedule = jobContext.getSchedule();
+		dataStore.createEventHistory(createEventHistory("JOB", "jobRunBefore", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onJobRunAfter(JobContext jobContext) {
+		Schedule schedule = jobContext.getSchedule();
+		dataStore.createEventHistory(createEventHistory("JOB", "jobRunAfter", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onJobMissedRun(JobDef jobDef, Schedule schedule) {
+		dataStore.createEventHistory(createEventHistory("JOB", "jobMissedRun", "schedule.id=" + schedule.getId()));
+	}
+
+	@Override
+	public void onJobRunException(JobContext jobContext, Exception exception) {
+		Schedule schedule = jobContext.getSchedule();
+		String exceptionMsg = exception.getMessage();
+		String rootCauseMsg = ExceptionUtils.getRootCauseMessage(exception);
+		dataStore.createEventHistory(createEventHistory("JOB", "jobRunException", "schedule.id=" + schedule.getId(), 
+				"msg=" + exceptionMsg, "rootCauseMsg=" + rootCauseMsg));
+	}
+
+	@Override
+	public void onCoreServices(CoreServices coreServices) {
+		this.dataStore = coreServices.getDataStoreService();
+	}
+	
+	@Override
+	public void onScheduler(Scheduler scheduler) {
+		this.schedulerNodeId = scheduler.getSchedulerNode().getId();
+	}
+
+	@Override
+	public void onConfigProps(Props configProps) {
+		this.configProps = configProps;
+	}
+	
+	private class RemoveEventHistoriesTask extends TimerTask {
+		@Override
+		public void run() {
+			long schedulerNodeId  = EventHistoryService.this.schedulerNodeId;
+			DataStore dataStore = EventHistoryService.this.dataStore;
+			long removeInterval = EventHistoryService.this.removeInterval;
+			Date olderThanTime = new Date(System.currentTimeMillis() - removeInterval);
+			
+			logger.debug("Removing event histories older than {}", olderThanTime);
+			dataStore.deleteEventHistories(schedulerNodeId, olderThanTime);
+		}
+	}
+}

File timemachine-scheduler/src/main/java/timemachine/scheduler/service/MemoryDataStore.java

 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import timemachine.scheduler.CoreServices;
 import timemachine.scheduler.CoreServicesListener;
 import timemachine.scheduler.Data;
+import timemachine.scheduler.EventHistory;
 import timemachine.scheduler.JobDef;
 import timemachine.scheduler.Schedule;
 import timemachine.scheduler.Schedule.State;
 		private Map<Long, JobDef> jobDefs = new HashMap<Long, JobDef>();
 		private Map<Long, Schedule> schedules = new HashMap<Long, Schedule>();
 		private Set<Schedule> sortedSchedules = new TreeSet<Schedule>(new ScheduleComparator());
+		private Map<Long, EventHistory> eventHistories = new HashMap<Long, EventHistory>();
 		
 		public Map<Long, JobDef> getJobDefs() {
 			return jobDefs;
 			}
 		}
 	}
+
+	// Event History
+	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	@Override
+	public void createEventHistory(EventHistory eventHistory) {
+		Long id = generateId(EventHistory.class);
+		eventHistory.setId(id);
+		SchedulerNodeStore nodeStore = getSchedulerNodeStore(eventHistory.getSchedulerNodeId());
+		nodeStore.eventHistories.put(id, eventHistory);
+	}
+	@Override
+	public EventHistory getEventHistory(Long schedulerNodeId, Long eventHistoryId) {
+		SchedulerNodeStore nodeStore = getSchedulerNodeStore(schedulerNodeId);
+		return nodeStore.eventHistories.get(eventHistoryId);
+	}
+	@Override
+	public List<EventHistory> findEventHistories(Long schedulerNodeId) {
+		List<EventHistory> result = new ArrayList<EventHistory>();
+		SchedulerNodeStore nodeStore = getSchedulerNodeStore(schedulerNodeId);
+		result.addAll(nodeStore.eventHistories.values());
+		// Sort the events
+		Collections.sort(result, new Comparator<EventHistory>() {
+			@Override
+			public int compare(EventHistory o1, EventHistory o2) {
+				return o2.getCreateTime().compareTo(o1.getCreateTime());
+			}
+		});
+		return result;
+	}
+	@Override
+	public List<EventHistory> findEventHistories(Long schedulerNodeId, Date fromTime, Date toTime) {
+		List<EventHistory> result = new ArrayList<EventHistory>();
+		SchedulerNodeStore nodeStore = getSchedulerNodeStore(schedulerNodeId);
+		long fromTimeLong = fromTime.getTime();
+		long toTimeLong = toTime.getTime();
+		for (EventHistory eh : nodeStore.eventHistories.values()) {
+			long createTimeLong = eh.getCreateTime().getTime();
+			if (createTimeLong >= fromTimeLong && createTimeLong <= toTimeLong)
+				result.add(eh);
+		}
+		// Sort the events
+		Collections.sort(result, new Comparator<EventHistory>() {
+			@Override
+			public int compare(EventHistory o1, EventHistory o2) {
+				return o2.getCreateTime().compareTo(o1.getCreateTime());
+			}
+		});
+		return result;
+	}
+	@Override
+	public void deleteEventHistories(Long schedulerNodeId, Date olderThanTime) {
+		List<EventHistory> result = new ArrayList<EventHistory>();
+		SchedulerNodeStore nodeStore = getSchedulerNodeStore(schedulerNodeId);
+		long olderThanTimeLong = olderThanTime.getTime();
+		for (EventHistory eh : nodeStore.eventHistories.values()) {
+			long createTimeLong = eh.getCreateTime().getTime();
+			if (createTimeLong < olderThanTimeLong)
+				result.add(eh);
+		}
+		for (EventHistory eh : result) {
+			nodeStore.eventHistories.remove(eh.getId());
+		}
+	}
 }

File timemachine-scheduler/src/main/resources/timemachine/scheduler/default.properties

 timemachine.scheduler.jobTaskThreadPool.DEFAULT.maxShutdownWaitTime = 1000
 timemachine.scheduler.jobTaskThreadPool.DEFAULT.threadNamePrefix = ${timemachine.scheduler.schedulerName}-JobTask-Thread-
 
+# Scheduler Event History Service (default removeInterval is 7 days in ms.)
+timemachine.scheduler.eventHistory.class = timemachine.scheduler.service.EventHistoryService
+timemachine.scheduler.eventHistory.removeInterval = 604800000
+#timemachine.scheduler.eventHistory.disable = true
+
 # Resolving multiple jobTask thread pools
 #timemachine.scheduler.jobTaskPoolNameResolver.poolName.MY_POOL.matchToJobNameRexp = MyJob.*
 
 #timemachine.scheduler.dataStore.hibernateDataStore.hibernate.c3p0.maxIdleTime=300000
 #timemachine.scheduler.dataStore.hibernateDataStore.hibernate.c3p0.idleConnectionTestPeriod=250000
 #timemachine.scheduler.dataStore.hibernateDataStore.hibernate.c3p0.preferredTestQuery=SELECT 1
+# One time database schema setup
+#timemachine.scheduler.dataStore.hibernateDataStore.hibernate.hbm2ddl.auto = update

File timemachine-scheduler/src/test/resources/log4j.properties

 #log4j.logger.timemachine.scheduler.service.PollingScheduleRunner = TRACE
 #log4j.logger.timemachine.scheduler.service.SchedulerEngine = TRACE
 #log4j.logger.timemachine.scheduler.service.DynamicThreadPool = TRACE
+log4j.logger.timemachine.scheduler.service.EventHistoryService = DEBUG
 
 log4j.logger.integration.timemachine.scheduler = INFO
 log4j.logger.unit.timemachine.scheduler = INFO

File timemachine-web/src/main/java/timemachine/schedulerweb/EventHistoryServlet.java

+/*
+ * Copyright 2012 Zemian Deng
+ * 
+ * Licensed 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 timemachine.schedulerweb;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import timemachine.scheduler.EventHistory;
+import timemachine.scheduler.Scheduler;
+import timemachine.scheduler.service.DataStore;
+import timemachine.scheduler.service.SchedulerEngine;
+
+/**
+ * Serve the scheduler event histories page.
+ * 
+ * @author Zemian Deng
+ */
+public class EventHistoryServlet extends HttpServlet
+{
+	private static Logger logger = LoggerFactory.getLogger(EventHistoryServlet.class);
+	
+	private static final long serialVersionUID = 1L;
+	
+	private static final String VIEW = "event-history.jsp";
+	
+	@Override
+	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
+	{
+		logger.trace("Getting scheduler event histories page.");
+		Scheduler scheduler = (Scheduler)getServletContext().getAttribute("scheduler");
+		// We assume we use the one and only SchedulerEngine impl.
+		SchedulerEngine schedulerEngine = (SchedulerEngine)scheduler;
+		DataStore dataStore = schedulerEngine.getSystemServiceContainer().getDataStoreService();
+		List<EventHistory> eventHistories = dataStore.findEventHistories(scheduler.getSchedulerNode().getId());
+		req.setAttribute("eventHistories", eventHistories);
+		req.getRequestDispatcher(VIEW).forward(req, resp);
+	}
+}

File timemachine-web/src/main/webapp/WEB-INF/web.xml

 	<servlet-mapping>
 		<servlet-name>ScriptingShellServlet</servlet-name>
 		<url-pattern>/scripting-shell</url-pattern>
-	</servlet-mapping>	
+	</servlet-mapping>
 	<servlet>
 		<servlet-name>EditConfigServlet</servlet-name>
 		<servlet-class>timemachine.schedulerweb.EditConfigServlet</servlet-class>
 		<servlet-name>SystemInfoServlet</servlet-name>
 		<url-pattern>/system-info</url-pattern>
 	</servlet-mapping>
+	<servlet>
+		<servlet-name>EventHistoryServlet</servlet-name>
+		<servlet-class>timemachine.schedulerweb.EventHistoryServlet</servlet-class>
+	</servlet>
+	<servlet-mapping>
+		<servlet-name>EventHistoryServlet</servlet-name>
+		<url-pattern>/event-history</url-pattern>
+	</servlet-mapping>
 
 	<context-param>
 		<param-name>schedulerConfigProps</param-name>

File timemachine-web/src/main/webapp/event-history.jsp

+<%@ include file="header.inc" %>
+
+<h2>Scheduler Event Histories</h2>
+<p>There are ${ fn:length(eventHistories) } events found.</p>
+<table>
+	<thead>
+	<tr>
+		<th> EventId </th>
+		<th> SchedulerNodeId </th>
+		<th> EventType </th>
+		<th> Name </th>
+		<th> CreateTime </th>
+		<th> Info1 </th>
+		<th> Info2 </th>
+		<th> Info3 </th>
+		<th> Info4 </th>
+		<th> Info5 </th>
+	</tr>
+	</thead>
+	<tbody>
+	<c:forEach items="${ eventHistories }" var="eventHistory">
+	<tr>
+		<td>${eventHistory.id}</td>
+		<td>${eventHistory.schedulerNodeId}</td>
+		<td>${eventHistory.type}</td>
+		<td>${eventHistory.name}</td>
+		<td><fmt:formatDate pattern="MM/dd/yy HH:mm:ss" value="${eventHistory.createTime}"/></td>
+		<td>${eventHistory.info1}</td>
+		<td>${eventHistory.info2}</td>
+		<td>${eventHistory.info3}</td>
+		<td>${eventHistory.info4}</td>
+		<td>${eventHistory.info5}</td>
+	</tr>
+	</c:forEach>
+	</tbody>
+</table>
+
+<%@ include file="footer.inc" %>

File timemachine-web/src/main/webapp/header.inc

 
 <ul id="menu">
 	<li><a href="${ pageContext.request.contextPath }/job-list">Job List</a></li>
+	<li><a href="${ pageContext.request.contextPath }/event-history">Events</a></li>
 	<li><a href="${ pageContext.request.contextPath }/edit-config">Edit Config</a></li>
 	<li><a href="${ pageContext.request.contextPath }/scripting-shell">Scripting Shell</a></li>
 	<li><a href="${ pageContext.request.contextPath }/system-info">System Info</a></li>