View Javadoc
1   /*
2    * Copyright 2009-2010 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.springframework.batch.admin.jmx;
17  
18  import java.util.Collection;
19  import java.util.Collections;
20  import java.util.HashSet;
21  import java.util.LinkedHashMap;
22  import java.util.Map;
23  import java.util.Set;
24  import java.util.concurrent.locks.ReentrantLock;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.springframework.batch.admin.service.JobService;
29  import org.springframework.batch.core.JobExecution;
30  import org.springframework.batch.core.StepExecution;
31  import org.springframework.batch.core.launch.NoSuchJobException;
32  import org.springframework.context.SmartLifecycle;
33  import org.springframework.jmx.export.MBeanExporter;
34  import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
35  import org.springframework.jmx.export.annotation.ManagedAttribute;
36  import org.springframework.jmx.export.annotation.ManagedMetric;
37  import org.springframework.jmx.export.annotation.ManagedResource;
38  import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler;
39  import org.springframework.jmx.export.naming.MetadataNamingStrategy;
40  import org.springframework.jmx.support.MetricType;
41  import org.springframework.util.Assert;
42  
43  @ManagedResource
44  public class BatchMBeanExporter extends MBeanExporter implements SmartLifecycle {
45  
46  	private static final Log logger = LogFactory.getLog(BatchMBeanExporter.class);
47  
48  	public static final String DEFAULT_DOMAIN = "org.springframework.batch";
49  
50  	private volatile boolean autoStartup = true;
51  
52  	private volatile int phase = 0;
53  
54  	private volatile boolean running;
55  
56  	private final Map<String, String> objectNameStaticProperties = new LinkedHashMap<String, String>();
57  
58  	private final ReentrantLock lifecycleLock = new ReentrantLock();
59  
60  	private Set<String> stepKeys = new HashSet<String>();
61  
62  	private Set<String> jobKeys = new HashSet<String>();
63  
64  	private final AnnotationJmxAttributeSource attributeSource = new AnnotationJmxAttributeSource();
65  
66  	private JobService jobService;
67  
68  	private String domain = DEFAULT_DOMAIN;
69  
70  	private boolean registerSteps = true;
71  
72  	private JobExecutionMetricsFactory jobExecutionMetricsFactory = new ExecutionMetricsFactory();
73  
74  	private StepExecutionMetricsFactory stepExecutionMetricsFactory = new ExecutionMetricsFactory();
75  
76  	public BatchMBeanExporter() {
77  		super();
78  		setAutodetect(false);
79  		setNamingStrategy(new MetadataNamingStrategy(attributeSource));
80  		setAssembler(new MetadataMBeanInfoAssembler(attributeSource));
81  	}
82  
83  	/**
84  	 * Flag to determine if any metrics at all should be exposed for step
85  	 * executions (default true). Set to fals eto only expose job-level metrics.
86  	 * 
87  	 * @param registerSteps the flag to set
88  	 */
89  	public void setRegisterSteps(boolean registerSteps) {
90  		this.registerSteps = registerSteps;
91  	}
92  
93  	/**
94  	 * The JMX domain to use for MBeans registered. Defaults to
95  	 * <code>org.springframework.batch</code> (which is useful in SpringSource
96  	 * HQ).
97  	 * 
98  	 * @param domain the domain name to set
99  	 */
100 	public void setDefaultDomain(String domain) {
101 		this.domain = domain;
102 	}
103 
104 	/**
105 	 * Help method for extensions which need access to the default domain.
106 	 * 
107 	 * @return the default domain used to construct object names
108 	 */
109 	protected String getDefaultDomain() {
110 		return this.domain;
111 	}
112 
113 	public void setJobService(JobService jobService) {
114 		this.jobService = jobService;
115 	}
116 
117 	/**
118 	 * Static properties that will be added to all object names.
119 	 * 
120 	 * @param objectNameStaticProperties the objectNameStaticProperties to set
121 	 */
122 	public void setObjectNameStaticProperties(Map<String, String> objectNameStaticProperties) {
123 		this.objectNameStaticProperties.putAll(objectNameStaticProperties);
124 	}
125 
126 	/**
127 	 * Factory for {@link JobExecutionMetrics}. Can be used to customize and
128 	 * extend the metrics exposed.
129 	 * 
130 	 * @param stepExecutionMetricsFactory the {@link StepExecutionMetricsFactory} to set
131 	 */
132 	public void setStepExecutionMetricsFactory(StepExecutionMetricsFactory stepExecutionMetricsFactory) {
133 		this.stepExecutionMetricsFactory = stepExecutionMetricsFactory;
134 	}
135 
136 	/**
137 	 * Factory for {@link StepExecutionMetrics}. Can be used to customize and
138 	 * extend the metrics exposed.
139 	 * 
140 	 * @param jobExecutionMetricsFactory the {@link JobExecutionMetricsFactory} to set
141 	 */
142 	public void setJobExecutionMetricsFactory(JobExecutionMetricsFactory jobExecutionMetricsFactory) {
143 		this.jobExecutionMetricsFactory = jobExecutionMetricsFactory;
144 	}
145 
146 	@Override
147 	public void afterPropertiesSet() {
148 		Assert.state(jobService != null, "A JobService must be provided");
149 		super.afterPropertiesSet();
150 	}
151 
152 	protected void registerBeans() {
153 		// Completely disable super class registration to avoid duplicates
154 	}
155 
156 	private void registerSteps() {
157 		if (!registerSteps) {
158 			return;
159 		}
160 		for (String jobName : jobService.listJobs(0, Integer.MAX_VALUE)) {
161 			Collection<JobExecution> jobExecutions = Collections.emptySet();
162 			try {
163 				jobExecutions = jobService.listJobExecutionsForJob(jobName, 0, 1);
164 			}
165 			catch (NoSuchJobException e) {
166 				// do-nothing
167 				logger.error("Job listed but does not exist", e);
168 			}
169 			for (JobExecution jobExecution : jobExecutions) {
170 				for (StepExecution stepExecution : jobExecution.getStepExecutions()) {
171 					String stepName = stepExecution.getStepName();
172 					String stepKey = String.format("%s/%s", jobName, stepName);
173 					String beanKey = getBeanKeyForStepExecution(jobName, stepName);
174 					if (!stepKeys.contains(stepKey)) {
175 						stepKeys.add(stepKey);
176 						logger.info("Registering step execution " + stepKey);
177 						registerBeanNameOrInstance(stepExecutionMetricsFactory.createMetricsForStep(jobName, stepName),
178 								beanKey);
179 					}
180 				}
181 			}
182 		}
183 	}
184 
185 	private void registerJobs() {
186 		for (String jobName : jobService.listJobs(0, Integer.MAX_VALUE)) {
187 			if (!jobKeys.contains(jobName)) {
188 				jobKeys.add(jobName);
189 				logger.info("Registering job execution " + jobName);
190 				registerBeanNameOrInstance(jobExecutionMetricsFactory.createMetricsForJob(jobName),
191 						getBeanKeyForJobExecution(jobName));
192 			}
193 		}
194 	}
195 
196 	/**
197 	 * Encode the job name into an ObjectName in the form
198 	 * <code>[domain]:type=JobExecution,name=[jobName]</code>.
199 	 * 
200 	 * @param jobName the name of the job
201 	 * @return a String representation of an ObjectName
202 	 */
203 	protected String getBeanKeyForJobExecution(String jobName) {
204 		jobName = escapeForObjectName(jobName);
205 		return String.format("%s:type=JobExecution,name=%s", domain, jobName) + getStaticNames();
206 	}
207 
208 	/**
209 	 * Encode the job and step name into an ObjectName in the form
210 	 * <code>[domain]:type=JobExecution,name=[jobName],step=[stepName]</code>.
211 	 * 
212 	 * @param jobName the name of the job
213 	 * @param stepName the name of the step
214 	 * @return a String representation of an ObjectName
215 	 */
216 	protected String getBeanKeyForStepExecution(String jobName, String stepName) {
217 		jobName = escapeForObjectName(jobName);
218 		stepName = escapeForObjectName(stepName);
219 		return String.format("%s:type=JobExecution,name=%s,step=%s", domain, jobName, stepName) + getStaticNames();
220 	}
221 
222 	private String getStaticNames() {
223 		if (objectNameStaticProperties.isEmpty()) {
224 			return "";
225 		}
226 		StringBuilder builder = new StringBuilder();
227 		for (String key : objectNameStaticProperties.keySet()) {
228 			builder.append("," + key + "=" + objectNameStaticProperties.get(key));
229 		}
230 		return builder.toString();
231 	}
232 
233 	private String escapeForObjectName(String value) {
234 		value = value.replaceAll(":", "@");
235 		value = value.replaceAll(",", ";");
236 		value = value.replaceAll("=", "~");
237 		return value;
238 	}
239 
240 	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Step Count")
241 	public int getStepCount() {
242 		registerSteps();
243 		return stepKeys.size();
244 	}
245 
246 	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Count")
247 	public int getJobCount() {
248 		registerJobs();
249 		return jobKeys.size();
250 	}
251 
252 	@ManagedAttribute
253 	public String[] getJobNames() {
254 		return jobKeys.toArray(new String[0]);
255 	}
256 
257 	@ManagedAttribute
258 	public String[] getStepNames() {
259 		return stepKeys.toArray(new String[0]);
260 	}
261 
262 	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Execution Failure Count")
263 	public int getJobExecutionFailureCount() {
264 		int count = 0;
265 		int start = 0;
266 		int pageSize = 100;
267 		Collection<JobExecution> jobExecutions;
268 		do {
269 			jobExecutions = jobService.listJobExecutions(start, pageSize);
270 			start += pageSize;
271 			for (JobExecution jobExecution : jobExecutions) {
272 				if (jobExecution.getStatus().isUnsuccessful()) {
273 					count++;
274 				}
275 			}
276 		} while (!jobExecutions.isEmpty());
277 		return count;
278 	}
279 
280 	@ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Execution Count")
281 	public int getJobExecutionCount() {
282 		return jobService.countJobExecutions();
283 	}
284 
285 	public final boolean isAutoStartup() {
286 		return this.autoStartup;
287 	}
288 
289 	public final int getPhase() {
290 		return this.phase;
291 	}
292 
293 	public final boolean isRunning() {
294 		this.lifecycleLock.lock();
295 		try {
296 			return this.running;
297 		}
298 		finally {
299 			this.lifecycleLock.unlock();
300 		}
301 	}
302 
303 	public final void start() {
304 		this.lifecycleLock.lock();
305 		try {
306 			if (!this.running) {
307 				this.doStart();
308 				this.running = true;
309 				if (logger.isInfoEnabled()) {
310 					logger.info("started " + this);
311 				}
312 			}
313 		}
314 		finally {
315 			this.lifecycleLock.unlock();
316 		}
317 	}
318 
319 	public final void stop() {
320 		this.lifecycleLock.lock();
321 		try {
322 			if (this.running) {
323 				this.doStop();
324 				this.running = false;
325 				if (logger.isInfoEnabled()) {
326 					logger.info("stopped " + this);
327 				}
328 			}
329 		}
330 		finally {
331 			this.lifecycleLock.unlock();
332 		}
333 	}
334 
335 	public final void stop(Runnable callback) {
336 		this.lifecycleLock.lock();
337 		try {
338 			this.stop();
339 			callback.run();
340 		}
341 		finally {
342 			this.lifecycleLock.unlock();
343 		}
344 	}
345 
346 	protected void doStop() {
347 		unregisterBeans();
348 		jobKeys.clear();
349 		stepKeys.clear();
350 	}
351 
352 	protected void doStart() {
353 		registerJobs();
354 		registerSteps();
355 	}
356 
357 	private class ExecutionMetricsFactory implements JobExecutionMetricsFactory, StepExecutionMetricsFactory {
358 
359 		public StepExecutionMetrics createMetricsForStep(String jobName, String stepName) {
360 			return new SimpleStepExecutionMetrics(jobService, jobName, stepName);
361 		}
362 
363 		public JobExecutionMetrics createMetricsForJob(String jobName) {
364 			return new SimpleJobExecutionMetrics(jobService, jobName);
365 		}
366 
367 	}
368 
369 }