/* * Copyright 2009-2010 the original author or authors. * * 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 org.springframework.batch.admin.jmx; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.batch.admin.service.JobService; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.launch.NoSuchJobException; import org.springframework.context.SmartLifecycle; import org.springframework.jmx.export.MBeanExporter; import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler; import org.springframework.jmx.export.naming.MetadataNamingStrategy; import org.springframework.jmx.support.MetricType; import org.springframework.util.Assert; @ManagedResource public class BatchMBeanExporter extends MBeanExporter implements SmartLifecycle { private static final Log logger = LogFactory.getLog(BatchMBeanExporter.class); public static final String DEFAULT_DOMAIN = "org.springframework.batch"; private volatile boolean autoStartup = true; private volatile int phase = 0; private volatile boolean running; private final Map<String, String> objectNameStaticProperties = new LinkedHashMap<String, String>(); private final ReentrantLock lifecycleLock = new ReentrantLock(); private Set<String> stepKeys = new HashSet<String>(); private Set<String> jobKeys = new HashSet<String>(); private final AnnotationJmxAttributeSource attributeSource = new AnnotationJmxAttributeSource(); private JobService jobService; private String domain = DEFAULT_DOMAIN; private boolean registerSteps = true; private JobExecutionMetricsFactory jobExecutionMetricsFactory = new ExecutionMetricsFactory(); private StepExecutionMetricsFactory stepExecutionMetricsFactory = new ExecutionMetricsFactory(); public BatchMBeanExporter() { super(); setAutodetect(false); setNamingStrategy(new MetadataNamingStrategy(attributeSource)); setAssembler(new MetadataMBeanInfoAssembler(attributeSource)); } /** * Flag to determine if any metrics at all should be exposed for step * executions (default true). Set to fals eto only expose job-level metrics. * * @param registerSteps the flag to set */ public void setRegisterSteps(boolean registerSteps) { this.registerSteps = registerSteps; } /** * The JMX domain to use for MBeans registered. Defaults to * <code>org.springframework.batch</code> (which is useful in SpringSource * HQ). * * @param domain the domain name to set */ public void setDefaultDomain(String domain) { this.domain = domain; } /** * Help method for extensions which need access to the default domain. * * @return the default domain used to construct object names */ protected String getDefaultDomain() { return this.domain; } public void setJobService(JobService jobService) { this.jobService = jobService; } /** * Static properties that will be added to all object names. * * @param objectNameStaticProperties the objectNameStaticProperties to set */ public void setObjectNameStaticProperties(Map<String, String> objectNameStaticProperties) { this.objectNameStaticProperties.putAll(objectNameStaticProperties); } /** * Factory for {@link JobExecutionMetrics}. Can be used to customize and * extend the metrics exposed. * * @param stepExecutionMetricsFactory the {@link StepExecutionMetricsFactory} to set */ public void setStepExecutionMetricsFactory(StepExecutionMetricsFactory stepExecutionMetricsFactory) { this.stepExecutionMetricsFactory = stepExecutionMetricsFactory; } /** * Factory for {@link StepExecutionMetrics}. Can be used to customize and * extend the metrics exposed. * * @param jobExecutionMetricsFactory the {@link JobExecutionMetricsFactory} to set */ public void setJobExecutionMetricsFactory(JobExecutionMetricsFactory jobExecutionMetricsFactory) { this.jobExecutionMetricsFactory = jobExecutionMetricsFactory; } @Override public void afterPropertiesSet() { Assert.state(jobService != null, "A JobService must be provided"); super.afterPropertiesSet(); } protected void registerBeans() { // Completely disable super class registration to avoid duplicates } private void registerSteps() { if (!registerSteps) { return; } for (String jobName : jobService.listJobs(0, Integer.MAX_VALUE)) { Collection<JobExecution> jobExecutions = Collections.emptySet(); try { jobExecutions = jobService.listJobExecutionsForJob(jobName, 0, 1); } catch (NoSuchJobException e) { // do-nothing logger.error("Job listed but does not exist", e); } for (JobExecution jobExecution : jobExecutions) { for (StepExecution stepExecution : jobExecution.getStepExecutions()) { String stepName = stepExecution.getStepName(); String stepKey = String.format("%s/%s", jobName, stepName); String beanKey = getBeanKeyForStepExecution(jobName, stepName); if (!stepKeys.contains(stepKey)) { stepKeys.add(stepKey); logger.info("Registering step execution " + stepKey); registerBeanNameOrInstance(stepExecutionMetricsFactory.createMetricsForStep(jobName, stepName), beanKey); } } } } } private void registerJobs() { for (String jobName : jobService.listJobs(0, Integer.MAX_VALUE)) { if (!jobKeys.contains(jobName)) { jobKeys.add(jobName); logger.info("Registering job execution " + jobName); registerBeanNameOrInstance(jobExecutionMetricsFactory.createMetricsForJob(jobName), getBeanKeyForJobExecution(jobName)); } } } /** * Encode the job name into an ObjectName in the form * <code>[domain]:type=JobExecution,name=[jobName]</code>. * * @param jobName the name of the job * @return a String representation of an ObjectName */ protected String getBeanKeyForJobExecution(String jobName) { jobName = escapeForObjectName(jobName); return String.format("%s:type=JobExecution,name=%s", domain, jobName) + getStaticNames(); } /** * Encode the job and step name into an ObjectName in the form * <code>[domain]:type=JobExecution,name=[jobName],step=[stepName]</code>. * * @param jobName the name of the job * @param stepName the name of the step * @return a String representation of an ObjectName */ protected String getBeanKeyForStepExecution(String jobName, String stepName) { jobName = escapeForObjectName(jobName); stepName = escapeForObjectName(stepName); return String.format("%s:type=JobExecution,name=%s,step=%s", domain, jobName, stepName) + getStaticNames(); } private String getStaticNames() { if (objectNameStaticProperties.isEmpty()) { return ""; } StringBuilder builder = new StringBuilder(); for (String key : objectNameStaticProperties.keySet()) { builder.append("," + key + "=" + objectNameStaticProperties.get(key)); } return builder.toString(); } private String escapeForObjectName(String value) { value = value.replaceAll(":", "@"); value = value.replaceAll(",", ";"); value = value.replaceAll("=", "~"); return value; } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Step Count") public int getStepCount() { registerSteps(); return stepKeys.size(); } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Count") public int getJobCount() { registerJobs(); return jobKeys.size(); } @ManagedAttribute public String[] getJobNames() { return jobKeys.toArray(new String[0]); } @ManagedAttribute public String[] getStepNames() { return stepKeys.toArray(new String[0]); } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Execution Failure Count") public int getJobExecutionFailureCount() { int count = 0; int start = 0; int pageSize = 100; Collection<JobExecution> jobExecutions; do { jobExecutions = jobService.listJobExecutions(start, pageSize); start += pageSize; for (JobExecution jobExecution : jobExecutions) { if (jobExecution.getStatus().isUnsuccessful()) { count++; } } } while (!jobExecutions.isEmpty()); return count; } @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Job Execution Count") public int getJobExecutionCount() { return jobService.countJobExecutions(); } public final boolean isAutoStartup() { return this.autoStartup; } public final int getPhase() { return this.phase; } public final boolean isRunning() { this.lifecycleLock.lock(); try { return this.running; } finally { this.lifecycleLock.unlock(); } } public final void start() { this.lifecycleLock.lock(); try { if (!this.running) { this.doStart(); this.running = true; if (logger.isInfoEnabled()) { logger.info("started " + this); } } } finally { this.lifecycleLock.unlock(); } } public final void stop() { this.lifecycleLock.lock(); try { if (this.running) { this.doStop(); this.running = false; if (logger.isInfoEnabled()) { logger.info("stopped " + this); } } } finally { this.lifecycleLock.unlock(); } } public final void stop(Runnable callback) { this.lifecycleLock.lock(); try { this.stop(); callback.run(); } finally { this.lifecycleLock.unlock(); } } protected void doStop() { unregisterBeans(); jobKeys.clear(); stepKeys.clear(); } protected void doStart() { registerJobs(); registerSteps(); } private class ExecutionMetricsFactory implements JobExecutionMetricsFactory, StepExecutionMetricsFactory { public StepExecutionMetrics createMetricsForStep(String jobName, String stepName) { return new SimpleStepExecutionMetrics(jobService, jobName, stepName); } public JobExecutionMetrics createMetricsForJob(String jobName) { return new SimpleJobExecutionMetrics(jobService, jobName); } } }