/*
* 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.flink.metrics.jmx;
import org.apache.flink.metrics.CharacterFilter;
import org.apache.flink.metrics.Counter;
import org.apache.flink.metrics.Gauge;
import org.apache.flink.metrics.Histogram;
import org.apache.flink.metrics.Meter;
import org.apache.flink.metrics.Metric;
import org.apache.flink.metrics.MetricConfig;
import org.apache.flink.metrics.MetricGroup;
import org.apache.flink.metrics.reporter.MetricReporter;
import org.apache.flink.runtime.metrics.groups.AbstractMetricGroup;
import org.apache.flink.runtime.metrics.groups.FrontMetricGroup;
import org.apache.flink.util.NetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.InstanceAlreadyExistsException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.rmi.NoSuchObjectException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
/**
* {@link MetricReporter} that exports {@link Metric Metrics} via JMX.
*
* Largely based on the JmxReporter class of the dropwizard metrics library
* https://github.com/dropwizard/metrics/blob/master/metrics-core/src/main/java/io/dropwizard/metrics/JmxReporter.java
*/
public class JMXReporter implements MetricReporter {
static final String JMX_DOMAIN_PREFIX = "org.apache.flink.";
public static final String ARG_PORT = "port";
private static final Logger LOG = LoggerFactory.getLogger(JMXReporter.class);
private static final CharacterFilter CHARACTER_FILTER = new CharacterFilter() {
@Override
public String filterCharacters(String input) {
return replaceInvalidChars(input);
}
};
// ------------------------------------------------------------------------
/** The server where the management beans are registered and deregistered */
private final MBeanServer mBeanServer;
/** The names under which the registered metrics have been added to the MBeanServer */
private final Map<Metric, ObjectName> registeredMetrics;
/** The server to which JMX clients connect to. ALlows for better control over port usage. */
private JMXServer jmxServer;
/**
* Creates a new JMXReporter
*/
public JMXReporter() {
this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
this.registeredMetrics = new HashMap<>();
}
// ------------------------------------------------------------------------
// life cycle
// ------------------------------------------------------------------------
@Override
public void open(MetricConfig config) {
String portsConfig = config.getString(ARG_PORT, null);
if (portsConfig != null) {
Iterator<Integer> ports = NetUtils.getPortRangeFromString(portsConfig);
JMXServer server = new JMXServer();
while (ports.hasNext()) {
int port = ports.next();
try {
server.start(port);
LOG.info("Started JMX server on port " + port + ".");
// only set our field if the server was actually started
jmxServer = server;
break;
} catch (IOException ioe) { //assume port conflict
LOG.debug("Could not start JMX server on port " + port + ".", ioe);
try {
server.stop();
} catch (Exception e) {
LOG.debug("Could not stop JMX server.", e);
}
}
}
if (jmxServer == null) {
throw new RuntimeException("Could not start JMX server on any configured port. Ports: " + portsConfig);
}
}
LOG.info("Configured JMXReporter with {port:{}}", portsConfig);
}
@Override
public void close() {
if (jmxServer != null) {
try {
jmxServer.stop();
} catch (IOException e) {
LOG.error("Failed to stop JMX server.", e);
}
}
}
public int getPort() {
if (jmxServer == null) {
throw new NullPointerException("No server was opened. Did you specify a port?");
}
return jmxServer.port;
}
// ------------------------------------------------------------------------
// adding / removing metrics
// ------------------------------------------------------------------------
@Override
public void notifyOfAddedMetric(Metric metric, String metricName, MetricGroup group) {
final String domain = generateJmxDomain(metricName, group);
final Hashtable<String, String> table = generateJmxTable(group.getAllVariables());
AbstractBean jmxMetric;
ObjectName jmxName;
try {
jmxName = new ObjectName(domain, table);
} catch (MalformedObjectNameException e) {
/**
* There is an implementation error on our side if this occurs. Either the domain was modified and no longer
* conforms to the JMX domain rules or the table wasn't properly generated.
*/
LOG.debug("Implementation error. The domain or table does not conform to JMX rules." , e);
return;
}
if (metric instanceof Gauge) {
jmxMetric = new JmxGauge((Gauge<?>) metric);
} else if (metric instanceof Counter) {
jmxMetric = new JmxCounter((Counter) metric);
} else if (metric instanceof Histogram) {
jmxMetric = new JmxHistogram((Histogram) metric);
} else if (metric instanceof Meter) {
jmxMetric = new JmxMeter((Meter) metric);
} else {
LOG.error("Cannot add unknown metric type: {}. This indicates that the metric type " +
"is not supported by this reporter.", metric.getClass().getName());
return;
}
try {
synchronized (this) {
mBeanServer.registerMBean(jmxMetric, jmxName);
registeredMetrics.put(metric, jmxName);
}
} catch (NotCompliantMBeanException e) {
// implementation error on our side
LOG.debug("Metric did not comply with JMX MBean rules.", e);
} catch (InstanceAlreadyExistsException e) {
LOG.warn("A metric with the name " + jmxName + " was already registered.", e);
} catch (Throwable t) {
LOG.warn("Failed to register metric", t);
}
}
@Override
public void notifyOfRemovedMetric(Metric metric, String metricName, MetricGroup group) {
try {
synchronized (this) {
final ObjectName jmxName = registeredMetrics.remove(metric);
// remove the metric if it is known. if it is not known, ignore the request
if (jmxName != null) {
mBeanServer.unregisterMBean(jmxName);
}
}
} catch (InstanceNotFoundException e) {
// alright then
} catch (Throwable t) {
// never propagate exceptions - the metrics reporter should not affect the stability
// of the running system
LOG.error("Un-registering metric failed", t);
}
}
// ------------------------------------------------------------------------
// Utilities
// ------------------------------------------------------------------------
static Hashtable<String, String> generateJmxTable(Map<String, String> variables) {
Hashtable<String, String> ht = new Hashtable<>(variables.size());
for (Map.Entry<String, String> variable : variables.entrySet()) {
ht.put(replaceInvalidChars(variable.getKey()), replaceInvalidChars(variable.getValue()));
}
return ht;
}
static String generateJmxDomain(String metricName, MetricGroup group) {
return JMX_DOMAIN_PREFIX + ((FrontMetricGroup<AbstractMetricGroup<?>>) group).getLogicalScope(CHARACTER_FILTER, '.') + '.' + metricName;
}
/**
* Lightweight method to replace unsupported characters.
* If the string does not contain any unsupported characters, this method creates no
* new string (and in fact no new objects at all).
*
* <p>Replacements:
*
* <ul>
* <li>{@code "} is removed</li>
* <li>{@code space} is replaced by {@code _} (underscore)</li>
* <li>{@code , = ; : ? ' *} are replaced by {@code -} (hyphen)</li>
* </ul>
*/
static String replaceInvalidChars(String str) {
char[] chars = null;
final int strLen = str.length();
int pos = 0;
for (int i = 0; i < strLen; i++) {
final char c = str.charAt(i);
switch (c) {
case '>':
case '<':
case '"':
// remove character by not moving cursor
if (chars == null) {
chars = str.toCharArray();
}
break;
case ' ':
if (chars == null) {
chars = str.toCharArray();
}
chars[pos++] = '_';
break;
case ',':
case '=':
case ';':
case ':':
case '?':
case '\'':
case '*':
if (chars == null) {
chars = str.toCharArray();
}
chars[pos++] = '-';
break;
default:
if (chars != null) {
chars[pos] = c;
}
pos++;
}
}
return chars == null ? str : new String(chars, 0, pos);
}
// ------------------------------------------------------------------------
// Interfaces and base classes for JMX beans
// ------------------------------------------------------------------------
public interface MetricMBean {}
private abstract static class AbstractBean implements MetricMBean {}
public interface JmxCounterMBean extends MetricMBean {
long getCount();
}
private static class JmxCounter extends AbstractBean implements JmxCounterMBean {
private Counter counter;
JmxCounter(Counter counter) {
this.counter = counter;
}
@Override
public long getCount() {
return counter.getCount();
}
}
public interface JmxGaugeMBean extends MetricMBean {
Object getValue();
}
private static class JmxGauge extends AbstractBean implements JmxGaugeMBean {
private final Gauge<?> gauge;
JmxGauge(Gauge<?> gauge) {
this.gauge = gauge;
}
@Override
public Object getValue() {
return gauge.getValue();
}
}
public interface JmxHistogramMBean extends MetricMBean {
long getCount();
double getMean();
double getStdDev();
long getMax();
long getMin();
double getMedian();
double get75thPercentile();
double get95thPercentile();
double get98thPercentile();
double get99thPercentile();
double get999thPercentile();
}
private static class JmxHistogram extends AbstractBean implements JmxHistogramMBean {
private final Histogram histogram;
JmxHistogram(Histogram histogram) {
this.histogram = histogram;
}
@Override
public long getCount() {
return histogram.getCount();
}
@Override
public double getMean() {
return histogram.getStatistics().getMean();
}
@Override
public double getStdDev() {
return histogram.getStatistics().getStdDev();
}
@Override
public long getMax() {
return histogram.getStatistics().getMax();
}
@Override
public long getMin() {
return histogram.getStatistics().getMin();
}
@Override
public double getMedian() {
return histogram.getStatistics().getQuantile(0.5);
}
@Override
public double get75thPercentile() {
return histogram.getStatistics().getQuantile(0.75);
}
@Override
public double get95thPercentile() {
return histogram.getStatistics().getQuantile(0.95);
}
@Override
public double get98thPercentile() {
return histogram.getStatistics().getQuantile(0.98);
}
@Override
public double get99thPercentile() {
return histogram.getStatistics().getQuantile(0.99);
}
@Override
public double get999thPercentile() {
return histogram.getStatistics().getQuantile(0.999);
}
}
public interface JmxMeterMBean extends MetricMBean {
double getRate();
long getCount();
}
private static class JmxMeter extends AbstractBean implements JmxMeterMBean {
private final Meter meter;
public JmxMeter(Meter meter) {
this.meter = meter;
}
@Override
public double getRate() {
return meter.getRate();
}
@Override
public long getCount() {
return meter.getCount();
}
}
/**
* JMX Server implementation that JMX clients can connect to.
*
* Heavily based on j256 simplejmx project
*
* https://github.com/j256/simplejmx/blob/master/src/main/java/com/j256/simplejmx/server/JmxServer.java
*/
private static class JMXServer {
private Registry rmiRegistry;
private JMXConnectorServer connector;
private int port;
public void start(int port) throws IOException {
if (rmiRegistry != null && connector != null) {
LOG.debug("JMXServer is already running.");
return;
}
startRmiRegistry(port);
startJmxService(port);
this.port = port;
}
/**
* Starts an RMI Registry that allows clients to lookup the JMX IP/port.
*
* @param port rmi port to use
* @throws IOException
*/
private void startRmiRegistry(int port) throws IOException {
rmiRegistry = LocateRegistry.createRegistry(port);
}
/**
* Starts a JMX connector that allows (un)registering MBeans with the MBean server and RMI invocations.
*
* @param port jmx port to use
* @throws IOException
*/
private void startJmxService(int port) throws IOException {
String serviceUrl = "service:jmx:rmi://localhost:" + port + "/jndi/rmi://localhost:" + port + "/jmxrmi";
JMXServiceURL url;
try {
url = new JMXServiceURL(serviceUrl);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Malformed service url created " + serviceUrl, e);
}
connector = JMXConnectorServerFactory.newJMXConnectorServer(url, null, ManagementFactory.getPlatformMBeanServer());
connector.start();
}
public void stop() throws IOException {
if (connector != null) {
try {
connector.stop();
} finally {
connector = null;
}
}
if (rmiRegistry != null) {
try {
UnicastRemoteObject.unexportObject(rmiRegistry, true);
} catch (NoSuchObjectException e) {
throw new IOException("Could not un-export our RMI registry", e);
} finally {
rmiRegistry = null;
}
}
}
}
}