/*
* Jopr Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.plugins.jbossas;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.Nullable;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.mc4j.ems.connection.bean.operation.EmsOperation;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.measurement.calltime.CallTimeData;
import org.rhq.core.pluginapi.inventory.DeleteResourceFacet;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.pluginapi.util.ResponseTimeConfiguration;
import org.rhq.core.pluginapi.util.ResponseTimeLogParser;
import org.rhq.plugins.jbossas.util.DeploymentUtility;
import org.rhq.plugins.jbossas.util.WarDeploymentInformation;
import org.rhq.plugins.jbossas.util.WarDiscoveryHelper;
import org.rhq.plugins.jmx.util.ObjectNameQueryUtility;
/**
* A resource component for managing a web application (WAR) deployed to a JBossAS server.
*
* @author Ian Springer
* @author Heiko W. Rupp
*/
public class WarComponent extends ApplicationComponent implements OperationFacet, DeleteResourceFacet {
private static final String SERVLET_PREFIX = "Servlet.";
public static final String CONTEXT_ROOT_CONFIG_PROP = "contextRoot";
public static final String NAME_CONFIG_PROP = "name";
public static final String FILE_NAME = "filename";
public static final String JBOSS_WEB_NAME = "jbossWebName";
public static final String RESPONSE_TIME_LOG_FILE_CONFIG_PROP = "responseTimeLogFile";
public static final String RESPONSE_TIME_URL_EXCLUDES_CONFIG_PROP = "responseTimeUrlExcludes";
public static final String RESPONSE_TIME_URL_TRANSFORMS_CONFIG_PROP = "responseTimeUrlTransforms";
private static final String RESPONSE_TIME_METRIC = "ResponseTime";
private static final String CONTEXT_ROOT_METRIC = "ContextRoot";
private static final String MAX_SERVLET_TIME = "Servlet.MaxResponseTime";
private static final String MIN_SERVLET_TIME = "Servlet.MinResponseTime";
private static final String AVG_SERVLET_TIME = "Servlet.AvgResponseTime";
private static final String NUM_SERVLET_REQUESTS = "Servlet.NumRequests";
private static final String NUM_SERVLET_ERRORS = "Servlet.NumErrors";
private static final String TOTAL_TIME = "Servlet.TotalTime";
private static final String SERVLET_NAME_BASE_TEMPLATE = "jboss.web:J2EEApplication=none,J2EEServer=none,j2eeType=Servlet,name=%name%";
private static final String SESSION_NAME_BASE_TEMPLATE = "jboss.web:host=%HOST%,type=Manager,path=%PATH%";
//WebModule=//localhost/test-simple,service=ClusterManager
private static final String CLUSTER_SESSION_NAME_BASE_TEMPLATE = "jboss.web:service=ClusterManager,WebModule=//%HOST%%PATH%";
private static final String SESSION_PREFIX = "Session.";
private static final String VHOST_PREFIX = "Vhost";
public static final String VHOST_CONFIG_PROP = "vHost";
public static final String ROOT_WEBAPP_CONTEXT_ROOT = "/";
private final Log log = LogFactory.getLog(this.getClass());
// Mapping non-clustered names -> attribute name in the cluster manager
private final String[] CLUSTER_SESSION_ATTRIBUTE_NAMES = { "maxInactiveInterval", "MaxInactiveInterval",
"activeSessions", "ActiveSessionCount", "sessionCounter", "CreatedSessionCount", "sessionAverageAliveTime", "",
"processingTime", "ProcessingTime", "maxActive", "MaxActiveSessionCount", "maxActiveSessions",
"MaxActiveAllowed", "expiredSessions", "ExpiredSessionCount", "rejectedSessions", "RejectedSessionCount",
"sessionIdLength", "SessionIdLength" };
private EmsBean jbossWebMBean;
private ResponseTimeLogParser logParser;
private String vhost;
private String contextRoot;
@Override
public AvailabilityType getAvailability() {
AvailabilityType availability;
if (this.jbossWebMBean != null) {
int state = (Integer) this.jbossWebMBean.getAttribute("state").refresh();
availability = (state == JBossWebMBeanState.STARTED) ? AvailabilityType.UP : AvailabilityType.DOWN;
} else {
// The WAR has no jboss.web MBean associated with it - this means it
// has no associated context root (i.e. it's not exposed via HTTP),
// so consider it down.
// @TODO In Embedded Console, we might want to call getJBossWebMBean() again to make sure. (See todo in that method for why)
// Try to get the JBossWebMBean again
// If you can't then set this to down.
this.jbossWebMBean = getJBossWebMBean();
if (this.jbossWebMBean == null) {
availability = AvailabilityType.DOWN;
} else {
availability = getAvailability();
}
}
return availability;
}
@Override
public void start(ResourceContext resourceContext) {
super.start(resourceContext);
Configuration pluginConfig = getResourceContext().getPluginConfiguration();
this.jbossWebMBean = getJBossWebMBean();
this.vhost = pluginConfig.getSimple(VHOST_CONFIG_PROP).getStringValue();
this.contextRoot = pluginConfig.getSimple(CONTEXT_ROOT_CONFIG_PROP).getStringValue();
ResponseTimeConfiguration responseTimeConfig = new ResponseTimeConfiguration(pluginConfig);
File logFile = responseTimeConfig.getLogFile();
if (logFile != null) {
this.logParser = new ResponseTimeLogParser(logFile);
this.logParser.setExcludes(responseTimeConfig.getExcludes());
this.logParser.setTransforms(responseTimeConfig.getTransforms());
}
}
@Override
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> schedules) {
Set<MeasurementScheduleRequest> remainingSchedules = new LinkedHashSet<MeasurementScheduleRequest>();
for (MeasurementScheduleRequest schedule : schedules) {
String metricName = schedule.getName();
if (metricName.equals(RESPONSE_TIME_METRIC)) {
if (this.logParser != null) {
try {
CallTimeData callTimeData = new CallTimeData(schedule);
this.logParser.parseLog(callTimeData);
report.addData(callTimeData);
} catch (Exception e) {
log.error("Failed to retrieve HTTP call-time data.", e);
}
} else {
log.error("The '" + RESPONSE_TIME_METRIC + "' metric is enabled for WAR resource '"
+ getApplicationName() + "', but no value is defined for the '"
+ RESPONSE_TIME_LOG_FILE_CONFIG_PROP + "' connection property.");
// TODO: Communicate this error back to the server for display in the GUI.
}
} else if (metricName.equals(CONTEXT_ROOT_METRIC)) {
MeasurementDataTrait trait = new MeasurementDataTrait(schedule, contextRoot);
report.addData(trait);
} else if (metricName.startsWith(SERVLET_PREFIX)) {
Double value = getServletMetric(metricName);
MeasurementDataNumeric metric = new MeasurementDataNumeric(schedule, value);
report.addData(metric);
} else if (metricName.startsWith(SESSION_PREFIX)) {
Double value = getSessionMetric(metricName);
MeasurementDataNumeric metric = new MeasurementDataNumeric(schedule, value);
report.addData(metric);
} else if (metricName.startsWith(VHOST_PREFIX)) {
if (metricName.equals("Vhost.name")) {
List<EmsBean> beans = getVHosts(contextRoot);
String value = "";
Iterator<EmsBean> iter = beans.iterator();
while (iter.hasNext()) {
EmsBean eBean = iter.next();
value += eBean.getBeanName().getKeyProperty("host");
if (iter.hasNext())
value += ",";
}
MeasurementDataTrait trait = new MeasurementDataTrait(schedule, value);
report.addData(trait);
}
} else {
remainingSchedules.add(schedule);
}
}
super.getValues(report, remainingSchedules);
}
private Double getSessionMetric(String metricName) {
boolean isClustered = false;
EmsConnection jmxConnection = getEmsConnection();
String servletMBeanNames = SESSION_NAME_BASE_TEMPLATE.replace("%PATH%",
WarDiscoveryHelper.getContextPath(this.contextRoot));
servletMBeanNames = servletMBeanNames.replace("%HOST%", vhost);
ObjectNameQueryUtility queryUtility = new ObjectNameQueryUtility(servletMBeanNames);
List<EmsBean> mBeans = jmxConnection.queryBeans(queryUtility.getTranslatedQuery());
if (mBeans.size() == 0) {
// retry with the cluster manager TODO select the local vs cluster mode on discovery
servletMBeanNames = CLUSTER_SESSION_NAME_BASE_TEMPLATE.replace("%PATH%",
WarDiscoveryHelper.getContextPath(this.contextRoot));
servletMBeanNames = servletMBeanNames.replace("%HOST%", vhost);
queryUtility = new ObjectNameQueryUtility(servletMBeanNames);
mBeans = jmxConnection.queryBeans(queryUtility.getTranslatedQuery());
if (mBeans.size() > 0)
isClustered = true;
}
String property = metricName.substring(SESSION_PREFIX.length());
Double ret = Double.NaN;
if (mBeans.size() > 0) { // TODO flag error if != 1 ?
EmsBean eBean = mBeans.get(0);
eBean.refreshAttributes();
if (isClustered) {
property = lookupClusteredAttributeName(property);
}
EmsAttribute att = eBean.getAttribute(property);
if (att != null) {
Object o = att.getValue();
if (o instanceof Long) {
Long l = (Long) o;
ret = Double.valueOf(l);
} else {
Integer i = (Integer) o;
ret = Double.valueOf(i);
}
}
}
return ret;
}
private String lookupClusteredAttributeName(String property) {
for (int i = 0; i < CLUSTER_SESSION_ATTRIBUTE_NAMES.length; i += 2) {
if (CLUSTER_SESSION_ATTRIBUTE_NAMES[i].equals(property))
return CLUSTER_SESSION_ATTRIBUTE_NAMES[i + 1];
}
return property;
}
private Double getServletMetric(String metricName) {
String servletMBeanNames = SERVLET_NAME_BASE_TEMPLATE + ",WebModule=//" + this.vhost
+ WarDiscoveryHelper.getContextPath(this.contextRoot);
ObjectNameQueryUtility queryUtility = new ObjectNameQueryUtility(servletMBeanNames);
List<EmsBean> mBeans = getEmsConnection().queryBeans(queryUtility.getTranslatedQuery());
long min = Long.MAX_VALUE;
long max = 0;
long processingTime = 0;
int requestCount = 0;
int errorCount = 0;
Double result;
for (EmsBean mBean : mBeans) {
mBean.refreshAttributes();
if (metricName.equals(MIN_SERVLET_TIME)) {
EmsAttribute att = mBean.getAttribute("minTime");
Long l = (Long) att.getValue();
if (l < min)
min = l;
} else if (metricName.equals(MAX_SERVLET_TIME)) {
EmsAttribute att = mBean.getAttribute("maxTime");
Long l = (Long) att.getValue();
if (l > max)
max = l;
} else if (metricName.equals(AVG_SERVLET_TIME)) {
EmsAttribute att = mBean.getAttribute("processingTime");
Long l = (Long) att.getValue();
processingTime += l;
att = mBean.getAttribute("requestCount");
Integer i = (Integer) att.getValue();
requestCount += i;
} else if (metricName.equals(NUM_SERVLET_REQUESTS)) {
EmsAttribute att = mBean.getAttribute("requestCount");
Integer i = (Integer) att.getValue();
requestCount += i;
} else if (metricName.equals(NUM_SERVLET_ERRORS)) {
EmsAttribute att = mBean.getAttribute("errorCount");
Integer i = (Integer) att.getValue();
errorCount += i;
} else if (metricName.equals(TOTAL_TIME)) {
EmsAttribute att = mBean.getAttribute("processingTime");
Long l = (Long) att.getValue();
processingTime += l;
}
}
if (metricName.equals(AVG_SERVLET_TIME)) {
result = (requestCount > 0) ? ((double) processingTime / (double) requestCount) : Double.NaN;
} else if (metricName.equals(MIN_SERVLET_TIME)) {
result = (min != Long.MAX_VALUE) ? (double) min : Double.NaN;
} else if (metricName.equals(MAX_SERVLET_TIME)) {
result = (max != 0) ? (double) max : Double.NaN;
} else if (metricName.equals(NUM_SERVLET_ERRORS)) {
result = (double) errorCount;
} else if (metricName.equals(NUM_SERVLET_REQUESTS)) {
result = (double) requestCount;
} else if (metricName.equals(TOTAL_TIME)) {
result = (double) processingTime;
} else {
// fallback
result = Double.NaN;
}
return result;
}
@Override
public OperationResult invokeOperation(String name, Configuration params) throws Exception {
WarOperation operation = getOperation(name);
if (this.jbossWebMBean == null) {
throw new IllegalStateException("Could not find jboss.web MBean for WAR '" + getApplicationName() + "'.");
}
if (operation == WarOperation.REVERT) {
try {
revertFromBackupFile();
return new OperationResult("Successfully reverted from backup");
} catch (Exception e) {
throw new RuntimeException("Error reverting from Backup: " + e.getMessage());
}
}
// The following are MBean operations.
EmsOperation mbeanOperation = this.jbossWebMBean.getOperation(name, new Class[0]);
if (mbeanOperation == null) {
throw new IllegalStateException("Operation [" + name + "] not found on bean ["
+ this.jbossWebMBean.getBeanName() + "]");
}
// NOTE: None of the supported operations have any parameters or return values, which makes our job easier.
Object[] paramValues = new Object[0];
mbeanOperation.invoke(paramValues);
int state = (Integer) this.jbossWebMBean.getAttribute("state").refresh();
int expectedState = getExpectedPostExecutionState(operation);
// regardless of the new state, the avail may have changed, request an avail check
getResourceContext().getAvailabilityContext().requestAvailabilityCheck();
if (state != expectedState) {
throw new Exception("Failed to " + name + " webapp (value of the 'state' attribute of MBean '"
+ this.jbossWebMBean.getBeanName() + "' is " + state + ", not " + expectedState + ").");
}
return new OperationResult();
}
private static int getExpectedPostExecutionState(WarOperation operation) {
int expectedState;
switch (operation) {
case START:
case RELOAD: {
expectedState = JBossWebMBeanState.STARTED;
break;
}
case STOP: {
expectedState = JBossWebMBeanState.STOPPED;
break;
}
default: {
throw new IllegalStateException("Unsupported operation: " + operation); // will never happen
}
}
return expectedState;
}
private WarOperation getOperation(String name) {
try {
return WarOperation.valueOf(name.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid operation name: " + name);
}
}
/**
* Returns the the jboss.web MBean associated with this WAR (e.g.
* jboss.web:J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=//localhost/jmx-console), or null if the
* WAR has no corresponding jboss.web MBean.
*
* <p/>This will return null only in the rare case that a deployed WAR has no context root associated with it. An
* example of this is ROOT.war in the RHQ Server. rhq.ear maps rhq-portal.war to "/" and overrides ROOT.war's
* association with "/".
*
* @return the jboss.web MBean associated with this WAR (e.g.
* jboss.web:J2EEApplication=none,J2EEServer=none,j2eeType=WebModule,name=//localhost/jmx-console), or null
* if the WAR has no corresponding jboss.web MBean
*/
@Nullable
private EmsBean getJBossWebMBean() {
String jbossWebMBeanName = getJBossWebMBeanName();
if (jbossWebMBeanName != null) {
ObjectNameQueryUtility queryUtility = new ObjectNameQueryUtility(jbossWebMBeanName);
List<EmsBean> mBeans = getEmsConnection().queryBeans(queryUtility.getTranslatedQuery());
// There should only be one mBean for this match.
if (mBeans.size() == 1) {
return mBeans.get(0);
}
} else {
// This might be in Embedded in which during discovery it doesn't discover because they aren't deployed yet.
// Might re-get, or let it get picked up the next time a discovery occurs.
}
return null;
}
@Nullable
private String getJBossWebMBeanName() {
Configuration pluginConfig = getResourceContext().getPluginConfiguration();
String jbossWebMBeanName = pluginConfig.getSimpleValue(WarComponent.JBOSS_WEB_NAME, null);
if (jbossWebMBeanName == null) {
WarDeploymentInformation deploymentInformation = getDeploymentInformation();
if (deploymentInformation != null) {
jbossWebMBeanName = deploymentInformation.getJbossWebModuleMBeanObjectName();
WarDiscoveryHelper.setDeploymentInformation(pluginConfig, deploymentInformation);
}
}
return jbossWebMBeanName;
}
/**
* Virtual hosts for this app have the patten 'jboss.web:host=*,path=<ctx_root>,type=Manager'
* @param contextRoot
* @return
*/
private List<EmsBean> getVHosts(String contextRoot) {
return DeploymentUtility.getVHostsFromLocalManager(contextRoot, getEmsConnection());
}
private WarDeploymentInformation getDeploymentInformation() {
WarDeploymentInformation deploymentInformation = null;
EmsBean mBean = this.getEmsBean();
if (mBean != null && mBean.getBeanName() != null) {
String beanName = this.getEmsBean().getBeanName().getCanonicalName();
List<String> beanNameList = new ArrayList<String>();
beanNameList.add(beanName);
// deploymentInformation = DeploymentUtility.getWarDeploymentInformation(getEmsConnection(), beanNameList)
// .get(beanName); TODO fix this
}
return deploymentInformation;
}
private enum WarOperation {
START, STOP, RELOAD, REVERT
}
private interface JBossWebMBeanState {
int STOPPED = 0;
int STARTED = 1;
}
}