/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.solr.core;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.Query;
import javax.management.QueryExp;
import javax.management.ReflectionException;
import javax.management.openmbean.OpenMBeanAttributeInfoSupport;
import javax.management.openmbean.OpenType;
import javax.management.openmbean.SimpleType;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrConfig.JmxConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.params.CommonParams.NAME;
/**
* <p>
* Responsible for finding (or creating) a MBeanServer from given configuration
* and registering all SolrInfoMBean objects with JMX.
* </p>
* <p>
* Please see http://wiki.apache.org/solr/SolrJmx for instructions on usage and configuration
* </p>
*
*
* @see org.apache.solr.core.SolrConfig.JmxConfiguration
* @since solr 1.3
*/
public class JmxMonitoredMap<K, V> extends
ConcurrentHashMap<String, SolrInfoMBean> {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
// set to true to use cached statistics NamedLists between getMBeanInfo calls to work
// around over calling getStatistics on MBeanInfos when iterating over all attributes (SOLR-6586)
private boolean useCachedStatsBetweenGetMBeanInfoCalls = Boolean.getBoolean("useCachedStatsBetweenGetMBeanInfoCalls");
private MBeanServer server = null;
private String jmxRootName;
private String coreHashCode;
public JmxMonitoredMap(String coreName, String coreHashCode,
final JmxConfiguration jmxConfig) {
this.coreHashCode = coreHashCode;
jmxRootName = (null != jmxConfig.rootName ?
jmxConfig.rootName
: ("solr" + (null != coreName ? "/" + coreName : "")));
if (jmxConfig.serviceUrl == null) {
List<MBeanServer> servers = null;
if (jmxConfig.agentId == null) {
// Try to find the first MBeanServer
servers = MBeanServerFactory.findMBeanServer(null);
} else if (jmxConfig.agentId != null) {
// Try to find the first MBean server with the given agentId
servers = MBeanServerFactory.findMBeanServer(jmxConfig.agentId);
// throw Exception if no servers were found with the given agentId
if (servers == null || servers.isEmpty())
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"No JMX Servers found with agentId: " + jmxConfig.agentId);
}
if (servers == null || servers.isEmpty()) {
log.debug("No JMX servers found, not exposing Solr information with JMX.");
return;
}
server = servers.get(0);
log.info("JMX monitoring is enabled. Adding Solr mbeans to JMX Server: "
+ server);
} else {
try {
// Create a new MBeanServer with the given serviceUrl
server = MBeanServerFactory.newMBeanServer();
JMXConnectorServer connector = JMXConnectorServerFactory
.newJMXConnectorServer(new JMXServiceURL(jmxConfig.serviceUrl),
null, server);
connector.start();
log.info("JMX monitoring is enabled at " + jmxConfig.serviceUrl);
} catch (Exception e) {
// Release the reference
server = null;
throw new RuntimeException("Could not start JMX monitoring ", e);
}
}
}
/**
* Clears the map and unregisters all SolrInfoMBeans in the map from
* MBeanServer
*/
@Override
public void clear() {
if (server != null) {
QueryExp exp = Query.eq(Query.attr("coreHashCode"), Query.value(coreHashCode));
Set<ObjectName> objectNames = null;
try {
ObjectName instance = new ObjectName(jmxRootName + ":*");
objectNames = server.queryNames(instance, exp);
} catch (Exception e) {
log.warn("Exception querying for mbeans", e);
}
if (objectNames != null) {
for (ObjectName name : objectNames) {
try {
server.unregisterMBean(name);
} catch (Exception e) {
log.warn("Exception un-registering mbean {}", name, e);
}
}
}
}
super.clear();
}
/**
* Adds the SolrInfoMBean to the map and registers the given SolrInfoMBean
* instance with the MBeanServer defined for this core. If a SolrInfoMBean is
* already registered with the MBeanServer then it is unregistered and then
* re-registered.
*
* @param key the JMX type name for this SolrInfoMBean
* @param infoBean the SolrInfoMBean instance to be registered
*/
@Override
public SolrInfoMBean put(String key, SolrInfoMBean infoBean) {
if (server != null && infoBean != null) {
try {
ObjectName name = getObjectName(key, infoBean);
if (server.isRegistered(name))
server.unregisterMBean(name);
SolrDynamicMBean mbean = new SolrDynamicMBean(coreHashCode, infoBean, useCachedStatsBetweenGetMBeanInfoCalls);
server.registerMBean(mbean, name);
} catch (Exception e) {
log.warn( "Failed to register info bean: " + key, e);
}
}
return super.put(key, infoBean);
}
/**
* Removes the SolrInfoMBean object at the given key and unregisters it from
* MBeanServer
*
* @param key the JMX type name for this SolrInfoMBean
*/
@Override
public SolrInfoMBean remove(Object key) {
SolrInfoMBean infoBean = get(key);
if (infoBean != null) {
try {
unregister((String) key, infoBean);
} catch (RuntimeException e) {
log.warn( "Failed to unregister info bean: " + key, e);
}
}
return super.remove(key);
}
private void unregister(String key, SolrInfoMBean infoBean) {
if (server == null)
return;
try {
ObjectName name = getObjectName(key, infoBean);
if (server.isRegistered(name) && coreHashCode.equals(server.getAttribute(name, "coreHashCode"))) {
server.unregisterMBean(name);
}
} catch (Exception e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"Failed to unregister info bean: " + key, e);
}
}
private ObjectName getObjectName(String key, SolrInfoMBean infoBean)
throws MalformedObjectNameException {
Hashtable<String, String> map = new Hashtable<>();
map.put("type", key);
if (infoBean.getName() != null && !"".equals(infoBean.getName())) {
map.put("id", infoBean.getName());
}
return ObjectName.getInstance(jmxRootName, map);
}
/** For test verification */
public MBeanServer getServer() {
return server;
}
/**
* DynamicMBean is used to dynamically expose all SolrInfoMBean
* getStatistics() NameList keys as String getters.
*/
static class SolrDynamicMBean implements DynamicMBean {
private SolrInfoMBean infoBean;
private HashSet<String> staticStats;
private String coreHashCode;
private volatile NamedList cachedDynamicStats;
private boolean useCachedStatsBetweenGetMBeanInfoCalls;
public SolrDynamicMBean(String coreHashCode, SolrInfoMBean managedResource) {
this(coreHashCode, managedResource, false);
}
public SolrDynamicMBean(String coreHashCode, SolrInfoMBean managedResource, boolean useCachedStatsBetweenGetMBeanInfoCalls) {
this.useCachedStatsBetweenGetMBeanInfoCalls = useCachedStatsBetweenGetMBeanInfoCalls;
if (managedResource instanceof JmxAugmentedSolrInfoMBean) {
final JmxAugmentedSolrInfoMBean jmxSpecific = (JmxAugmentedSolrInfoMBean)managedResource;
this.infoBean = new SolrInfoMBeanWrapper(jmxSpecific) {
@Override
public NamedList getStatistics() { return jmxSpecific.getStatisticsForJmx(); }
};
} else {
this.infoBean = managedResource;
}
staticStats = new HashSet<>();
// For which getters are already available in SolrInfoMBean
staticStats.add(NAME);
staticStats.add("version");
staticStats.add("description");
staticStats.add("category");
staticStats.add("source");
this.coreHashCode = coreHashCode;
}
@Override
public MBeanInfo getMBeanInfo() {
ArrayList<MBeanAttributeInfo> attrInfoList = new ArrayList<>();
for (String stat : staticStats) {
attrInfoList.add(new MBeanAttributeInfo(stat, String.class.getName(),
null, true, false, false));
}
// add core's hashcode
attrInfoList.add(new MBeanAttributeInfo("coreHashCode", String.class.getName(),
null, true, false, false));
try {
NamedList dynamicStats = infoBean.getStatistics();
if (useCachedStatsBetweenGetMBeanInfoCalls) {
cachedDynamicStats = dynamicStats;
}
if (dynamicStats != null) {
for (int i = 0; i < dynamicStats.size(); i++) {
String name = dynamicStats.getName(i);
if (staticStats.contains(name)) {
continue;
}
Class type = dynamicStats.get(name).getClass();
OpenType typeBox = determineType(type);
if (type.equals(String.class) || typeBox == null) {
attrInfoList.add(new MBeanAttributeInfo(dynamicStats.getName(i),
String.class.getName(), null, true, false, false));
} else {
attrInfoList.add(new OpenMBeanAttributeInfoSupport(
dynamicStats.getName(i), dynamicStats.getName(i), typeBox,
true, false, false));
}
}
}
} catch (Exception e) {
// don't log issue if the core is closing
if (!(SolrException.getRootCause(e) instanceof AlreadyClosedException))
log.warn("Could not getStatistics on info bean {}", infoBean.getName(), e);
}
MBeanAttributeInfo[] attrInfoArr = attrInfoList
.toArray(new MBeanAttributeInfo[attrInfoList.size()]);
return new MBeanInfo(getClass().getName(), infoBean
.getDescription(), attrInfoArr, null, null, null);
}
private OpenType determineType(Class type) {
try {
for (Field field : SimpleType.class.getFields()) {
if (field.getType().equals(SimpleType.class)) {
SimpleType candidate = (SimpleType) field.get(SimpleType.class);
if (candidate.getTypeName().equals(type.getName())) {
return candidate;
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
Object val;
if ("coreHashCode".equals(attribute)) {
val = coreHashCode;
} else if (staticStats.contains(attribute) && attribute != null
&& attribute.length() > 0) {
try {
String getter = "get" + attribute.substring(0, 1).toUpperCase(Locale.ROOT)
+ attribute.substring(1);
Method meth = infoBean.getClass().getMethod(getter);
val = meth.invoke(infoBean);
} catch (Exception e) {
throw new AttributeNotFoundException(attribute);
}
} else {
NamedList stats = null;
if (useCachedStatsBetweenGetMBeanInfoCalls) {
NamedList cachedStats = this.cachedDynamicStats;
if (cachedStats != null) {
stats = cachedStats;
}
}
if (stats == null) {
stats = infoBean.getStatistics();
}
val = stats.get(attribute);
}
if (val != null) {
// It's String or one of the simple types, just return it as JMX suggests direct support for such types
for (String simpleTypeName : SimpleType.ALLOWED_CLASSNAMES_LIST) {
if (val.getClass().getName().equals(simpleTypeName)) {
return val;
}
}
// It's an arbitrary object which could be something complex and odd, return its toString, assuming that is
// a workable representation of the object
return val.toString();
}
return null;
}
@Override
public AttributeList getAttributes(String[] attributes) {
AttributeList list = new AttributeList();
for (String attribute : attributes) {
try {
list.add(new Attribute(attribute, getAttribute(attribute)));
} catch (Exception e) {
log.warn("Could not get attribute " + attribute);
}
}
return list;
}
@Override
public void setAttribute(Attribute attribute)
throws AttributeNotFoundException, InvalidAttributeValueException,
MBeanException, ReflectionException {
throw new UnsupportedOperationException("Operation not Supported");
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
throw new UnsupportedOperationException("Operation not Supported");
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
throw new UnsupportedOperationException("Operation not Supported");
}
}
/**
* SolrInfoMBean that provides JMX-specific statistics. Used, for example,
* if generating full statistics is expensive; the expensive statistics can
* be generated normally for use with the web ui, while an abbreviated version
* are generated for period jmx use.
*/
public interface JmxAugmentedSolrInfoMBean extends SolrInfoMBean {
/**
* JMX-specific statistics
*/
public NamedList getStatisticsForJmx();
}
}