/*
* Copyright 2012 Jörg Hoh, Alexander Saar, Markus Haack
*
* 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 de.joerghoh.cq5.healthcheck.impl.providers;
import java.lang.management.ManagementFactory;
import java.util.Dictionary;
import java.util.Set;
import javax.jcr.RepositoryException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.joerghoh.cq5.healthcheck.Status;
import de.joerghoh.cq5.healthcheck.StatusCode;
import de.joerghoh.cq5.healthcheck.StatusProvider;
@Service
@Component(label = "MBean Status Provider Factory", metatype = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE)
public class MBeanStatusProvider implements StatusProvider {
private static final Logger log = LoggerFactory
.getLogger(MBeanStatusProvider.class);
private MBeanServer server = ManagementFactory.getPlatformMBeanServer();
@Property
private static String CATEGORY = "provider.category";
private String category;
@Property
private static String MBEAN_NAME = "mbean.name";
private String mbeanName;
@Property
private static String MBEAN_PROPERTY = "mbean.property";
private String[] properties;
@Property
private static String MBEAN_PROVIDER_HINT = "mbean.providerHint";
private String providerHint;
private String providerName;
private ObjectName mbean;
private String statusMessage = "";
/**
* @see de.joerghoh.cq5.healthcheck.HealthStatusProvider#getHealthStatus()
*/
public Status getStatus() {
mbean = buildObjectName(mbeanName);
if (mbean != null && mbeanExists(mbean)) {
StatusCode status = calculateStatus();
return new Status(status, statusMessage, providerName);
} else {
log.info("Cannot resolve mbean " + mbeanName);
return new Status(StatusCode.UNKNOWN, "cannot find mbean",
providerName);
}
}
/**
* @see de.joerghoh.cq5.healthcheck.StatusProvider#getCategory()
*/
public String getCategory() {
return category != null ? category : DEFAULT_CATEGORY;
}
@Activate
protected void activate(ComponentContext ctx) throws RepositoryException {
Dictionary<?, ?> props = ctx.getProperties();
category = PropertiesUtil.toString(props.get(CATEGORY), null);
mbeanName = PropertiesUtil.toString(props.get(MBEAN_NAME), null);
properties = PropertiesUtil.toStringArray(props.get(MBEAN_PROPERTY));
providerHint = PropertiesUtil.toString(props.get(MBEAN_PROVIDER_HINT),
null);
mbean = buildObjectName(mbeanName);
if (mbean != null && mbeanExists(mbean)) {
log.info("Instantiate healtcheck for MBean {}", mbeanName);
} else {
log.warn("Cannot instantiate healthcheck for MBean {}", mbeanName);
}
providerName = mbeanName;
if (providerHint != null) {
providerName += " (" + providerHint + ")";
}
}
@Deactivate
protected void deactivate() {
}
/**
* calculate the overall status of the service
*
* @return the overall status
*/
private StatusCode calculateStatus() {
StatusCode accumulatedStatus = StatusCode.OK;
statusMessage = "";
for (String property : properties) {
/*
* value might be: "my.JMX.propertyName.warn.>", and we need the
* triple of "jmxAttributeName","level" and "type of comparison".
* The jmxAttributeName might contain dots, so we need to reverse to
* use String.split()
*/
final String key = new StringBuffer(property).reverse().toString(); // revert
// it
final String[] split = key.split("\\.", 4);
if (split.length != 4) {
// key does not match the needed configuration triple
continue;
}
final String comparisonAttributeName = new StringBuffer(split[3])
.reverse().toString();
// what would be the statusCode if we have a match?
final String comparisonLevel = new StringBuffer(split[2]).reverse()
.toString();
StatusCode statusCode = StatusCode.OK;
if (comparisonLevel.equalsIgnoreCase("warn")) {
statusCode = StatusCode.WARN;
} else if (comparisonLevel.equalsIgnoreCase("critical")) {
statusCode = StatusCode.CRITICAL;
} else {
log.warn("Ignoring property (invalid level): " + property);
continue;
}
final String comparisonOperation = new StringBuffer(split[1])
.reverse().toString();
// retrieve the long value for comparison
final String comparisonValue = new StringBuffer(split[0]).reverse()
.toString();
log.debug("compare {} and {}", property, comparisonValue);
// read the correct value via JMX
Object jmxValueObj = getAttributeValue(comparisonAttributeName);
if (jmxValueObj == null) {
log.warn("Ignoring property " + property
+ " (no such JMX attribute " + comparisonAttributeName
+ ")");
statusMessage = "Cannot resolve property or MBean";
return StatusCode.WARN;
}
log.debug("jmx value = {}", jmxValueObj);
// do the comparison
boolean match = false;
try {
match = compareAttributeValue(comparisonOperation,
comparisonValue, jmxValueObj);
} catch (RuntimeException e) {
log.info("Ignoring property (invalid value type): " + property);
continue;
}
// if we have a WARN or CRITICAL state for this value, report it
if (match) {
if (statusMessage.length() > 0) {
statusMessage += ", ";
}
statusMessage += comparisonAttributeName
+ " = "
+ (jmxValueObj.getClass().isArray() ? ArrayUtils
.toString(jmxValueObj) : jmxValueObj.toString());
}
// if we have a match, update the overall status
if (match && statusCode.compareTo(accumulatedStatus) > 0) {
accumulatedStatus = statusCode;
}
}
return accumulatedStatus;
}
/**
* small wrapper method to read the value via JMX
*
* @param attributeName
* @return
*/
private Object getAttributeValue(String attributeName) {
try {
return server.getAttribute(mbean, attributeName);
} catch (Exception e) {
log.warn("Cannot read attribute " + attributeName + " from MBean "
+ mbean);
}
return null;
}
/**
* compare helper method to compare the different types of attribute values
* supported types: ** long ** integer ** boolean ** string
*
* @param comparisonOperation
* @param comparisonValue
* @param jmxValueObj
* @return
*/
private boolean compareAttributeValue(final String comparisonOperation,
final String comparisonValue, Object jmxValueObj) {
boolean match = false;
if (jmxValueObj instanceof Long) { // first check for plain value
final long jmxLongValue = Long.parseLong(jmxValueObj.toString());
final long comparisonLongValue = NumberUtils
.createLong(comparisonValue);
if (comparisonOperation.equals(">")) {
match = (jmxLongValue > comparisonLongValue);
} else if (comparisonOperation.equals("==")) {
match = (jmxLongValue == comparisonLongValue);
} else if (comparisonOperation.equals("!=")) {
match = (jmxLongValue != comparisonLongValue);
} else if (comparisonOperation.equals("<")) {
match = (jmxLongValue < comparisonLongValue);
} else {
log.warn("Can not compare long values {} and {}", jmxLongValue,
comparisonLongValue);
throw new RuntimeException();
}
} else if (jmxValueObj instanceof Integer) {
final int jmxIntValue = Integer.parseInt(jmxValueObj.toString());
final int comparisonIntValue = NumberUtils
.createInteger(comparisonValue);
if (comparisonOperation.equals(">")) {
match = (jmxIntValue > comparisonIntValue);
} else if (comparisonOperation.equals("==")) {
match = (jmxIntValue == comparisonIntValue);
} else if (comparisonOperation.equals("!=")) {
match = (jmxIntValue != comparisonIntValue);
} else if (comparisonOperation.equals("<")) {
match = (jmxIntValue < comparisonIntValue);
} else {
log.warn("Can not compare int values {} and {}", jmxIntValue,
comparisonIntValue);
throw new RuntimeException();
}
} else if (jmxValueObj instanceof long[]) { // second check for array
final long[] jmxLongArrayValue = (long[]) jmxValueObj;
final long comparisonLongValue = NumberUtils
.createLong(comparisonValue);
if (comparisonOperation.equals(">")) {
match = (NumberUtils.max(jmxLongArrayValue) > comparisonLongValue);
} else if (comparisonOperation.equals("==")) {
// TODO maybe it makes more sense for == to compare all array
// values?
match = ArrayUtils.contains(jmxLongArrayValue,
comparisonLongValue);
} else if (comparisonOperation.equals("<")) {
match = (NumberUtils.min(jmxLongArrayValue) < comparisonLongValue);
} else {
log.warn("Can not compare long array values {} and {}",
jmxLongArrayValue, comparisonLongValue);
throw new RuntimeException();
}
} else if (jmxValueObj instanceof String) { // third check for String
// values
log.info("type comparison: string");
final String jmxStringValue = jmxValueObj.toString();
if (comparisonOperation.equals("equals")) {
match = (comparisonValue.equals(jmxStringValue));
} else if (comparisonOperation.equals("notequals")) {
match = (!comparisonValue.equals(jmxStringValue));
} else {
log.warn("Can not compare String values {} and {}",
jmxStringValue, comparisonValue);
throw new RuntimeException();
}
} else if (jmxValueObj instanceof Boolean) {
final boolean jmxBooleanValue = (Boolean) jmxValueObj;
final boolean comparisonBooleanValue = Boolean
.parseBoolean(comparisonValue);
if (comparisonOperation.equals("equals")) {
match = (comparisonBooleanValue == jmxBooleanValue);
} else if (comparisonOperation.equals("==")) {
match = (comparisonBooleanValue == jmxBooleanValue);
} else if (comparisonOperation.equals("notequals")) {
match = (comparisonBooleanValue != jmxBooleanValue);
} else {
log.warn("Can not compare boolean values {} and {}",
jmxBooleanValue, comparisonBooleanValue);
throw new RuntimeException();
}
} else {
log.warn("Can not compare jmx attribute value {} with {}",
jmxValueObj, comparisonValue);
log.warn("jmxValueObj type = " + jmxValueObj.getClass().getName());
throw new RuntimeException();
}
return match;
}
private ObjectName buildObjectName(String name) {
ObjectName mbean = null;
try {
mbean = new ObjectName(name);
} catch (MalformedObjectNameException e) {
log.error("Cannot create ObjectName " + name, e);
} catch (NullPointerException e) {
log.error("Cannot create ObjectName " + name, e);
}
return mbean;
}
private boolean mbeanExists(ObjectName mbean) {
Set<ObjectName> beans = server.queryNames(mbean, null);
return (beans.size() == 1);
}
}