diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ed1ba9..73d217af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ Changelog ========= # 0.51.1 / TBC +- [FEATURE] Add support for WebSphere PMI Stats objects with subCollections [#XXX][] # 0.51.0 / 2025-10-28 - [FEATURE] Add configuration-level dynamic tags for JMX attribute values via `dynamic_tags` [#581][] diff --git a/src/main/java/org/datadog/jmxfetch/Instance.java b/src/main/java/org/datadog/jmxfetch/Instance.java index 82570c88..a01fadbe 100644 --- a/src/main/java/org/datadog/jmxfetch/Instance.java +++ b/src/main/java/org/datadog/jmxfetch/Instance.java @@ -704,6 +704,27 @@ private void getMatchingAttributes() throws IOException { cassandraAliasing, emptyDefaultHostname, normalizeBeanParamTags); + } else if (PmiSubCollectionAttribute.matchAttribute( + attributeType, beanName, attributeInfo, connection)) { + log.debug( + ATTRIBUTE + + beanName + + " : " + + attributeInfo + + " has attributeInfo WebSphere Stats type with " + + "subCollections"); + jmxAttribute = + new PmiSubCollectionAttribute( + attributeInfo, + beanName, + className, + instanceName, + checkName, + connection, + serviceNameProvider, + tags, + emptyDefaultHostname, + normalizeBeanParamTags); } else if (JmxComplexAttribute.matchAttributeType(attributeType)) { log.debug( ATTRIBUTE diff --git a/src/main/java/org/datadog/jmxfetch/JmxComplexAttribute.java b/src/main/java/org/datadog/jmxfetch/JmxComplexAttribute.java index e57cb84f..41ae83c6 100644 --- a/src/main/java/org/datadog/jmxfetch/JmxComplexAttribute.java +++ b/src/main/java/org/datadog/jmxfetch/JmxComplexAttribute.java @@ -76,6 +76,9 @@ private void populateSubAttributeList(Object attributeValue) { this.subAttributeList.addAll(JeeStatisticsAttributes.attributesFor(attributeValue)); } else if (JeeStatisticsAttributes.isJeeStat(attributeValue)) { this.subAttributeList.addAll(JeeStatisticsAttributes.getStatisticNames(attributeValue)); + } else { + log.trace("beanName {} attributeValue type {}: no match {}", + getBeanName(), attributeValue.getClass(), attributeValue); } } @@ -136,21 +139,6 @@ public boolean match(Configuration configuration) { return matchAttribute(configuration) && !excludeMatchAttribute(configuration); } - private boolean matchSubAttribute( - Filter params, String subAttributeName, boolean matchOnEmpty) { - if ((params.getAttribute() instanceof Map) - && ((Map) (params.getAttribute())) - .containsKey(subAttributeName)) { - return true; - } else if ((params.getAttribute() instanceof List - && ((List) (params.getAttribute())).contains(subAttributeName))) { - return true; - } else if (params.getAttribute() == null) { - return matchOnEmpty; - } - return false; - } - private boolean matchAttribute(Configuration configuration) { if (matchSubAttribute(configuration.getInclude(), getAttributeName(), true)) { return true; diff --git a/src/main/java/org/datadog/jmxfetch/JmxSubAttribute.java b/src/main/java/org/datadog/jmxfetch/JmxSubAttribute.java index 98a6e166..cddcb621 100644 --- a/src/main/java/org/datadog/jmxfetch/JmxSubAttribute.java +++ b/src/main/java/org/datadog/jmxfetch/JmxSubAttribute.java @@ -2,6 +2,8 @@ import org.datadog.jmxfetch.service.ServiceNameProvider; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -49,4 +51,80 @@ public Metric getCachedMetric(String name) { cachedMetrics.put(name, metric); return metric; } + + /** + * Check if a sub-attribute matches the filter parameters. + * + * @param params the filter parameters + * @param subAttributeName the sub-attribute name to check + * @param matchOnEmpty whether to match if the attribute filter is null + * @return true if the sub-attribute matches + */ + protected boolean matchSubAttribute( + Filter params, String subAttributeName, boolean matchOnEmpty) { + if ((params.getAttribute() instanceof Map) + && ((Map) (params.getAttribute())) + .containsKey(subAttributeName)) { + return true; + } else if ((params.getAttribute() instanceof List + && ((List) (params.getAttribute())).contains(subAttributeName))) { + return true; + } else if (params.getAttribute() == null) { + return matchOnEmpty; + } + return false; + } + + /** + * Get the attribute configuration for a specific metric key. + * + * @param key the metric key + * @return the attribute configuration map, or null if not found + */ + protected Map getAttributesFor(String key) { + Filter include = getMatchingConf().getInclude(); + if (include != null) { + Object includeAttribute = include.getAttribute(); + if (includeAttribute instanceof Map) { + return (Map) ((Map) includeAttribute).get(key); + } + } + return null; + } + + /** + * Sort and filter metrics based on limit and sort order. + * + * @param metricKey the metric key + * @param metrics the list of metrics to sort and filter + * @return the sorted and filtered list of metrics + */ + protected List sortAndFilter(String metricKey, List metrics) { + Map attributes = getAttributesFor(metricKey); + if (attributes == null || !attributes.containsKey("limit")) { + return metrics; + } + Integer limit = (Integer) attributes.get("limit"); + if (metrics.size() <= limit) { + return metrics; + } + MetricComparator comp = new MetricComparator(); + Collections.sort(metrics, comp); + String sort = (String) attributes.get("sort"); + if (sort == null || sort.equals("desc")) { + metrics.subList(0, limit).clear(); + } else { + metrics.subList(metrics.size() - limit, metrics.size()).clear(); + } + return metrics; + } + + /** + * Comparator for sorting metrics by value. + */ + protected static class MetricComparator implements Comparator { + public int compare(Metric o1, Metric o2) { + return Double.compare(o1.getValue(), o2.getValue()); + } + } } diff --git a/src/main/java/org/datadog/jmxfetch/JmxTabularAttribute.java b/src/main/java/org/datadog/jmxfetch/JmxTabularAttribute.java index 816e0aa2..778a1f2f 100644 --- a/src/main/java/org/datadog/jmxfetch/JmxTabularAttribute.java +++ b/src/main/java/org/datadog/jmxfetch/JmxTabularAttribute.java @@ -133,17 +133,6 @@ protected String[] getTags(String key, String subAttribute) return tags; } - private Map getAttributesFor(String key) { - Filter include = getMatchingConf().getInclude(); - if (include != null) { - Object includeAttribute = include.getAttribute(); - if (includeAttribute instanceof Map) { - return (Map) ((Map) includeAttribute).get(key); - } - } - return null; - } - @Override public List getMetrics() throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, @@ -179,32 +168,6 @@ public List getMetrics() return metrics; } - private List sortAndFilter(String metricKey, List metrics) { - Map attributes = getAttributesFor(metricKey); - if (!attributes.containsKey("limit")) { - return metrics; - } - Integer limit = (Integer) attributes.get("limit"); - if (metrics.size() <= limit) { - return metrics; - } - MetricComparator comp = new MetricComparator(); - Collections.sort(metrics, comp); - String sort = (String) attributes.get("sort"); - if (sort == null || sort.equals("desc")) { - metrics.subList(0, limit).clear(); - } else { - metrics.subList(metrics.size() - limit, metrics.size()).clear(); - } - return metrics; - } - - private class MetricComparator implements Comparator { - public int compare(Metric o1, Metric o2) { - return Double.compare(o1.getValue(), o2.getValue()); - } - } - private Object getValue(String key, String subAttribute) throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, ReflectionException, IOException { @@ -268,21 +231,6 @@ public boolean match(Configuration configuration) { return matchAttribute(configuration); // TODO && !excludeMatchAttribute(configuration); } - private boolean matchSubAttribute( - Filter params, String subAttributeName, boolean matchOnEmpty) { - if ((params.getAttribute() instanceof Map) - && ((Map) (params.getAttribute())) - .containsKey(subAttributeName)) { - return true; - } else if ((params.getAttribute() instanceof List - && ((List) (params.getAttribute())).contains(subAttributeName))) { - return true; - } else if (params.getAttribute() == null) { - return matchOnEmpty; - } - return false; - } - private boolean matchAttribute(Configuration configuration) { if (matchSubAttribute(configuration.getInclude(), getAttributeName(), true)) { return true; diff --git a/src/main/java/org/datadog/jmxfetch/PmiSubCollectionAttribute.java b/src/main/java/org/datadog/jmxfetch/PmiSubCollectionAttribute.java new file mode 100644 index 00000000..17f7699d --- /dev/null +++ b/src/main/java/org/datadog/jmxfetch/PmiSubCollectionAttribute.java @@ -0,0 +1,291 @@ +package org.datadog.jmxfetch; + +import lombok.extern.slf4j.Slf4j; +import org.datadog.jmxfetch.service.ServiceNameProvider; +import org.datadog.jmxfetch.util.JeeStatisticsAttributes; +import org.datadog.jmxfetch.util.PmiStatisticsAttributes; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +/** + * Handles WebSphere PMI Stats objects with subCollections. + * Each subCollection contains a named group of statistics that should be tagged + * with the subCollection name. + */ +@Slf4j +public class PmiSubCollectionAttribute extends JmxSubAttribute { + // Map from subCollection name to list of statistic attribute names (e.g., "statName.count") + private final Map> subCollectionAttributes; + + /** Constructor for PmiSubCollectionAttribute. */ + public PmiSubCollectionAttribute( + MBeanAttributeInfo attribute, + ObjectName beanName, + String className, + String instanceName, + String checkName, + Connection connection, + ServiceNameProvider serviceNameProvider, + Map instanceTags, + boolean emptyDefaultHostname, + boolean normalizeBeanParamTags) { + super( + attribute, + beanName, + className, + instanceName, + checkName, + connection, + serviceNameProvider, + instanceTags, + false, + emptyDefaultHostname, + normalizeBeanParamTags); + subCollectionAttributes = new HashMap>(); + } + + /** + * Check if this attribute is a WebSphere Stats object with subCollections. + * Performs comprehensive checks: + * 1. Domain must be "WebSphere" + * 2. Attribute type must be Stats + * 3. Connection used to fetch and verify the attribute has subCollections + */ + public static boolean matchAttribute( + String attributeType, + ObjectName beanName, + MBeanAttributeInfo attributeInfo, + Connection connection) { + // First check: domain must be WebSphere + if (!"WebSphere".equals(beanName.getDomain())) { + return false; + } + + // Second check: attribute type must be Stats + if (!"javax.management.j2ee.statistics.Stats".equals(attributeType)) { + return false; + } + + // Third and fourth check: fetch the attribute value and verify it has + // subCollections + try { + Object value = connection.getAttribute(beanName, attributeInfo.getName()); + if (value == null) { + return false; + } + + // Fourth check: must be a JEE Stat with subCollections + if (!JeeStatisticsAttributes.isJeeStat(value)) { + return false; + } + + return PmiStatisticsAttributes.hasSubCollections(value); + } catch (Exception e) { + log.debug( + "Unable to fetch attribute {} from {} to check for subCollections: {}", + attributeInfo.getName(), + beanName, + e.getMessage()); + return false; + } + } + + private boolean matchAttribute(Configuration configuration) { + if (matchSubAttribute(configuration.getInclude(), getAttributeName(), true)) { + return true; + } + + Iterator it1 = subCollectionAttributes.keySet().iterator(); + while (it1.hasNext()) { + String subCollectionName = it1.next(); + List attributes = subCollectionAttributes.get(subCollectionName); + Iterator it2 = attributes.iterator(); + while (it2.hasNext()) { + String attrKey = it2.next(); + if (!matchSubAttribute( + configuration.getInclude(), getAttributeName() + "." + attrKey, true)) { + it2.remove(); + } + } + if (attributes.isEmpty()) { + it1.remove(); + } + } + + return !subCollectionAttributes.isEmpty(); + } + + private void populateSubCollectionAttributes(Object value) { + if (!JeeStatisticsAttributes.isJeeStat(value)) { + return; + } + + // Get the subCollections structure from JeeStatisticsAttributes + Map> subCollections = + PmiStatisticsAttributes.getSubCollectionStatistics(value); + + if (!subCollections.isEmpty()) { + subCollectionAttributes.putAll(subCollections); + int totalAttributes = 0; + for (Map.Entry> entry : subCollections.entrySet()) { + String subCollName = entry.getKey(); + List attrs = entry.getValue(); + totalAttributes += attrs.size(); + log.trace("SubCollection '{}' attributes: {}", subCollName, attrs); + } + log.debug("Found {} subCollections in {} with total {} attributes", + subCollections.size(), getBeanName(), totalAttributes); + } + + // Also get regular statistics (not in subCollections) + List regularStats = JeeStatisticsAttributes.getStatisticNames(value); + if (!regularStats.isEmpty()) { + // Stats not in a subCollection go under a default key + subCollectionAttributes.put("", regularStats); + log.debug("Found {} regular statistics in {}", regularStats.size(), getBeanName()); + log.trace("Regular statistics attributes: {}", regularStats); + } + } + + protected String[] getTags(String subCollectionName, String subAttribute) + throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, + ReflectionException, IOException { + List tagsList = new ArrayList(); + String fullMetricKey = getAttributeName() + "." + subAttribute; + Map attributeParams = getAttributesFor(fullMetricKey); + + if (attributeParams != null) { + Map yamlTags = (Map) attributeParams.get("tags"); + + if (yamlTags != null) { + for (String tagName : yamlTags.keySet()) { + String value = yamlTags.get(tagName); + + // Support tag value substitution (e.g., $subCollectionName) + if (value.startsWith("$")) { + String varName = value.substring(1); + if ("subCollection".equals(varName) && !subCollectionName.isEmpty()) { + value = subCollectionName; + } + } + + tagsList.add(tagName + ":" + value); + } + } + } + + // Add subCollection name as a tag if it's not empty + if (!subCollectionName.isEmpty()) { + tagsList.add("subcollection:" + subCollectionName); + } + + String[] defaultTags = super.getTags(); + tagsList.addAll(Arrays.asList(defaultTags)); + + String[] tags = new String[tagsList.size()]; + tags = tagsList.toArray(tags); + return tags; + } + + @Override + public List getMetrics() + throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, + ReflectionException, IOException { + Map> subMetrics = new HashMap<>(); + + for (Map.Entry> entry : subCollectionAttributes.entrySet()) { + String subCollectionName = entry.getKey(); + List statisticAttributes = entry.getValue(); + + for (String metricKey : statisticAttributes) { + String alias = getAlias(metricKey); + String metricType = getMetricType(metricKey); + String[] tags = getTags(subCollectionName, metricKey); + Metric metric = new Metric(alias, metricType, tags, checkName); + double value = castToDouble(getValue(subCollectionName, metricKey), null); + metric.setValue(value); + + String fullMetricKey = getAttributeName() + "." + metricKey; + if (!subMetrics.containsKey(fullMetricKey)) { + subMetrics.put(fullMetricKey, new ArrayList()); + } + subMetrics.get(fullMetricKey).add(metric); + } + } + + List metrics = new ArrayList<>(subMetrics.size()); + for (String key : subMetrics.keySet()) { + // Only add explicitly included metrics + if (getAttributesFor(key) != null) { + metrics.addAll(sortAndFilter(key, subMetrics.get(key))); + } + } + + return metrics; + } + + private Object getValue(String subCollectionName, String subAttribute) + throws AttributeNotFoundException, InstanceNotFoundException, MBeanException, + ReflectionException, IOException { + + Object value = this.getJmxValue(); + + if (JeeStatisticsAttributes.isJeeStat(value)) { + // If this is from a subCollection, prefix with the subCollection name + String fullPath = subCollectionName.isEmpty() + ? subAttribute + : subCollectionName + "." + subAttribute; + return PmiStatisticsAttributes.getStatisticDataFor(value, fullPath); + } + + throw new NumberFormatException(); + } + + @Override + public boolean match(Configuration configuration) { + if (!matchDomain(configuration) + || !matchClassName(configuration) + || !matchBean(configuration) + || excludeMatchDomain(configuration) + || excludeMatchClassName(configuration) + || excludeMatchBean(configuration)) { + return false; + } + + try { + Object value = getJmxValue(); + + // Only match if this is a Stats object with subCollections + if (!JeeStatisticsAttributes.isJeeStat(value)) { + return false; + } + + // Check if it has subCollections + if (!PmiStatisticsAttributes.hasSubCollections(value)) { + return false; + } + + populateSubCollectionAttributes(value); + } catch (Exception e) { + log.debug("Failed to populate subCollection attributes for {}: {}", + getBeanName(), e.getMessage()); + return false; + } + + return matchAttribute(configuration); + } +} diff --git a/src/main/java/org/datadog/jmxfetch/util/JeeStatisticsAttributes.java b/src/main/java/org/datadog/jmxfetch/util/JeeStatisticsAttributes.java index e3338bbc..12bfb298 100644 --- a/src/main/java/org/datadog/jmxfetch/util/JeeStatisticsAttributes.java +++ b/src/main/java/org/datadog/jmxfetch/util/JeeStatisticsAttributes.java @@ -21,22 +21,22 @@ public class JeeStatisticsAttributes { /** Attributes for @see javax.management.j2ee.statistics.CountStatistic */ - private static final List COUNT_ATTRIBUTES = Collections.singletonList("count"); + static final List COUNT_ATTRIBUTES = Collections.singletonList("count"); /** Attributes for @see javax.management.j2ee.statistics.BoundaryStatistic */ - private static final List BOUNDARY_ATTRIBUTES = + static final List BOUNDARY_ATTRIBUTES = Collections.unmodifiableList(Arrays.asList("upperBound", "lowerBound")); /** Attributes for @see javax.management.j2ee.statistics.TimeStatistic */ - private static final List TIME_ATTRIBUTES = + static final List TIME_ATTRIBUTES = Collections.unmodifiableList(Arrays.asList("count", "minTime", "maxTime", "totalTime")); /** Attributes for @see javax.management.j2ee.statistics.RangeStatistic */ - private static final List RANGE_ATTRIBUTES = + static final List RANGE_ATTRIBUTES = Collections.unmodifiableList(Arrays.asList("highWaterMark", "lowWaterMark", "current")); /** Attributes for @see javax.management.j2ee.statistics.BoundedRangeStatistic */ - private static final List BOUNDED_RANGE_ATTRIBUTES = + static final List BOUNDED_RANGE_ATTRIBUTES = Collections.unmodifiableList( Arrays.asList("upperBound", "lowerBound", "highWaterMark", "lowWaterMark", "current")); @@ -44,7 +44,7 @@ public class JeeStatisticsAttributes { private static final WeakHashMap> REFLECTION_CACHE = new WeakHashMap<>(); - private static class ReflectionHolder { + static class ReflectionHolder { public final Class classStat; public final MethodHandle mhStatGetStatisticNames; @@ -122,7 +122,7 @@ private Map, Map> buildMethodCache() { return map; } - private static MethodHandle maybeFindMethodHandleFor( + static MethodHandle maybeFindMethodHandleFor( final Class cls, final String name, final Class... parameterTypes) { if (cls == null) { return null; @@ -143,7 +143,7 @@ public MethodHandle run() { }); } - private static Map buildMethodCacheFor( + static Map buildMethodCacheFor( final Class cls, final List attributes) { final Map map = new HashMap<>(); for (String attribute : attributes) { @@ -156,7 +156,7 @@ private static Map buildMethodCacheFor( return map; } - private static Class maybeLookupClass(final String name, final ClassLoader classLoader) { + static Class maybeLookupClass(final String name, final ClassLoader classLoader) { try { return Class.forName(name, false, classLoader); } catch (Throwable t) { @@ -168,7 +168,7 @@ private static Class maybeLookupClass(final String name, final ClassLoader cl return null; } - private static String getterMethodName(String attribute) { + static String getterMethodName(String attribute) { // inspired from JavaBean PropertyDescriptor return "get" + attribute.substring(0, 1).toUpperCase(Locale.ROOT) + attribute.substring(1); @@ -210,6 +210,8 @@ public static List attributesFor(Object instance) { if (rh.classBoundaryStatistic != null && rh.classBoundaryStatistic.isInstance(instance)) { return BOUNDARY_ATTRIBUTES; } + + LOGGER.debug("Getting attributes for class of type {} not supported", instance.getClass()); return Collections.emptyList(); } diff --git a/src/main/java/org/datadog/jmxfetch/util/PmiStatisticsAttributes.java b/src/main/java/org/datadog/jmxfetch/util/PmiStatisticsAttributes.java new file mode 100644 index 00000000..52fdedaf --- /dev/null +++ b/src/main/java/org/datadog/jmxfetch/util/PmiStatisticsAttributes.java @@ -0,0 +1,557 @@ +package org.datadog.jmxfetch.util; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.invoke.MethodHandle; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import javax.management.AttributeNotFoundException; +import javax.management.ReflectionException; + +/** + * Utility class for working with IBM WebSphere PMI + * (Performance Monitoring Infrastructure) Statistics. + * PMI classes are IBM-specific implementations that don't implement standard JEE interfaces, + * requiring class name-based detection and optimized caching strategies. + * + *

Also handles WebSphere PMI subCollections - named groups of statistics within + * a Stats object, commonly used for JDBC connection pools and other resources. + */ +@Slf4j +public class PmiStatisticsAttributes { + // Cache for WebSphere PMI reflection metadata + private static final WeakHashMap> + REFLECTION_CACHE = new WeakHashMap<>(); + + private PmiStatisticsAttributes() { + // Utility class + } + + /** + * Reflection holder for WebSphere PMI classes. + */ + private static class ReflectionHolder { + // PMI Statistic implementation classes + private final Class classCountStatisticImpl; + private final Class classTimeStatisticImpl; + private final Class classBoundedRangeStatisticImpl; + private final Class classRangeStatisticImpl; + private final Class classBoundaryStatisticImpl; + + // Method cache per class type + private final Map, Map> methodCache; + + // SubCollection support + public final Class classStat; + public final Class classStatsImpl; // com.ibm.ws.pmi.stat.StatsImpl + public final Class classJ2eeStatsImpl; // com.ibm.ws.pmi.j2ee.StatsImpl + + // Method handles for com.ibm.ws.pmi.stat.StatsImpl + public final MethodHandle mhStatsImplSubCollection; + public final MethodHandle mhStatsImplGetName; + public final MethodHandle mhStatsImplGetStatisticNames; + public final MethodHandle mhStatsImplGetStatistic; + + // Method handles for com.ibm.ws.pmi.j2ee.StatsImpl (used by most Stats implementations) + public final MethodHandle mhJ2eeStatsImplGetStatistic; + public final MethodHandle mhJ2eeStatsImplGetStatisticNames; + public final MethodHandle mhJ2eeStatsImplGetWsImpl; + + ReflectionHolder(final ClassLoader classLoader) { + // Load PMI Statistic implementation classes + classCountStatisticImpl = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.CountStatisticImpl", classLoader); + classTimeStatisticImpl = + JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.TimeStatisticImpl", classLoader); + classBoundedRangeStatisticImpl = + JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.BoundedRangeStatisticImpl", classLoader); + classRangeStatisticImpl = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.RangeStatisticImpl", classLoader); + classBoundaryStatisticImpl = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.BoundaryStatisticImpl", classLoader); + + // Build method cache for PMI classes + methodCache = buildMethodCache(); + + // SubCollection support + classStat = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "javax.management.j2ee.statistics.Stats", classLoader); + classStatsImpl = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.stat.StatsImpl", classLoader); + classJ2eeStatsImpl = JeeStatisticsAttributes.ReflectionHolder.maybeLookupClass( + "com.ibm.ws.pmi.j2ee.StatsImpl", classLoader); + + // Initialize method handles for com.ibm.ws.pmi.stat.StatsImpl + if (classStatsImpl != null) { + mhStatsImplSubCollection = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classStatsImpl, "subCollections"); + mhStatsImplGetName = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classStatsImpl, "getName"); + mhStatsImplGetStatisticNames = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classStatsImpl, "getStatisticNames"); + mhStatsImplGetStatistic = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classStatsImpl, "getStatistic", String.class); + } else { + mhStatsImplSubCollection = null; + mhStatsImplGetName = null; + mhStatsImplGetStatisticNames = null; + mhStatsImplGetStatistic = null; + } + + // Initialize method handles for com.ibm.ws.pmi.j2ee.StatsImpl + if (classJ2eeStatsImpl != null) { + mhJ2eeStatsImplGetStatistic = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classJ2eeStatsImpl, "getStatistic", String.class); + mhJ2eeStatsImplGetStatisticNames = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classJ2eeStatsImpl, "getStatisticNames"); + mhJ2eeStatsImplGetWsImpl = + JeeStatisticsAttributes.ReflectionHolder.maybeFindMethodHandleFor( + classJ2eeStatsImpl, "getWSImpl"); + } else { + mhJ2eeStatsImplGetStatistic = null; + mhJ2eeStatsImplGetStatisticNames = null; + mhJ2eeStatsImplGetWsImpl = null; + } + } + + private Map, Map> buildMethodCache() { + Map, Map> map = new HashMap<>(); + if (classCountStatisticImpl != null) { + map.put(classCountStatisticImpl, + JeeStatisticsAttributes.ReflectionHolder.buildMethodCacheFor( + classCountStatisticImpl, JeeStatisticsAttributes.COUNT_ATTRIBUTES)); + } + if (classTimeStatisticImpl != null) { + map.put(classTimeStatisticImpl, + JeeStatisticsAttributes.ReflectionHolder.buildMethodCacheFor( + classTimeStatisticImpl, JeeStatisticsAttributes.TIME_ATTRIBUTES)); + } + if (classBoundaryStatisticImpl != null) { + map.put(classBoundaryStatisticImpl, + JeeStatisticsAttributes.ReflectionHolder.buildMethodCacheFor( + classBoundaryStatisticImpl, JeeStatisticsAttributes.BOUNDARY_ATTRIBUTES)); + } + if (classRangeStatisticImpl != null) { + map.put(classRangeStatisticImpl, + JeeStatisticsAttributes.ReflectionHolder.buildMethodCacheFor( + classRangeStatisticImpl, JeeStatisticsAttributes.RANGE_ATTRIBUTES)); + } + if (classBoundedRangeStatisticImpl != null) { + map.put(classBoundedRangeStatisticImpl, + JeeStatisticsAttributes.ReflectionHolder.buildMethodCacheFor( + classBoundedRangeStatisticImpl, + JeeStatisticsAttributes.BOUNDED_RANGE_ATTRIBUTES)); + } + return map; + } + } + + private static ReflectionHolder getOrCreateReflectionHolder(final ClassLoader classLoader) { + // no need to lock here. At worst, we'll do it more time if there is contention. + SoftReference ref = REFLECTION_CACHE.get(classLoader); + if (ref != null && ref.get() != null) { + return ref.get(); + } + final ReflectionHolder holder = new ReflectionHolder(classLoader); + REFLECTION_CACHE.put(classLoader, new SoftReference<>(holder)); + return holder; + } + + /** + * Fetch the data for a PMI Statistic instance given an attribute name. + * For non-PMI classes, delegates to JeeStatisticsAttributes.dataFor(). + * + * @param instance the Statistic instance (maybe PMI or standard J2EE) + * @param attribute the attribute name (e.g., "count", "current", "upperBound") + * @return the attribute value + * @throws ReflectionException if reflection fails + * @throws AttributeNotFoundException if attribute not found + */ + public static long dataFor(Object instance, String attribute) + throws ReflectionException, AttributeNotFoundException { + Class cls = null; + ReflectionHolder rh = getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + if (rh.classCountStatisticImpl != null + && rh.classCountStatisticImpl.isInstance(instance)) { + cls = rh.classCountStatisticImpl; + } else if (rh.classTimeStatisticImpl != null + && rh.classTimeStatisticImpl.isInstance(instance)) { + cls = rh.classTimeStatisticImpl; + } else if (rh.classBoundedRangeStatisticImpl != null + && rh.classBoundedRangeStatisticImpl.isInstance(instance)) { + cls = rh.classBoundedRangeStatisticImpl; + } else if (rh.classRangeStatisticImpl != null + && rh.classRangeStatisticImpl.isInstance(instance)) { + cls = rh.classRangeStatisticImpl; + } else if (rh.classBoundaryStatisticImpl != null + && rh.classBoundaryStatisticImpl.isInstance(instance)) { + cls = rh.classBoundaryStatisticImpl; + } + if (cls == null) { + // Not a PMI class - delegate to standard implementation + return JeeStatisticsAttributes.dataFor(instance, attribute); + } + MethodHandle methodHandle = rh.methodCache.get(cls).get(attribute); + if (methodHandle == null) { + throw new AttributeNotFoundException( + "Unable to find getter for attribute " + attribute + " on class " + cls.getName()); + } + try { + return (long) methodHandle.invoke(instance); + } catch (Throwable t) { + throw new ReflectionException( + new Exception(t), + "Unable to invoke getter for attribute " + attribute + " on class " + + cls.getName()); + } + } + + /** + * Get the list of available attributes for a Statistic object. + * Handles both PMI implementation classes and standard JEE Statistics. + * For PMI classes, uses cached Class references; for standard JEE, delegates. + * + * @param instance the Statistic instance + * @return list of attribute names (e.g., ["count"] or ["current", "upperBound", ...]) + */ + public static List attributesFor(Object instance) { + ReflectionHolder rh = getOrCreateReflectionHolder( + instance.getClass().getClassLoader()); + + // Check PMI implementation classes using instanceof (via cached Class references) + if (rh.classCountStatisticImpl != null + && rh.classCountStatisticImpl.isInstance(instance)) { + return JeeStatisticsAttributes.COUNT_ATTRIBUTES; + } + if (rh.classTimeStatisticImpl != null + && rh.classTimeStatisticImpl.isInstance(instance)) { + return JeeStatisticsAttributes.TIME_ATTRIBUTES; + } + if (rh.classBoundedRangeStatisticImpl != null + && rh.classBoundedRangeStatisticImpl.isInstance(instance)) { + return JeeStatisticsAttributes.BOUNDED_RANGE_ATTRIBUTES; + } + if (rh.classRangeStatisticImpl != null + && rh.classRangeStatisticImpl.isInstance(instance)) { + return JeeStatisticsAttributes.RANGE_ATTRIBUTES; + } + if (rh.classBoundaryStatisticImpl != null + && rh.classBoundaryStatisticImpl.isInstance(instance)) { + return JeeStatisticsAttributes.BOUNDARY_ATTRIBUTES; + } + + // Not PMI - delegate to standard implementation + return JeeStatisticsAttributes.attributesFor(instance); + } + + /** + * Get statistic names from a Stats object. + * Handles both PMI Stats objects and standard JEE Stats. + * Returns full paths like "StatisticName.attribute" (e.g., "PoolSize.current"). + * + * @param instance a Stats instance + * @return list of statistic paths + */ + public static List getStatisticNames(Object instance) { + List ret = new ArrayList<>(); + ReflectionHolder rh = getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + + try { + final MethodHandle mhGetStatisticNames; + final MethodHandle mhGetStatistic; + + // Determine which method handles to use based on class hierarchy + if (rh.classJ2eeStatsImpl != null + && rh.classJ2eeStatsImpl.isAssignableFrom(instance.getClass())) { + mhGetStatisticNames = rh.mhJ2eeStatsImplGetStatisticNames; + mhGetStatistic = rh.mhJ2eeStatsImplGetStatistic; + } else if (rh.classStatsImpl != null + && rh.classStatsImpl.isAssignableFrom(instance.getClass())) { + mhGetStatisticNames = rh.mhStatsImplGetStatisticNames; + mhGetStatistic = rh.mhStatsImplGetStatistic; + } else { + return Collections.emptyList(); + } + + String[] names = (String[]) mhGetStatisticNames.invoke(instance); + if (names != null && mhGetStatistic != null) { + for (String name : names) { + Object stat = mhGetStatistic.invoke(instance, name); + List attrs = attributesFor(stat); + for (String attr : attrs) { + ret.add(name + "." + attr); + } + } + } + } catch (Throwable t) { + log.debug("Unable to get statistic names from Stats class {}: {}", + instance.getClass(), t.getMessage()); + } + + return ret; + } + + /** + * Check if a Stats instance has subCollections. + * + * @param instance a Stats instance + * @return true if the instance has subCollections + */ + public static boolean hasSubCollections(Object instance) { + ReflectionHolder rh = + getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + + if (!JeeStatisticsAttributes.isJeeStat(instance)) { + return false; + } + + // WebSphere J2EE Stats implementations (JDBCStatsImpl, ServletStatsImpl, etc.) + // are wrapper classes with a getWSImpl() method that returns the actual + // StatsImpl which has the subCollections() method + if (rh.classStat != null && rh.classStat.isInstance(instance)) { + try { + MethodHandle mhGetWsImpl = rh.mhJ2eeStatsImplGetWsImpl; + if (mhGetWsImpl != null) { + Object wsImpl = mhGetWsImpl.invoke(instance); + if (wsImpl != null && rh.mhStatsImplSubCollection != null) { + ArrayList subCollections = + (ArrayList) rh.mhStatsImplSubCollection.invoke(wsImpl); + return !subCollections.isEmpty(); + } + } + } catch (Throwable t) { + log.trace("No subCollections via getWSImpl for {}", instance.getClass()); + } + } + + return false; + } + + /** + * Get subCollection statistics structure. + * Returns a map from subCollection name to list of statistic attribute names. + * + * @param instance a Stats instance + * @return map of subCollection name to list of statistic paths (e.g., "statName.count") + */ + public static Map> getSubCollectionStatistics(Object instance) { + ReflectionHolder rh = + getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + Map> result = new HashMap<>(); + + if (!JeeStatisticsAttributes.isJeeStat(instance)) { + return result; + } + + // WebSphere JEE Stats implementations have getWSImpl() method that returns + // the actual StatsImpl object with subCollections + try { + MethodHandle mhGetWsImpl = rh.mhJ2eeStatsImplGetWsImpl; + if (mhGetWsImpl != null) { + Object wsImpl = mhGetWsImpl.invoke(instance); + if (wsImpl != null && rh.mhStatsImplSubCollection != null) { + ArrayList subCollections = + (ArrayList) rh.mhStatsImplSubCollection.invoke(wsImpl); + if (subCollections != null && !subCollections.isEmpty()) { + return extractSubCollectionStats(subCollections, rh); + } + } + } + } catch (Throwable t) { + log.trace("Could not get subCollections via getWSImpl: {}", t.getMessage()); + } + + return result; + } + + /** + * Extract statistics from a list of subCollection objects. + */ + private static Map> extractSubCollectionStats( + ArrayList subCollections, + ReflectionHolder rh) { + Map> result = new HashMap<>(); + + for (Object subColl : subCollections) { + try { + // Get the name of this subCollection + String subCollName = (String) rh.mhStatsImplGetName.invoke(subColl); + + // Get the statistic names for this subCollection + // getStatisticNames() returns full paths like "PoolSize.current" + // So we can just use them directly as attributes + List statNames = getStatisticNames(subColl); + List attributes = new ArrayList<>(statNames); + + if (!attributes.isEmpty()) { + result.put(subCollName, attributes); + log.debug("SubCollection '{}' has {} attributes", + subCollName, attributes.size()); + } + } catch (Throwable t) { + log.debug("Could not process subCollection: {}", t.getMessage()); + } + } + + return result; + } + + /** + * Get a list of subCollections from a Stats instance. + * Used internally for navigation in getStatisticDataFor. + */ + private static ArrayList getSubCollections(Object instance) { + ReflectionHolder rh = + getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + + try { + // WebSphere JEE Stats are wrapper classes - get actual StatsImpl via getWSImpl() + MethodHandle mhGetWsImpl = rh.mhJ2eeStatsImplGetWsImpl; + if (mhGetWsImpl != null) { + Object wsImpl = mhGetWsImpl.invoke(instance); + if (wsImpl != null && rh.mhStatsImplSubCollection != null) { + return (ArrayList) rh.mhStatsImplSubCollection.invoke(wsImpl); + } + } + return null; + } catch (Throwable t) { + log.trace("Could not get subCollections: {}", t.getMessage()); + return null; + } + } + + /** + * Get the name of a subCollection object. + */ + private static String getName(Object subCollection) { + ReflectionHolder rh = getOrCreateReflectionHolder( + subCollection.getClass().getClassLoader()); + + try { + if (rh.mhStatsImplGetName != null) { + return (String) rh.mhStatsImplGetName.invoke(subCollection); + } + } catch (Throwable t) { + log.trace("Could not get subCollection name: {}", t.getMessage()); + } + return null; + } + + /** + * Get statistic data for a given path in a Stats instance. + * Handles both direct statistics and subCollection statistics. + * + * @param instance Stats instance + * @param name Statistic path (e.g., "PoolSize.current" or + * "subCollectionName.PoolSize.current") + * @return the statistic value + * @throws AttributeNotFoundException if not found + */ + public static long getStatisticDataFor(Object instance, String name) + throws AttributeNotFoundException { + ReflectionHolder rh = getOrCreateReflectionHolder(instance.getClass().getClassLoader()); + int idx = name.indexOf("."); + if (idx == -1) { + throw new AttributeNotFoundException("Invalid attribute name " + name); + } + + String firstPart = name.substring(0, idx); + String remainder = name.substring(idx + 1); + + MethodHandle mhGetStatistic = null; + if (rh.classJ2eeStatsImpl != null + && rh.classJ2eeStatsImpl.isAssignableFrom(instance.getClass())) { + mhGetStatistic = rh.mhJ2eeStatsImplGetStatistic; + } else if (rh.classStatsImpl != null + && rh.classStatsImpl.isAssignableFrom(instance.getClass())) { + mhGetStatistic = rh.mhStatsImplGetStatistic; + } + + // Try direct statistic first using getStatistic method + try { + if (mhGetStatistic != null) { + Object stat = mhGetStatistic.invoke(instance, firstPart); + if (stat != null && JeeStatisticsAttributes.isJeeStatistic(stat)) { + return dataFor(stat, remainder); + } + } + } catch (Throwable t) { + log.trace("'{}' is not a direct statistic, trying as subCollection", firstPart); + } + + // Try as subCollection (firstPart is subCollection name) + try { + ArrayList subCollections = getSubCollections(instance); + + if (subCollections != null) { + for (Object subColl : subCollections) { + String subCollName = getName(subColl); + if (firstPart.equals(subCollName)) { + try { + MethodHandle subCollMhGetStatistic = rh.mhStatsImplGetStatistic; + if (subCollMhGetStatistic != null) { + // Parse remainder to get statistic name and attribute + int idx2 = remainder.indexOf("."); + if (idx2 != -1) { + String statName = remainder.substring(0, idx2); + String attrName = remainder.substring(idx2 + 1); + log.trace( + "Getting statistic '{}' from subCollection '{}' class {}", + statName, subCollName, subColl.getClass().getName()); + Object stat = subCollMhGetStatistic.invoke(subColl, statName); + if (stat != null) { + log.trace( + "Got statistic object of type {}, isJeeStatistic={}", + stat.getClass().getName(), + JeeStatisticsAttributes.isJeeStatistic(stat)); + try { + return dataFor(stat, attrName); + } catch (Throwable t3) { + log.debug( + "Failed to get attribute '{}' from statistic: {}", + attrName, t3.getMessage()); + } + } + log.debug("Statistic '{}' returned null", statName); + } + } else { + log.debug( + "No getStatistic method found on subCollection class {}", + subColl.getClass().getName()); + } + } catch (Throwable t2) { + log.debug("Failed to get statistic from subCollection: {}", + t2.getMessage()); + } + + // Fallback: try recursive call (for nested subCollections) + log.trace("Trying recursive call for subCollection"); + return getStatisticDataFor(subColl, remainder); + } + } + } + } catch (AttributeNotFoundException e) { + // Re-throw AttributeNotFoundException + throw e; + } catch (Throwable t) { + log.debug("Failed to access subCollection '{}': {}", firstPart, t.getMessage()); + } + + throw new AttributeNotFoundException( + "Unable to get statistic with name " + name + " from jee stat class " + + instance.getClass()); + } +}