Skip to content

Commit efd946a

Browse files
Merge pull request #581 from DataDog/piotr.wolski/add-dynamic-tags
DSMON-1102: Add configuration-level dynamic tags for JMX attribute values Co-authored-by: piochelepiotr <piotr.wolski42@gmail.com> Co-authored-by: piotr.wolski <piotr.wolski@datadoghq.com>
2 parents 15ce3a2 + 91ffa17 commit efd946a

13 files changed

+780
-0
lines changed

src/main/java/org/datadog/jmxfetch/Configuration.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.datadog.jmxfetch;
22

3+
import lombok.extern.slf4j.Slf4j;
4+
35
import java.util.ArrayList;
46
import java.util.HashMap;
57
import java.util.HashSet;
@@ -9,11 +11,13 @@
911
import java.util.Map.Entry;
1012
import java.util.Set;
1113

14+
@Slf4j
1215
public class Configuration {
1316

1417
private Map<String, Object> conf;
1518
private Filter include;
1619
private Filter exclude;
20+
private List<DynamicTag> dynamicTags = null;
1721

1822
/**
1923
* Access configuration elements more easily
@@ -24,6 +28,37 @@ public Configuration(Map<String, Object> conf) {
2428
this.conf = conf;
2529
this.include = new Filter(conf.get("include"));
2630
this.exclude = new Filter(conf.get("exclude"));
31+
this.parseDynamicTags(conf.get("dynamic_tags"));
32+
}
33+
34+
/**
35+
* Parse dynamic tags from configuration.
36+
* Expected format:
37+
* dynamic_tags:
38+
* - tag_name: cluster_id
39+
* bean_name: kafka.server:type=KafkaServer,name=ClusterId
40+
* attribute: Value
41+
*/
42+
private void parseDynamicTags(Object dynamicTagsConfig) {
43+
this.dynamicTags = new ArrayList<DynamicTag>();
44+
45+
if (dynamicTagsConfig == null) {
46+
return;
47+
}
48+
49+
if (!(dynamicTagsConfig instanceof List)) {
50+
log.warn("Invalid dynamic_tags configuration: expected list of tag definitions");
51+
return;
52+
}
53+
54+
List<Object> dynamicTagsList = (List<Object>) dynamicTagsConfig;
55+
56+
for (Object tagConfig : dynamicTagsList) {
57+
DynamicTag dynamicTag = DynamicTag.parse(tagConfig);
58+
if (dynamicTag != null) {
59+
this.dynamicTags.add(dynamicTag);
60+
}
61+
}
2762
}
2863

2964
public Map<String, Object> getConf() {
@@ -45,6 +80,14 @@ public String toString() {
4580
private Boolean hasInclude() {
4681
return getInclude() != null && !getInclude().isEmptyFilter();
4782
}
83+
84+
/** Get list of dynamic tags defined for this configuration. */
85+
public List<DynamicTag> getDynamicTags() {
86+
if (dynamicTags == null) {
87+
return new ArrayList<DynamicTag>();
88+
}
89+
return dynamicTags;
90+
}
4891

4992
/**
5093
* Filter a configuration list to keep the ones with `include` filters.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.datadog.jmxfetch;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
import javax.management.MalformedObjectNameException;
8+
import javax.management.ObjectName;
9+
10+
@Slf4j
11+
public class DynamicTag {
12+
private final String tagName;
13+
private final String beanName;
14+
private final String attributeName;
15+
16+
/** Parse dynamic tag from configuration map (list entry format). */
17+
public static DynamicTag parse(Object tagConfig) {
18+
if (tagConfig == null) {
19+
return null;
20+
}
21+
22+
if (!(tagConfig instanceof Map)) {
23+
log.warn("Invalid dynamic tag config: expected map with 'tag_name', 'bean_name' and "
24+
+ "'attribute' keys");
25+
return null;
26+
}
27+
28+
Map<String, Object> config = (Map<String, Object>) tagConfig;
29+
Object tagNameObj = config.get("tag_name");
30+
Object beanObj = config.get("bean_name");
31+
Object attrObj = config.get("attribute");
32+
33+
if (tagNameObj == null || beanObj == null || attrObj == null) {
34+
String missing = "Invalid dynamic tag config: missing"
35+
+ (tagNameObj == null ? " tag_name" : "")
36+
+ (beanObj == null ? " bean_name" : "")
37+
+ (attrObj == null ? " attribute" : "");
38+
log.warn(missing);
39+
return null;
40+
}
41+
42+
String tagName = tagNameObj.toString();
43+
String beanName = beanObj.toString();
44+
String attributeName = attrObj.toString();
45+
46+
return new DynamicTag(tagName, beanName, attributeName);
47+
}
48+
49+
private DynamicTag(String tagName, String beanName, String attributeName) {
50+
this.tagName = tagName;
51+
this.beanName = beanName;
52+
this.attributeName = attributeName;
53+
}
54+
55+
public String getTagName() {
56+
return tagName;
57+
}
58+
59+
public String getBeanName() {
60+
return beanName;
61+
}
62+
63+
public String getAttributeName() {
64+
return attributeName;
65+
}
66+
67+
/** Gets a unique key for the bean and attribute combination. */
68+
public String getBeanAttributeKey() {
69+
return beanName + "#" + attributeName;
70+
}
71+
72+
/** Resolve the dynamic tag by fetching the attribute value from JMX. */
73+
public Map.Entry<String, String> resolve(Connection connection) {
74+
try {
75+
ObjectName objectName = new ObjectName(beanName);
76+
Object value = connection.getAttribute(objectName, attributeName);
77+
78+
if (value == null) {
79+
log.warn("Dynamic tag '{}' resolved to null for bean '{}' attribute '{}'",
80+
tagName, beanName, attributeName);
81+
return null;
82+
}
83+
84+
String stringValue = value.toString();
85+
log.info("Resolved dynamic tag '{}' to value '{}' from bean '{}' attribute '{}'",
86+
tagName, stringValue, beanName, attributeName);
87+
88+
return new HashMap.SimpleEntry<>(tagName, stringValue);
89+
90+
} catch (MalformedObjectNameException e) {
91+
log.error("Invalid bean name '{}' for dynamic tag '{}': {}",
92+
beanName, tagName, e.getMessage());
93+
return null;
94+
} catch (Exception e) {
95+
log.warn("Failed to resolve dynamic tag '{}' from bean '{}' attribute '{}': {}",
96+
tagName, beanName, attributeName, e.getMessage());
97+
log.debug("Dynamic tag resolution error details", e);
98+
return null;
99+
}
100+
}
101+
102+
@Override
103+
public String toString() {
104+
return String.format("DynamicTag{name='%s', bean='%s', attribute='%s'}",
105+
tagName, beanName, attributeName);
106+
}
107+
}
108+

src/main/java/org/datadog/jmxfetch/Instance.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public Yaml initialValue() {
7979
private ObjectName instanceTelemetryBeanName;
8080
private MBeanServer mbs;
8181
private Boolean normalizeBeanParamTags;
82+
private Map<String, Map.Entry<String, String>> dynamicTagsCache;
8283

8384
/** Constructor, instantiates Instance based of a previous instance and appConfig. */
8485
public Instance(Instance instance, AppConfig appConfig) {
@@ -447,14 +448,87 @@ public void init(boolean forceNewConnection)
447448
throws IOException, FailedLoginException, SecurityException {
448449
log.info("Trying to connect to JMX Server at " + this.toString());
449450
connection = getConnection(instanceMap, forceNewConnection);
451+
450452
log.info(
451453
"Trying to collect bean list for the first time for JMX Server at {}", this);
452454
this.refreshBeansList();
453455
this.initialRefreshTime = this.lastRefreshTime;
454456
log.info("Connected to JMX Server at {} with {} beans", this, this.beans.size());
457+
458+
// Resolve configuration-level dynamic tags for all configurations
459+
// Must be done after refreshBeansList() so the beans exist
460+
resolveConfigurationDynamicTags();
461+
455462
this.getMatchingAttributes();
456463
log.info("Done initializing JMX Server at {}", this);
457464
}
465+
466+
private void resolveConfigurationDynamicTags() {
467+
if (configurationList == null || configurationList.isEmpty()) {
468+
return;
469+
}
470+
471+
this.dynamicTagsCache = new HashMap<>();
472+
List<DynamicTag> allDynamicTags = new ArrayList<>();
473+
474+
for (Configuration config : configurationList) {
475+
List<DynamicTag> dynamicTags = config.getDynamicTags();
476+
if (dynamicTags != null && !dynamicTags.isEmpty()) {
477+
allDynamicTags.addAll(dynamicTags);
478+
}
479+
}
480+
481+
if (allDynamicTags.isEmpty()) {
482+
return;
483+
}
484+
485+
int successfulResolutions = 0;
486+
for (DynamicTag dynamicTag : allDynamicTags) {
487+
String cacheKey = dynamicTag.getBeanAttributeKey();
488+
if (!this.dynamicTagsCache.containsKey(cacheKey)) {
489+
Map.Entry<String, String> resolved = dynamicTag.resolve(connection);
490+
// Cache both successful and failed resolutions (null) to avoid retrying
491+
this.dynamicTagsCache.put(cacheKey, resolved);
492+
}
493+
// Count successful resolutions (cached value is not null)
494+
if (this.dynamicTagsCache.get(cacheKey) != null) {
495+
successfulResolutions++;
496+
}
497+
}
498+
499+
log.info("Resolved {} unique dynamic tag(s) from {} total references for instance {}",
500+
successfulResolutions, allDynamicTags.size(), instanceName);
501+
}
502+
503+
/**
504+
* Get resolved dynamic tags for a specific configuration.
505+
* This resolves the dynamic tags defined in the configuration using the cached values.
506+
*
507+
* @param config the configuration to get resolved tags for
508+
* @return map of tag name to tag value
509+
*/
510+
private Map<String, String> getResolvedDynamicTagsForConfig(Configuration config) {
511+
Map<String, String> resolvedTags = new HashMap<>();
512+
513+
if (this.dynamicTagsCache == null || this.dynamicTagsCache.isEmpty()) {
514+
return resolvedTags;
515+
}
516+
517+
List<DynamicTag> dynamicTags = config.getDynamicTags();
518+
if (dynamicTags == null || dynamicTags.isEmpty()) {
519+
return resolvedTags;
520+
}
521+
522+
for (DynamicTag dynamicTag : dynamicTags) {
523+
String cacheKey = dynamicTag.getBeanAttributeKey();
524+
Map.Entry<String, String> cached = this.dynamicTagsCache.get(cacheKey);
525+
if (cached != null) {
526+
resolvedTags.put(cached.getKey(), cached.getValue());
527+
}
528+
}
529+
530+
return resolvedTags;
531+
}
458532

459533
/** Returns a string representation for the instance. */
460534
@Override
@@ -690,6 +764,9 @@ private void getMatchingAttributes() throws IOException {
690764
for (Configuration conf : configurationList) {
691765
try {
692766
if (jmxAttribute.match(conf)) {
767+
Map<String, String> resolvedDynamicTags =
768+
getResolvedDynamicTagsForConfig(conf);
769+
jmxAttribute.setResolvedDynamicTags(resolvedDynamicTags);
693770
jmxAttribute.setMatchingConf(conf);
694771
metricsCount += jmxAttribute.getMetricsCount();
695772
this.matchingAttributes.add(jmxAttribute);

src/main/java/org/datadog/jmxfetch/JmxAttribute.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public abstract class JmxAttribute {
5959
new HashMap<String, Map<Object, Object>>();
6060
protected String[] tags;
6161
private Configuration matchingConf;
62+
private Map<String, String> resolvedDynamicTags;
6263
private List<String> defaultTagsList;
6364
private boolean cassandraAliasing;
6465
protected String checkName;
@@ -139,6 +140,15 @@ private void addAdditionalTags() {
139140
}
140141
}
141142
}
143+
144+
/** Add dynamic tags that were resolved at connection time. */
145+
private void addDynamicTags() {
146+
if (this.resolvedDynamicTags != null && !this.resolvedDynamicTags.isEmpty()) {
147+
for (Map.Entry<String, String> tag : this.resolvedDynamicTags.entrySet()) {
148+
this.defaultTagsList.add(tag.getKey() + ":" + tag.getValue());
149+
}
150+
}
151+
}
142152

143153
private void addServiceTags() {
144154
Iterable<String> serviceNames = this.serviceNameProvider.getServiceNames();
@@ -495,13 +505,20 @@ public Configuration getMatchingConf() {
495505
return matchingConf;
496506
}
497507

508+
/** Sets resolved dynamic tags for the attribute. */
509+
public void setResolvedDynamicTags(Map<String, String> resolvedDynamicTags) {
510+
this.resolvedDynamicTags = resolvedDynamicTags;
511+
}
512+
498513
/** Sets a matching configuration for the attribute. */
499514
public void setMatchingConf(Configuration matchingConf) {
500515
this.matchingConf = matchingConf;
501516

502517
// Now that we have the matchingConf we can:
503518
// - add additional tags
504519
this.addAdditionalTags();
520+
// - add dynamic tags that were resolved at connection time
521+
this.addDynamicTags();
505522
// - filter out excluded tags
506523
this.applyTagsBlackList();
507524
// Add the service tag(s) - comes last because if the service tag is blacklisted as
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.datadog.jmxfetch;
2+
3+
public class DynamicTagTestApp implements DynamicTagTestAppMBean {
4+
private final String clusterId;
5+
private final String version;
6+
private final int port;
7+
private double metric;
8+
9+
public DynamicTagTestApp() {
10+
this("local-kafka-cluster", "2.8.0", 9092);
11+
}
12+
13+
public DynamicTagTestApp(String clusterId, String version, int port) {
14+
this.clusterId = clusterId;
15+
this.version = version;
16+
this.port = port;
17+
this.metric = 100.0;
18+
}
19+
20+
@Override
21+
public String getClusterId() {
22+
return clusterId;
23+
}
24+
25+
@Override
26+
public String getVersion() {
27+
return version;
28+
}
29+
30+
@Override
31+
public int getPort() {
32+
return port;
33+
}
34+
35+
@Override
36+
public Double getMetric() {
37+
return metric;
38+
}
39+
40+
public void setMetric(double metric) {
41+
this.metric = metric;
42+
}
43+
}
44+
45+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.datadog.jmxfetch;
2+
3+
public interface DynamicTagTestAppMBean {
4+
String getClusterId();
5+
String getVersion();
6+
int getPort();
7+
Double getMetric();
8+
}
9+
10+

0 commit comments

Comments
 (0)