Skip to content
Merged
43 changes: 43 additions & 0 deletions src/main/java/org/datadog/jmxfetch/Configuration.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.datadog.jmxfetch;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -9,11 +11,13 @@
import java.util.Map.Entry;
import java.util.Set;

@Slf4j
public class Configuration {

private Map<String, Object> conf;
private Filter include;
private Filter exclude;
private List<DynamicTag> dynamicTags = null;

/**
* Access configuration elements more easily
Expand All @@ -24,6 +28,37 @@ public Configuration(Map<String, Object> conf) {
this.conf = conf;
this.include = new Filter(conf.get("include"));
this.exclude = new Filter(conf.get("exclude"));
this.parseDynamicTags(conf.get("dynamic_tags"));
}

/**
* Parse dynamic tags from configuration.
* Expected format:
* dynamic_tags:
* - tag_name: cluster_id
* bean_name: kafka.server:type=KafkaServer,name=ClusterId
* attribute: Value
*/
private void parseDynamicTags(Object dynamicTagsConfig) {
this.dynamicTags = new ArrayList<DynamicTag>();

if (dynamicTagsConfig == null) {
return;
}

if (!(dynamicTagsConfig instanceof List)) {
log.warn("Invalid dynamic_tags configuration: expected list of tag definitions");
return;
}

List<Object> dynamicTagsList = (List<Object>) dynamicTagsConfig;

for (Object tagConfig : dynamicTagsList) {
DynamicTag dynamicTag = DynamicTag.parse(tagConfig);
if (dynamicTag != null) {
this.dynamicTags.add(dynamicTag);
}
}
}

public Map<String, Object> getConf() {
Expand All @@ -45,6 +80,14 @@ public String toString() {
private Boolean hasInclude() {
return getInclude() != null && !getInclude().isEmptyFilter();
}

/** Get list of dynamic tags defined for this configuration. */
public List<DynamicTag> getDynamicTags() {
if (dynamicTags == null) {
return new ArrayList<DynamicTag>();
}
return dynamicTags;
}

/**
* Filter a configuration list to keep the ones with `include` filters.
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/org/datadog/jmxfetch/DynamicTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.datadog.jmxfetch;

import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

@Slf4j
public class DynamicTag {
private final String tagName;
private final String beanName;
private final String attributeName;

/** Parse dynamic tag from configuration map (list entry format). */
public static DynamicTag parse(Object tagConfig) {
if (tagConfig == null) {
return null;
}

if (!(tagConfig instanceof Map)) {
log.warn("Invalid dynamic tag config: expected map with 'tag_name', 'bean_name' and "
+ "'attribute' keys");
return null;
}

Map<String, Object> config = (Map<String, Object>) tagConfig;
Object tagNameObj = config.get("tag_name");
Object beanObj = config.get("bean_name");
Object attrObj = config.get("attribute");

if (tagNameObj == null || beanObj == null || attrObj == null) {
String missing = "Invalid dynamic tag config: missing"
+ (tagNameObj == null ? " tag_name" : "")
+ (beanObj == null ? " bean_name" : "")
+ (attrObj == null ? " attribute" : "");
log.warn(missing);
return null;
}

String tagName = tagNameObj.toString();
String beanName = beanObj.toString();
String attributeName = attrObj.toString();

return new DynamicTag(tagName, beanName, attributeName);
}

private DynamicTag(String tagName, String beanName, String attributeName) {
this.tagName = tagName;
this.beanName = beanName;
this.attributeName = attributeName;
}

public String getTagName() {
return tagName;
}

public String getBeanName() {
return beanName;
}

public String getAttributeName() {
return attributeName;
}

/** Gets a unique key for the bean and attribute combination. */
public String getBeanAttributeKey() {
return beanName + "#" + attributeName;
}

/** Resolve the dynamic tag by fetching the attribute value from JMX. */
public Map.Entry<String, String> resolve(Connection connection) {
try {
ObjectName objectName = new ObjectName(beanName);
Object value = connection.getAttribute(objectName, attributeName);

if (value == null) {
log.warn("Dynamic tag '{}' resolved to null for bean '{}' attribute '{}'",
tagName, beanName, attributeName);
return null;
}

String stringValue = value.toString();
log.info("Resolved dynamic tag '{}' to value '{}' from bean '{}' attribute '{}'",
tagName, stringValue, beanName, attributeName);

return new HashMap.SimpleEntry<>(tagName, stringValue);

} catch (MalformedObjectNameException e) {
log.error("Invalid bean name '{}' for dynamic tag '{}': {}",
beanName, tagName, e.getMessage());
return null;
} catch (Exception e) {
log.warn("Failed to resolve dynamic tag '{}' from bean '{}' attribute '{}': {}",
tagName, beanName, attributeName, e.getMessage());
log.debug("Dynamic tag resolution error details", e);
return null;
}
}

@Override
public String toString() {
return String.format("DynamicTag{name='%s', bean='%s', attribute='%s'}",
tagName, beanName, attributeName);
}
}

77 changes: 77 additions & 0 deletions src/main/java/org/datadog/jmxfetch/Instance.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public Yaml initialValue() {
private ObjectName instanceTelemetryBeanName;
private MBeanServer mbs;
private Boolean normalizeBeanParamTags;
private Map<String, Map.Entry<String, String>> dynamicTagsCache;

/** Constructor, instantiates Instance based of a previous instance and appConfig. */
public Instance(Instance instance, AppConfig appConfig) {
Expand Down Expand Up @@ -447,14 +448,87 @@ public void init(boolean forceNewConnection)
throws IOException, FailedLoginException, SecurityException {
log.info("Trying to connect to JMX Server at " + this.toString());
connection = getConnection(instanceMap, forceNewConnection);

log.info(
"Trying to collect bean list for the first time for JMX Server at {}", this);
this.refreshBeansList();
this.initialRefreshTime = this.lastRefreshTime;
log.info("Connected to JMX Server at {} with {} beans", this, this.beans.size());

// Resolve configuration-level dynamic tags for all configurations
// Must be done after refreshBeansList() so the beans exist
resolveConfigurationDynamicTags();

this.getMatchingAttributes();
log.info("Done initializing JMX Server at {}", this);
}

private void resolveConfigurationDynamicTags() {
if (configurationList == null || configurationList.isEmpty()) {
return;
}

this.dynamicTagsCache = new HashMap<>();
List<DynamicTag> allDynamicTags = new ArrayList<>();

for (Configuration config : configurationList) {
List<DynamicTag> dynamicTags = config.getDynamicTags();
if (dynamicTags != null && !dynamicTags.isEmpty()) {
allDynamicTags.addAll(dynamicTags);
}
}

if (allDynamicTags.isEmpty()) {
return;
}

int successfulResolutions = 0;
for (DynamicTag dynamicTag : allDynamicTags) {
String cacheKey = dynamicTag.getBeanAttributeKey();
if (!this.dynamicTagsCache.containsKey(cacheKey)) {
Map.Entry<String, String> resolved = dynamicTag.resolve(connection);
// Cache both successful and failed resolutions (null) to avoid retrying
this.dynamicTagsCache.put(cacheKey, resolved);
}
// Count successful resolutions (cached value is not null)
if (this.dynamicTagsCache.get(cacheKey) != null) {
successfulResolutions++;
}
}

log.info("Resolved {} unique dynamic tag(s) from {} total references for instance {}",
successfulResolutions, allDynamicTags.size(), instanceName);
}

/**
* Get resolved dynamic tags for a specific configuration.
* This resolves the dynamic tags defined in the configuration using the cached values.
*
* @param config the configuration to get resolved tags for
* @return map of tag name to tag value
*/
private Map<String, String> getResolvedDynamicTagsForConfig(Configuration config) {
Map<String, String> resolvedTags = new HashMap<>();

if (this.dynamicTagsCache == null || this.dynamicTagsCache.isEmpty()) {
return resolvedTags;
}

List<DynamicTag> dynamicTags = config.getDynamicTags();
if (dynamicTags == null || dynamicTags.isEmpty()) {
return resolvedTags;
}

for (DynamicTag dynamicTag : dynamicTags) {
String cacheKey = dynamicTag.getBeanAttributeKey();
Map.Entry<String, String> cached = this.dynamicTagsCache.get(cacheKey);
if (cached != null) {
resolvedTags.put(cached.getKey(), cached.getValue());
}
}

return resolvedTags;
}

/** Returns a string representation for the instance. */
@Override
Expand Down Expand Up @@ -690,6 +764,9 @@ private void getMatchingAttributes() throws IOException {
for (Configuration conf : configurationList) {
try {
if (jmxAttribute.match(conf)) {
Map<String, String> resolvedDynamicTags =
getResolvedDynamicTagsForConfig(conf);
jmxAttribute.setResolvedDynamicTags(resolvedDynamicTags);
jmxAttribute.setMatchingConf(conf);
metricsCount += jmxAttribute.getMetricsCount();
this.matchingAttributes.add(jmxAttribute);
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/datadog/jmxfetch/JmxAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public abstract class JmxAttribute {
new HashMap<String, Map<Object, Object>>();
protected String[] tags;
private Configuration matchingConf;
private Map<String, String> resolvedDynamicTags;
private List<String> defaultTagsList;
private boolean cassandraAliasing;
protected String checkName;
Expand Down Expand Up @@ -139,6 +140,15 @@ private void addAdditionalTags() {
}
}
}

/** Add dynamic tags that were resolved at connection time. */
private void addDynamicTags() {
if (this.resolvedDynamicTags != null && !this.resolvedDynamicTags.isEmpty()) {
for (Map.Entry<String, String> tag : this.resolvedDynamicTags.entrySet()) {
this.defaultTagsList.add(tag.getKey() + ":" + tag.getValue());
}
}
}

private void addServiceTags() {
Iterable<String> serviceNames = this.serviceNameProvider.getServiceNames();
Expand Down Expand Up @@ -495,13 +505,20 @@ public Configuration getMatchingConf() {
return matchingConf;
}

/** Sets resolved dynamic tags for the attribute. */
public void setResolvedDynamicTags(Map<String, String> resolvedDynamicTags) {
this.resolvedDynamicTags = resolvedDynamicTags;
}

/** Sets a matching configuration for the attribute. */
public void setMatchingConf(Configuration matchingConf) {
this.matchingConf = matchingConf;

// Now that we have the matchingConf we can:
// - add additional tags
this.addAdditionalTags();
// - add dynamic tags that were resolved at connection time
this.addDynamicTags();
// - filter out excluded tags
this.applyTagsBlackList();
// Add the service tag(s) - comes last because if the service tag is blacklisted as
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/org/datadog/jmxfetch/DynamicTagTestApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.datadog.jmxfetch;

public class DynamicTagTestApp implements DynamicTagTestAppMBean {
private final String clusterId;
private final String version;
private final int port;
private double metric;

public DynamicTagTestApp() {
this("local-kafka-cluster", "2.8.0", 9092);
}

public DynamicTagTestApp(String clusterId, String version, int port) {
this.clusterId = clusterId;
this.version = version;
this.port = port;
this.metric = 100.0;
}

@Override
public String getClusterId() {
return clusterId;
}

@Override
public String getVersion() {
return version;
}

@Override
public int getPort() {
return port;
}

@Override
public Double getMetric() {
return metric;
}

public void setMetric(double metric) {
this.metric = metric;
}
}


10 changes: 10 additions & 0 deletions src/test/java/org/datadog/jmxfetch/DynamicTagTestAppMBean.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.datadog.jmxfetch;

public interface DynamicTagTestAppMBean {
String getClusterId();
String getVersion();
int getPort();
Double getMetric();
}


Loading