Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions checkstyle/import-control-core.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<allow pkg="kafka.serializer" />
<allow pkg="org.apache.kafka.common" />

<!-- see KIP-544 for why KafkaYammerMetrics should be used instead of the global default yammer metrics registry
https://cwiki.apache.org/confluence/display/KAFKA/KIP-544%3A+Make+metrics+exposed+via+JMX+configurable -->
<disallow class="com.yammer.metrics.Metrics" />
<allow pkg="com.yammer.metrics"/>

<subpackage name="tools">
<allow pkg="org.apache.kafka.clients.admin" />
<allow pkg="kafka.admin" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,9 @@ static KafkaAdminClient createInternal(AdminClientConfig config, TimeoutProcesso
.timeWindow(config.getLong(AdminClientConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS)
.recordLevel(Sensor.RecordingLevel.forName(config.getString(AdminClientConfig.METRICS_RECORDING_LEVEL_CONFIG)))
.tags(metricTags);
reporters.add(new JmxReporter(JMX_PREFIX));
JmxReporter jmxReporter = new JmxReporter(JMX_PREFIX);
jmxReporter.configure(config.originals());
Comment thread
cmccabe marked this conversation as resolved.
Outdated
reporters.add(jmxReporter);
metrics = new Metrics(metricConfig, reporters, time);
String metricGrpPrefix = "admin-client";
channelBuilder = ClientUtils.createChannelBuilder(config, time, logContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,9 @@ private static Metrics buildMetrics(ConsumerConfig config, Time time, String cli
.tags(metricsTags);
List<MetricsReporter> reporters = config.getConfiguredInstances(ConsumerConfig.METRIC_REPORTER_CLASSES_CONFIG,
MetricsReporter.class, Collections.singletonMap(ConsumerConfig.CLIENT_ID_CONFIG, clientId));
reporters.add(new JmxReporter(JMX_PREFIX));
JmxReporter jmxReporter = new JmxReporter(JMX_PREFIX);
jmxReporter.configure(config.originals());
reporters.add(jmxReporter);
return new Metrics(metricConfig, reporters, time);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ public KafkaProducer(Properties properties, Serializer<K> keySerializer, Seriali
List<MetricsReporter> reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG,
MetricsReporter.class,
Collections.singletonMap(ProducerConfig.CLIENT_ID_CONFIG, clientId));
reporters.add(new JmxReporter(JMX_PREFIX));
JmxReporter jmxReporter = new JmxReporter(JMX_PREFIX);
jmxReporter.configure(userProvidedConfigs);
reporters.add(jmxReporter);
this.metrics = new Metrics(metricConfig, reporters, time);
this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.Reconfigurable;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.common.utils.Sanitizer;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -36,16 +39,32 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* Register metrics in JMX as dynamic mbeans based on the metric names
*/
public class JmxReporter implements MetricsReporter {
public class JmxReporter implements MetricsReporter, Reconfigurable {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make the MetricsReporter interface itself extend Reconfigurable, rather than just JmxReporter. We might want to reconfigure other metrics reporters at runtime, and there is no reason to special-case just this one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to add default implementation for Reconfigurable methods to the MetricsReporter interface, but that would theoretically break binary compatibility for existing plugins.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a new method with default implementation to a Java interface doesn't break binary compatibility. See https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there is a separate question as to whether having a superclass implement a new interface is a binary-compatible change. Section 13.4.4 of the Java spec seems to imply that it is. https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.4.4

Changing the direct superclass or the set of direct superinterfaces of a class type will not break compatibility with pre-existing binaries, provided that the total set of superclasses or superinterfaces, respectively, of the class type loses no members.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://wiki.eclipse.org/Evolving_Java-based_APIs_2#Evolving_API_Interfaces adding a default method is binary incompatible. The reasoning is that if the implementing class already implements a different interface with a default method matching the signature of the added new default methods it would break. In practice this will almost never be the case, but it is theoretically possible. I don't know how strict we are about those things in Kafka.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do consider adding new default methods to interfaces to be a compatible change in Kafka. I agree that it is theoretically possible for an existing implementation to contain a different method with the same name, and have an issue that way. But if we treat every method addition as a compatibility break, it defeats the whole point of default methods in Java. This would make it very hard to evolve interfaces and would lead to some pretty ugly hacks.

In practice people choose descriptive methods names for public interfaces which makes this is a non-issue. For example, I think the chances that an existing MetricsReporter implements reconfigurableConfigs, validateReconfiguration, or reconfigure are basically zero. We also know what most of the widely-deployed MetricsReporters are, so we can check them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MetricsReporters have been optionally reconfigurable since 1.1.0, before Java 7 support was dropped. Any metrics reporter implementation that implements Reconfigurable is reconfigured by the broker when any of its configs changes. Since then, we have adopted the same approach for other interfaces like Authorizer as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajinisivaram do I understand correctly that you suggest we leave the Reconfigurable interface optional then?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xvrl: The only reason the interface was optional is that we needed to retain support for Java 7, which didn't have default methods. Since we don't have to worry about Java 7 any more, let's make the interface part of the type.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I misundertood; I updated the interface to be Reconfigurable and added default methods for backwards compatibility.


public static final String METRICS_CONFIG_PREFIX = "metrics.jmx.";

public static final String BLACKLIST_CONFIG = METRICS_CONFIG_PREFIX + "blacklist";
public static final String WHITELIST_CONFIG = METRICS_CONFIG_PREFIX + "whitelist";

public static final Set<String> RECONFIGURABLE_CONFIGS = Utils.mkSet(WHITELIST_CONFIG,
BLACKLIST_CONFIG);

public static final String DEFAULT_WHITELIST = ".*";
public static final String DEFAULT_BLACKLIST = "";

private static final Logger log = LoggerFactory.getLogger(JmxReporter.class);
private static final Object LOCK = new Object();
private String prefix;
private final Map<String, KafkaMbean> mbeans = new HashMap<>();
private Predicate<String> mbeanPredicate = s -> true;

public JmxReporter() {
this("");
Expand All @@ -59,15 +78,46 @@ public JmxReporter(String prefix) {
}

@Override
public void configure(Map<String, ?> configs) {}
public void configure(Map<String, ?> configs) {
reconfigure(configs);
}

@Override
public Set<String> reconfigurableConfigs() {
return RECONFIGURABLE_CONFIGS;
}

@Override
public void validateReconfiguration(Map<String, ?> configs) throws ConfigException {
compilePredicate(configs);
}

@Override
public void reconfigure(Map<String, ?> configs) {
synchronized (LOCK) {
this.mbeanPredicate = JmxReporter.compilePredicate(configs);

mbeans.forEach((name, mbean) -> {
if (mbeanPredicate.test(name)) {
reregister(mbean);
} else {
unregister(mbean);
}
});
}
}

@Override
public void init(List<KafkaMetric> metrics) {
synchronized (LOCK) {
for (KafkaMetric metric : metrics)
addAttribute(metric);
for (KafkaMbean mbean : mbeans.values())
reregister(mbean);

mbeans.forEach((name, mbean) -> {
if (mbeanPredicate.test(name)) {
reregister(mbean);
}
});
}
}

Expand All @@ -78,8 +128,10 @@ public boolean containsMbean(String mbeanName) {
@Override
public void metricChange(KafkaMetric metric) {
synchronized (LOCK) {
KafkaMbean mbean = addAttribute(metric);
reregister(mbean);
String mbeanName = addAttribute(metric);
if (mbeanName != null && mbeanPredicate.test(mbeanName)) {
reregister(mbeans.get(mbeanName));
}
}
}

Expand All @@ -93,7 +145,7 @@ public void metricRemoval(KafkaMetric metric) {
if (mbean.metrics.isEmpty()) {
unregister(mbean);
mbeans.remove(mBeanName);
} else
} else if (mbeanPredicate.test(mBeanName))
reregister(mbean);
}
}
Expand All @@ -107,15 +159,15 @@ private KafkaMbean removeAttribute(KafkaMetric metric, String mBeanName) {
return mbean;
}

private KafkaMbean addAttribute(KafkaMetric metric) {
private String addAttribute(KafkaMetric metric) {
try {
MetricName metricName = metric.metricName();
String mBeanName = getMBeanName(prefix, metricName);
if (!this.mbeans.containsKey(mBeanName))
mbeans.put(mBeanName, new KafkaMbean(mBeanName));
KafkaMbean mbean = this.mbeans.get(mBeanName);
mbean.setAttribute(metricName.name(), metric);
return mbean;
return mBeanName;
} catch (JMException e) {
throw new KafkaException("Error creating mbean attribute for metricName :" + metric.metricName(), e);
}
Expand Down Expand Up @@ -244,4 +296,27 @@ public AttributeList setAttributes(AttributeList list) {

}

public static Predicate<String> compilePredicate(Map<String, ?> configs) {
String whitelist = (String) configs.get(WHITELIST_CONFIG);
String blacklist = (String) configs.get(BLACKLIST_CONFIG);

if (whitelist == null) {
whitelist = DEFAULT_WHITELIST;
}

if (blacklist == null) {
blacklist = DEFAULT_BLACKLIST;
}

try {
Pattern whitelistPattern = Pattern.compile(whitelist);
Pattern blacklistPattern = Pattern.compile(blacklist);

return s -> whitelistPattern.matcher(s).matches()
&& !blacklistPattern.matcher(s).matches();
} catch (PatternSyntaxException e) {
throw new ConfigException("JMX filter for configuration" + METRICS_CONFIG_PREFIX
+ ".(whitelist/blacklist) is not a valid regular expression");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -111,4 +113,46 @@ public void testJmxRegistrationSanitization() throws Exception {
metrics.close();
}
}

@Test
public void testPredicateAndDynamicReload() throws Exception {
Metrics metrics = new Metrics();
MBeanServer server = ManagementFactory.getPlatformMBeanServer();

Map<String, String> configs = new HashMap<>();

configs.put(JmxReporter.BLACKLIST_CONFIG,
JmxReporter.getMBeanName("", metrics.metricName("pack.bean2.total", "grp2")));

try {
JmxReporter reporter = new JmxReporter();
reporter.configure(configs);
metrics.addReporter(reporter);

Sensor sensor = metrics.sensor("kafka.requests");
sensor.add(metrics.metricName("pack.bean2.avg", "grp1"), new Avg());
sensor.add(metrics.metricName("pack.bean2.total", "grp2"), new CumulativeSum());
sensor.record();

assertTrue(server.isRegistered(new ObjectName(":type=grp1")));
assertEquals(1.0, server.getAttribute(new ObjectName(":type=grp1"), "pack.bean2.avg"));
assertFalse(server.isRegistered(new ObjectName(":type=grp2")));

sensor.record();

configs.put(JmxReporter.BLACKLIST_CONFIG,
JmxReporter.getMBeanName("", metrics.metricName("pack.bean2.avg", "grp1")));

reporter.reconfigure(configs);

assertFalse(server.isRegistered(new ObjectName(":type=grp1")));
assertTrue(server.isRegistered(new ObjectName(":type=grp2")));
assertEquals(2.0, server.getAttribute(new ObjectName(":type=grp2"), "pack.bean2.total"));

metrics.removeMetric(metrics.metricName("pack.bean2.total", "grp2"));
assertFalse(server.isRegistered(new ObjectName(":type=grp2")));
} finally {
metrics.close();
}
}
Comment thread
cmccabe marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ Map<String, Object> sourceAdminConfig() {
List<MetricsReporter> metricsReporters() {
List<MetricsReporter> reporters = getConfiguredInstances(
CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, MetricsReporter.class);
reporters.add(new JmxReporter("kafka.connect.mirror"));
JmxReporter jmxReporter = new JmxReporter("kafka.connect.mirror");
jmxReporter.configure(this.originals());
reporters.add(jmxReporter);
return reporters;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,20 @@ public class ConnectMetrics {
* @param time the time; may not be null
*/
public ConnectMetrics(String workerId, WorkerConfig config, Time time) {
this(workerId, time, config.getInt(CommonClientConfigs.METRICS_NUM_SAMPLES_CONFIG),
config.getLong(CommonClientConfigs.METRICS_SAMPLE_WINDOW_MS_CONFIG),
config.getString(CommonClientConfigs.METRICS_RECORDING_LEVEL_CONFIG),
config.getConfiguredInstances(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, MetricsReporter.class));
}

public ConnectMetrics(String workerId, Time time, int numSamples, long sampleWindowMs, String metricsRecordingLevel,
List<MetricsReporter> reporters) {
this.workerId = workerId;
this.time = time;

int numSamples = config.getInt(CommonClientConfigs.METRICS_NUM_SAMPLES_CONFIG);
long sampleWindowMs = config.getLong(CommonClientConfigs.METRICS_SAMPLE_WINDOW_MS_CONFIG);
String metricsRecordingLevel = config.getString(CommonClientConfigs.METRICS_RECORDING_LEVEL_CONFIG);
List<MetricsReporter> reporters = config.getConfiguredInstances(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG, MetricsReporter.class);

MetricConfig metricConfig = new MetricConfig().samples(numSamples)
.timeWindow(sampleWindowMs, TimeUnit.MILLISECONDS).recordLevel(
Sensor.RecordingLevel.forName(metricsRecordingLevel));
reporters.add(new JmxReporter(JMX_PREFIX));
JmxReporter jmxReporter = new JmxReporter(JMX_PREFIX);
jmxReporter.configure(config.originals());
reporters.add(jmxReporter);
this.metrics = new Metrics(metricConfig, reporters, time);
LOG.debug("Registering Connect metrics with JMX for worker '{}'", workerId);
AppInfoParser.registerAppInfo(JMX_PREFIX, workerId, metrics, time.milliseconds());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ public WorkerGroupMember(DistributedConfig config,
List<MetricsReporter> reporters = config.getConfiguredInstances(CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG,
MetricsReporter.class,
Collections.singletonMap(CommonClientConfigs.CLIENT_ID_CONFIG, clientId));
reporters.add(new JmxReporter(JMX_PREFIX));
JmxReporter jmxReporter = new JmxReporter(JMX_PREFIX);
jmxReporter.configure(config.originals());
reporters.add(jmxReporter);
this.metrics = new Metrics(metricConfig, reporters, time);
this.retryBackoffMs = config.getLong(CommonClientConfigs.RETRY_BACKOFF_MS_CONFIG);
this.metadata = new Metadata(retryBackoffMs, config.getLong(CommonClientConfigs.METADATA_MAX_AGE_CONFIG),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import org.apache.kafka.connect.runtime.ConnectMetricsRegistry;
import org.apache.kafka.connect.util.ConnectorTaskId;

import java.util.ArrayList;

/**
* Contains various sensors used for monitoring errors.
*/
Expand All @@ -45,13 +43,6 @@ public class ErrorHandlingMetrics {
private final Sensor dlqProduceFailures;
private long lastErrorTime = 0;

// for testing only
public ErrorHandlingMetrics() {
this(new ConnectorTaskId("noop-connector", -1),
new ConnectMetrics("noop-worker", new SystemTime(), 2, 3000, Sensor.RecordingLevel.INFO.toString(),
new ArrayList<>()));
}

public ErrorHandlingMetrics(ConnectorTaskId id, ConnectMetrics connectMetrics) {

ConnectMetricsRegistry registry = connectMetrics.registry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@
import static org.junit.Assert.assertTrue;

@PowerMockIgnore({"javax.management.*",
"org.apache.log4j.*",
"org.apache.kafka.connect.runtime.isolation.*"})
"org.apache.log4j.*"})
@RunWith(PowerMockRunner.class)
public class WorkerSourceTaskTest extends ThreadedTest {
private static final String TOPIC = "topic";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@
*/
package org.apache.kafka.connect.runtime.errors;

import org.apache.kafka.clients.CommonClientConfigs;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.utils.MockTime;
import org.apache.kafka.common.utils.SystemTime;
import org.apache.kafka.connect.errors.ConnectException;
import org.apache.kafka.connect.errors.RetriableException;
import org.apache.kafka.connect.runtime.ConnectMetrics;
import org.apache.kafka.connect.runtime.ConnectorConfig;
import org.apache.kafka.connect.runtime.WorkerConfig;
import org.apache.kafka.connect.runtime.isolation.Plugins;
import org.apache.kafka.connect.runtime.isolation.PluginsTest.TestConverter;
import org.apache.kafka.connect.runtime.isolation.PluginsTest.TestableWorkerConfig;
import org.apache.kafka.connect.sink.SinkTask;
import org.apache.kafka.connect.util.ConnectorTaskId;
import org.easymock.EasyMock;
import org.easymock.Mock;
import org.junit.Test;
Expand All @@ -33,6 +41,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
Expand All @@ -59,7 +68,19 @@ public class RetryWithToleranceOperatorTest {
public static final RetryWithToleranceOperator NOOP_OPERATOR = new RetryWithToleranceOperator(
ERRORS_RETRY_TIMEOUT_DEFAULT, ERRORS_RETRY_MAX_DELAY_DEFAULT, NONE, SYSTEM);
static {
NOOP_OPERATOR.metrics(new ErrorHandlingMetrics());
Map<String, String> properties = new HashMap<>();
properties.put(CommonClientConfigs.METRICS_NUM_SAMPLES_CONFIG, Objects.toString(2));
properties.put(CommonClientConfigs.METRICS_SAMPLE_WINDOW_MS_CONFIG, Objects.toString(3000));
properties.put(CommonClientConfigs.METRICS_RECORDING_LEVEL_CONFIG, Sensor.RecordingLevel.INFO.toString());

// define required properties
properties.put(WorkerConfig.KEY_CONVERTER_CLASS_CONFIG, TestConverter.class.getName());
properties.put(WorkerConfig.VALUE_CONVERTER_CLASS_CONFIG, TestConverter.class.getName());

NOOP_OPERATOR.metrics(new ErrorHandlingMetrics(
new ConnectorTaskId("noop-connector", -1),
new ConnectMetrics("noop-worker", new TestableWorkerConfig(properties), new SystemTime()))
);
}

@SuppressWarnings("unused")
Expand Down
Loading