/* * 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.metrics.reporters; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; import javax.management.ObjectInstance; import javax.management.ObjectName; import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import com.codahale.metrics.Gauge; import com.codahale.metrics.JmxReporter; import com.codahale.metrics.MetricFilter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.MetricRegistryListener; import org.apache.solr.core.PluginInfo; import org.apache.solr.metrics.MetricsMap; import org.apache.solr.metrics.SolrMetricManager; import org.apache.solr.metrics.SolrMetricReporter; import org.apache.solr.util.JmxUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A {@link SolrMetricReporter} that finds (or creates) a MBeanServer from * the given configuration and registers metrics to it with JMX. * <p>NOTE: {@link JmxReporter} that this class uses exports only newly added metrics (it doesn't * process already existing metrics in a registry)</p> */ public class SolrJmxReporter extends SolrMetricReporter { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final ReporterClientCache<MBeanServer> serviceRegistry = new ReporterClientCache<>(); private String domain; private String agentId; private String serviceUrl; private String rootName; private List<String> filters = new ArrayList<>(); private JmxReporter reporter; private MetricRegistry registry; private MBeanServer mBeanServer; private MetricsMapListener listener; /** * Creates a new instance of {@link SolrJmxReporter}. * * @param registryName name of the registry to report */ public SolrJmxReporter(SolrMetricManager metricManager, String registryName) { super(metricManager, registryName); setDomain(registryName); } /** * Initializes the reporter by finding an MBeanServer * and registering the metricManager's metric registry. * * @param pluginInfo the configuration for the reporter */ @Override public synchronized void init(PluginInfo pluginInfo) { super.init(pluginInfo); if (!enabled) { log.info("Reporter disabled for registry " + registryName); return; } log.debug("Initializing for registry " + registryName); if (serviceUrl != null && agentId != null) { mBeanServer = JmxUtil.findFirstMBeanServer(); log.warn("No more than one of serviceUrl({}) and agentId({}) should be configured, using first MBeanServer instead of configuration.", serviceUrl, agentId, mBeanServer); } else if (serviceUrl != null) { // reuse existing services mBeanServer = serviceRegistry.getOrCreate(serviceUrl, () -> JmxUtil.findMBeanServerForServiceUrl(serviceUrl)); } else if (agentId != null) { mBeanServer = JmxUtil.findMBeanServerForAgentId(agentId); } else { mBeanServer = JmxUtil.findFirstMBeanServer(); log.debug("No serviceUrl or agentId was configured, using first MBeanServer: " + mBeanServer); } if (mBeanServer == null) { log.warn("No JMX server found. Not exposing Solr metrics via JMX."); return; } if (domain == null || domain.isEmpty()) { domain = registryName; } String fullDomain = domain; if (rootName != null && !rootName.isEmpty()) { fullDomain = rootName + "." + domain; } JmxObjectNameFactory jmxObjectNameFactory = new JmxObjectNameFactory(pluginInfo.name, fullDomain); registry = metricManager.registry(registryName); // filter out MetricsMap gauges - we have a better way of handling them MetricFilter mmFilter = (name, metric) -> !(metric instanceof MetricsMap); MetricFilter filter; if (filters.isEmpty()) { filter = mmFilter; } else { // apply also prefix filters SolrMetricManager.PrefixFilter prefixFilter = new SolrMetricManager.PrefixFilter(filters); filter = new SolrMetricManager.AndFilter(prefixFilter, mmFilter); } reporter = JmxReporter.forRegistry(registry) .registerWith(mBeanServer) .inDomain(fullDomain) .filter(filter) .createsObjectNamesWith(jmxObjectNameFactory) .build(); reporter.start(); // workaround for inability to register custom MBeans (to be available in metrics 4.0?) listener = new MetricsMapListener(mBeanServer, jmxObjectNameFactory); registry.addListener(listener); log.info("JMX monitoring for '" + fullDomain + "' (registry '" + registryName + "') enabled at server: " + mBeanServer); } /** * Stops the reporter from publishing metrics. */ @Override public synchronized void close() { if (reporter != null) { reporter.close(); reporter = null; } if (listener != null && registry != null) { registry.removeListener(listener); listener.close(); listener = null; } } /** * Validates that the reporter has been correctly configured. * Note that all configurable arguments are currently optional. * * @throws IllegalStateException if the reporter is not properly configured */ @Override protected void validate() throws IllegalStateException { // Nothing to validate } /** * Set root name of the JMX hierarchy for this reporter. Default (null or empty) is none, ie. * the hierarchy will start from the domain name. * @param rootName root name of the JMX name hierarchy, or null or empty for default. */ public void setRootName(String rootName) { this.rootName = rootName; } /** * Sets the domain with which MBeans are published. If none is set, * the domain defaults to the name of the registry. * * @param domain the domain */ public void setDomain(String domain) { if (domain != null) { this.domain = domain; } else { this.domain = registryName; } } /** * Sets the service url for a JMX server. * Note that this configuration is optional. * * @param serviceUrl the service url */ public void setServiceUrl(String serviceUrl) { this.serviceUrl = serviceUrl; } /** * Sets the agent id for a JMX server. * Note that this configuration is optional. * * @param agentId the agent id */ public void setAgentId(String agentId) { this.agentId = agentId; } /** * Return configured agentId or null. */ public String getAgentId() { return agentId; } /** * Return configured serviceUrl or null. */ public String getServiceUrl() { return serviceUrl; } /** * Return configured domain or null. */ public String getDomain() { return domain; } /** * Report only metrics with names matching any of the prefix filters. * @param filters list of 0 or more prefixes. If the list is empty then * all names will match. */ public void setFilter(List<String> filters) { if (filters == null || filters.isEmpty()) { return; } this.filters.addAll(filters); } public void setFilter(String filter) { if (filter != null && !filter.isEmpty()) { this.filters.add(filter); } } /** * Return the reporter's MBeanServer. * * @return the reporter's MBeanServer */ public MBeanServer getMBeanServer() { return mBeanServer; } /** * For unit tests. * @return true if this reporter is actively reporting metrics to JMX. */ public boolean isActive() { return reporter != null; } @Override public String toString() { return String.format(Locale.ENGLISH, "[%s@%s: rootName = %s, domain = %s, service url = %s, agent id = %s]", getClass().getName(), Integer.toHexString(hashCode()), rootName, domain, serviceUrl, agentId); } private static class MetricsMapListener extends MetricRegistryListener.Base { MBeanServer server; JmxObjectNameFactory nameFactory; // keep the names so that we can unregister them on core close Set<ObjectName> registered = new HashSet<>(); MetricsMapListener(MBeanServer server, JmxObjectNameFactory nameFactory) { this.server = server; this.nameFactory = nameFactory; } @Override public void onGaugeAdded(String name, Gauge<?> gauge) { if (!(gauge instanceof MetricsMap)) { return; } synchronized (server) { try { ObjectName objectName = nameFactory.createName("gauges", nameFactory.getDomain(), name); log.debug("REGISTER " + objectName); if (registered.contains(objectName) || server.isRegistered(objectName)) { log.debug("-unregistering old instance of " + objectName); try { server.unregisterMBean(objectName); } catch (InstanceNotFoundException e) { // ignore } } // some MBean servers re-write object name to include additional properties ObjectInstance instance = server.registerMBean(gauge, objectName); if (instance != null) { registered.add(instance.getObjectName()); } } catch (Exception e) { log.warn("bean registration error", e); } } } public void close() { synchronized (server) { for (ObjectName name : registered) { try { if (server.isRegistered(name)) { server.unregisterMBean(name); } } catch (Exception e) { log.debug("bean unregistration error", e); } } registered.clear(); } } } }