SOLR-256 -- Support exposing Solr statistics through JMX

git-svn-id: https://svn.apache.org/repos/asf/lucene/solr/trunk@680795 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Shalin Shekhar Mangar 2008-07-29 18:13:57 +00:00
parent ed366c71ad
commit 2d13d9eab4
10 changed files with 628 additions and 12 deletions

View File

@ -325,6 +325,8 @@ New Features
62. SOLR-611: Expose sort_values returned by QueryComponent in SolrJ's QueryResponse (Dan Rosher via shalin)
63. SOLR-256: Support exposing Solr statistics through JMX (Sharad Agrawal, shalin)
Changes in runtime behavior
1. SOLR-559: use Lucene updateDocument, deleteDocuments methods. This

View File

@ -117,6 +117,20 @@
<unlockOnStartup>false</unlockOnStartup>
</mainIndex>
<!-- Enables JMX if and only if an existing MBeanServer is found, use
this if you want to configure JMX through JVM parameters. Remove
this to disable exposing Solr configuration and statistics to JMX.
If you want to connect to a particular server, specify the agentId
e.g. <jmx agentId="myAgent" />
If you want to start a new MBeanServer, specify the serviceUrl
e.g <jmx serviceurl="service:jmx:rmi:///jndi/rmi://localhost:9999/solr" />
For more details see http://wiki.apache.org/solr/SolrJmx
-->
<jmx />
<!-- the default high-performance update handler -->
<updateHandler class="solr.DirectUpdateHandler2">

View File

@ -0,0 +1,295 @@
/**
* 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 org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrConfig.JmxConfiguration;
import javax.management.*;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>
* Responsible for finding (or creating) a MBeanServer from given configuration
* and registering all SolrInfoMBean objects with JMX.
* </p>
* <p/>
* <p>
* Please see http://wiki.apache.org/solr/SolrJmx for instructions on usage and configuration
* </p>
*
* @version $Id$
* @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 = Logger.getLogger(JmxMonitoredMap.class
.getName());
private MBeanServer server = null;
private String jmxRootName;
public JmxMonitoredMap(String coreName, JmxConfiguration jmxConfig) {
jmxRootName = "solr" + (coreName == null ? "" : "/" + coreName);
if (jmxConfig.agentId != null && jmxConfig.serviceUrl != null) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR,
"Incorrect JMX Configuration in solrconfig.xml, both agentId and serviceUrl cannot be specified at the same time");
}
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
.info("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) {
for (Map.Entry<String, SolrInfoMBean> entry : entrySet()) {
unregister(entry.getKey(), entry.getValue());
}
}
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(infoBean);
server.registerMBean(mbean, name);
} catch (Exception e) {
LOG.log(Level.WARNING, "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.log(Level.WARNING, "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)) {
server.unregisterMBean(name);
} else {
LOG.info("Failed to unregister mbean: " + key
+ " because it was not registered");
}
} 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<String, String>();
map.put("type", key);
map.put("id", infoBean.getName());
return ObjectName.getInstance(jmxRootName, map);
}
/**
* 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;
public SolrDynamicMBean(SolrInfoMBean managedResource) {
this.infoBean = managedResource;
staticStats = new HashSet<String>();
// For which getters are already available in SolrInfoMBean
staticStats.add("name");
staticStats.add("version");
staticStats.add("description");
staticStats.add("category");
staticStats.add("sourceId");
staticStats.add("source");
}
public MBeanInfo getMBeanInfo() {
ArrayList<MBeanAttributeInfo> attrInfoList = new ArrayList<MBeanAttributeInfo>();
for (String stat : staticStats) {
attrInfoList.add(new MBeanAttributeInfo(stat, String.class.getName(),
null, true, false, false));
}
try {
NamedList dynamicStats = infoBean.getStatistics();
if (dynamicStats != null) {
for (int i = 0; i < dynamicStats.size(); i++) {
String name = dynamicStats.getName(i);
if (!staticStats.contains(name))
attrInfoList.add(new MBeanAttributeInfo(dynamicStats.getName(i),
String.class.getName(), null, true, false, false));
}
}
} catch (Exception e) {
LOG.log(Level.WARNING, "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);
}
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
Object val;
if (staticStats.contains(attribute) && attribute != null
& attribute.length() > 0) {
try {
String getter = "get" + attribute.substring(0, 1).toUpperCase()
+ attribute.substring(1);
Method meth = infoBean.getClass().getMethod(getter);
val = meth.invoke(infoBean);
} catch (Exception e) {
throw new AttributeNotFoundException(attribute);
}
} else {
NamedList list = infoBean.getStatistics();
val = list.get(attribute);
}
if (val != null)
return val.toString();
else
return val;
}
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.warning("Could not get attibute " + attribute);
}
}
return list;
}
public void setAttribute(Attribute attribute)
throws AttributeNotFoundException, InvalidAttributeValueException,
MBeanException, ReflectionException {
throw new UnsupportedOperationException("Operation not Supported");
}
public AttributeList setAttributes(AttributeList attributes) {
throw new UnsupportedOperationException("Operation not Supported");
}
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
throw new UnsupportedOperationException("Operation not Supported");
}
}
}

View File

@ -26,6 +26,7 @@ import org.apache.solr.search.CacheConfig;
import org.apache.solr.update.SolrIndexConfig;
import org.apache.lucene.search.BooleanQuery;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
@ -133,6 +134,15 @@ public class SolrConfig extends Config {
pingQueryParams = readPingQueryParams(this);
httpCachingConfig = new HttpCachingConfig(this);
Node jmx = (Node) getNode("jmx", false);
if (jmx != null) {
jmxConfig = new JmxConfiguration(true, get("jmx/@agentId", null), get(
"jmx/@serviceUrl", null));
} else {
jmxConfig = new JmxConfiguration(false, null, null);
}
Config.log.info("Loaded SolrConfig: " + name);
// TODO -- at solr 2.0. this should go away
@ -162,6 +172,9 @@ public class SolrConfig extends Config {
public final SolrIndexConfig defaultIndexConfig;
public final SolrIndexConfig mainIndexConfig;
//JMX configuration
public final JmxConfiguration jmxConfig;
private final HttpCachingConfig httpCachingConfig;
public HttpCachingConfig getHttpCachingConfig() {
return httpCachingConfig;
@ -197,6 +210,19 @@ public class SolrConfig extends Config {
return new LocalSolrQueryRequest(core, pingQueryParams);
}
public static class JmxConfiguration {
public boolean enabled = false;
public String agentId;
public String serviceUrl;
public JmxConfiguration(boolean enabled, String agentId, String serviceUrl) {
this.enabled = enabled;
this.agentId = agentId;
this.serviceUrl = serviceUrl;
}
}
public static class HttpCachingConfig {

View File

@ -85,7 +85,7 @@ public final class SolrCore {
private final SolrHighlighter highlighter;
private final Map<String,SearchComponent> searchComponents;
private final Map<String,UpdateRequestProcessorChain> updateProcessorChains;
private final Map<String,SolrInfoMBean> infoRegistry = new java.util.HashMap<String,SolrInfoMBean>();
private final Map<String, SolrInfoMBean> infoRegistry;
public long getStartTime() { return startTime; }
@ -184,6 +184,7 @@ public final class SolrCore {
}
/**
* @return the Info Registry map which contains SolrInfoMBean objects keyed by name
* @since solr 1.3
*/
public Map<String, SolrInfoMBean> getInfoRegistry() {
@ -394,6 +395,14 @@ public final class SolrCore {
schema = new IndexSchema(config, IndexSchema.DEFAULT_SCHEMA_FILE, null);
}
//Initialize JMX
if (config.jmxConfig.enabled) {
infoRegistry = new JmxMonitoredMap<String, SolrInfoMBean>(name, config.jmxConfig);
} else {
log.info("JMX monitoring not detected for core: " + name);
infoRegistry = new LinkedHashMap<String, SolrInfoMBean>();
}
this.schema = schema;
this.dataDir = dataDir;
this.solrConfig = config;
@ -444,6 +453,7 @@ public final class SolrCore {
solrConfig.get("updateHandler/@class", DirectUpdateHandler.class.getName())
);
infoRegistry.put("updateHandler", updateHandler);
// Finally tell anyone who wants to know
loader.inform( loader );
@ -552,6 +562,7 @@ public final class SolrCore {
* 1. searcher
* 2. updateHandler
* 3. all CloseHooks will be notified
* 4. All MBeans will be unregistered from MBeanServer if JMX was enabled
*/
public void close() {
log.info(logid+" CLOSING SolrCore!");
@ -575,6 +586,11 @@ public final class SolrCore {
hook.close( this );
}
}
try {
infoRegistry.clear();
} catch (Exception e) {
SolrException.log(log, e);
}
}
public boolean isClosed() {

View File

@ -158,8 +158,6 @@ public class SolrIndexSearcher extends Searcher implements SolrInfoMBean {
// for DocSets
HASHSET_INVERSE_LOAD_FACTOR = solrConfig.hashSetInverseLoadFactor;
HASHDOCSET_MAXSIZE = solrConfig.hashDocSetMaxSize;
// register self
core.getInfoRegistry().put(this.name, this);
}
@ -171,6 +169,9 @@ public class SolrIndexSearcher extends Searcher implements SolrInfoMBean {
/** Register sub-objects such as caches
*/
public void register() {
// register self
core.getInfoRegistry().put("searcher", this);
core.getInfoRegistry().put(name, this);
for (SolrCache cache : cacheList) {
cache.setState(SolrCache.State.LIVE);
core.getInfoRegistry().put(cache.name(), cache);
@ -184,9 +185,6 @@ public class SolrIndexSearcher extends Searcher implements SolrInfoMBean {
* In particular, the underlying reader and any cache's in use are closed.
*/
public void close() throws IOException {
// unregister first, so no management actions are tried on a closing searcher.
core.getInfoRegistry().remove(name);
if (cachingEnabled) {
StringBuilder sb = new StringBuilder();
sb.append("Closing ").append(name);
@ -198,6 +196,7 @@ public class SolrIndexSearcher extends Searcher implements SolrInfoMBean {
} else {
log.fine("Closing " + name);
}
core.getInfoRegistry().remove(name);
try {
searcher.close();
}

View File

@ -116,7 +116,6 @@ public abstract class UpdateHandler implements SolrInfoMBean {
idFieldType = idField!=null ? idField.getType() : null;
idTerm = idField!=null ? new Term(idField.getName(),"") : null;
parseEventListeners();
core.getInfoRegistry().put("updateHandler", this);
}
protected SolrIndexWriter createMainIndexWriter(String name, boolean removeAllExisting) throws IOException {

View File

@ -0,0 +1,108 @@
/**
* 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 org.apache.solr.core.JmxMonitoredMap.SolrDynamicMBean;
import org.apache.solr.util.AbstractSolrTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Set;
/**
* Test for JMX Integration
*
* @version $Id$
* @since solr 1.3
*/
public class TestJmxIntegration extends AbstractSolrTestCase {
@Override
public String getSchemaFile() {
return "schema.xml";
}
@Override
public String getSolrConfigFile() {
return "solrconfig.xml";
}
@Before
public void setUp() throws Exception {
// Make sure that at least one MBeanServer is available
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
super.setUp();
}
@After
public void tearDown() throws Exception {
super.tearDown();
}
@Test
public void testJmxRegistration() throws Exception {
List<MBeanServer> servers = MBeanServerFactory.findMBeanServer(null);
System.out.println("Servers: " + servers);
assertNotNull("MBeanServers were null", servers);
assertFalse("No MBeanServer was found", servers.isEmpty());
MBeanServer mbeanServer = servers.get(0);
assertTrue("No MBeans found in server", mbeanServer.getMBeanCount() > 0);
Set<ObjectInstance> objects = mbeanServer.queryMBeans(null, null);
assertFalse("No SolrInfoMBean objects found in mbean server", objects
.isEmpty());
for (ObjectInstance o : objects) {
MBeanInfo mbeanInfo = mbeanServer.getMBeanInfo(o.getObjectName());
if (mbeanInfo.getClassName().endsWith(SolrDynamicMBean.class.getName())) {
assertTrue("No Attributes found for mbean: " + mbeanInfo, mbeanInfo
.getAttributes().length > 0);
}
}
}
@Test
public void testJmxUpdate() throws Exception {
List<MBeanServer> servers = MBeanServerFactory.findMBeanServer(null);
MBeanServer mbeanServer = servers.get(0);
Set<ObjectInstance> objects = mbeanServer.queryMBeans(null, Query.match(
Query.attr("numDocs"), Query.value("[0-9]")));
assertFalse("No MBean for SolrIndexSearcher found in MBeanServer", objects
.isEmpty());
int oldNumDocs = Integer.valueOf((String) mbeanServer.getAttribute(objects
.iterator().next().getObjectName(), "numDocs"));
assertU(adoc("id", "1"));
assertU(commit());
objects = mbeanServer.queryMBeans(null, Query.match(Query.attr("numDocs"),
Query.value("[0-9]")));
assertFalse("No MBean for SolrIndexSearcher found in MBeanServer", objects
.isEmpty());
int numDocs = Integer.valueOf((String) mbeanServer.getAttribute(objects
.iterator().next().getObjectName(), "numDocs"));
assertTrue("New numDocs is same as old numDocs as reported by JMX",
numDocs > oldNumDocs);
}
}

View File

@ -0,0 +1,155 @@
/**
* 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 org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrConfig.JmxConfiguration;
import org.junit.After;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.Query;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.net.ServerSocket;
import java.net.URL;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.Set;
/**
* Test for JmxMonitoredMap
*
* @version $Id$
* @since solr 1.3
*/
public class TestJmxMonitoredMap {
private int port = 0;
private JMXConnector connector;
private MBeanServerConnection mbeanServer;
private JmxMonitoredMap<String, SolrInfoMBean> monitoredMap;
@Before
public void setUp() throws Exception {
for (int i = 0; i < 5; i++) {
try {
ServerSocket server = new ServerSocket(0);
port = server.getLocalPort();
server.close();
System.out.println("Using port: " + port);
try {
LocateRegistry.createRegistry(port);
} catch (RemoteException e) {
}
String url = "service:jmx:rmi:///jndi/rmi://:" + port + "/solrjmx";
JmxConfiguration config = new JmxConfiguration(true, null, url);
monitoredMap = new JmxMonitoredMap<String, SolrInfoMBean>(null, config);
JMXServiceURL u = new JMXServiceURL(url);
connector = JMXConnectorFactory.connect(u);
mbeanServer = connector.getMBeanServerConnection();
break;
} catch (Exception e) {
}
}
}
@After
public void tearDown() throws Exception {
try {
connector.close();
} catch (Exception e) {
}
}
@Test
public void testPutRemoveClear() throws Exception {
MockInfoMBean mock = new MockInfoMBean();
monitoredMap.put("mock", mock);
Set<ObjectInstance> objects = mbeanServer.queryMBeans(null, Query.match(
Query.attr("name"), Query.value("mock")));
assertFalse("No MBean for mock object found in MBeanServer", objects
.isEmpty());
monitoredMap.remove("mock");
objects = mbeanServer.queryMBeans(null, Query.match(Query.attr("name"),
Query.value("mock")));
assertTrue("MBean for mock object found in MBeanServer even after removal",
objects.isEmpty());
monitoredMap.put("mock", mock);
monitoredMap.put("mock2", mock);
objects = mbeanServer.queryMBeans(null, Query.match(Query.attr("name"),
Query.value("mock")));
assertFalse("No MBean for mock object found in MBeanServer", objects
.isEmpty());
monitoredMap.clear();
objects = mbeanServer.queryMBeans(null, Query.match(Query.attr("name"),
Query.value("mock")));
assertTrue(
"MBean for mock object found in MBeanServer even after clear has been called",
objects.isEmpty());
}
private class MockInfoMBean implements SolrInfoMBean {
public String getName() {
return "mock";
}
public Category getCategory() {
return Category.OTHER;
}
public String getDescription() {
return "mock";
}
public URL[] getDocs() {
// TODO Auto-generated method stub
return null;
}
public String getVersion() {
return "mock";
}
public String getSource() {
return "mock";
}
@SuppressWarnings("unchecked")
public NamedList getStatistics() {
return null;
}
public String getSourceId() {
return "mock";
}
}
}

View File

@ -24,6 +24,8 @@
<config>
<jmx />
<!-- Used to specify an alternate directory to hold all index data.
It defaults to "index" if not present, and should probably
not be changed if replication is in use. -->