diff --git a/instrumentation/solr-jmx-7.4.0/build.gradle b/instrumentation/solr-jmx-7.4.0/build.gradle index f796bad204..101ff742a3 100644 --- a/instrumentation/solr-jmx-7.4.0/build.gradle +++ b/instrumentation/solr-jmx-7.4.0/build.gradle @@ -19,4 +19,9 @@ verifyInstrumentation { passesOnly 'org.apache.solr:solr-core:[7.4.0,9.0.0)' excludeRegex 'org.apache.solr:solr-core:.*(ALPHA|BETA)+$' -} \ No newline at end of file +} + +site { + title 'Solr' + type 'Datastore' +} diff --git a/instrumentation/solr-jmx-9.0.0/build.gradle b/instrumentation/solr-jmx-9.0.0/build.gradle new file mode 100644 index 0000000000..4a72729313 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/build.gradle @@ -0,0 +1,39 @@ +dependencies { + implementation(project(":agent-bridge")) + implementation(project(":agent-bridge-datastore")) + implementation(project(":newrelic-api")) + implementation(project(":newrelic-weaver-api")) + + implementation("org.apache.lucene:lucene-core:9.0.0") + implementation("org.apache.solr:solr-core:9.0.0") { + exclude(group: "org.restlet.jee", module: "org.restlet") + exclude(group: "org.restlet.jee", module: "org.restlet.ext.servlet") + } +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.solr-jmx-9.0.0' } +} + +verifyInstrumentation { + passesOnly 'org.apache.solr:solr-core:[9.0.0,10.0.0)' + + excludeRegex 'org.apache.solr:solr-core:.*(ALPHA|BETA)+$' +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +test { + onlyIf { + !project.hasProperty('test8') + } +} + +site { + title 'Solr' + type 'Datastore' +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/CacheMetric.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/CacheMetric.java new file mode 100644 index 0000000000..0d2f06adb6 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/CacheMetric.java @@ -0,0 +1,65 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.solr; + +import com.codahale.metrics.Metric; +import com.newrelic.api.agent.NewRelic; +import org.apache.solr.core.SolrInfoBean; +import org.apache.solr.metrics.MetricsMap; + +import java.util.Map; + +public class CacheMetric extends NRMetric { + + MetricsMap metric = null; + String metricType = null; + + public CacheMetric(String mt, String r, Metric m, String b) { + super(r, b); + metricType = mt; + if (MetricsMap.class.isInstance(m)) { + metric = (MetricsMap) m; + } + } + + public CacheMetric(String mt, String r, Metric m, String b, String tag) { + super(r, b); + metricType = mt; + if (MetricsMap.class.isInstance(m)) { + metric = (MetricsMap) m; + } + this.contextTag = tag; + } + + @Override + public String getMetricName(String name) { + return getMetricBase() + "/" + name; + } + + @Override + public int reportMetrics() { + int numMetrics = 0; + Map map = metric.getValue(); + for (String key : map.keySet()) { + Object obj = map.get(key); + if (Number.class.isInstance(obj)) { + Number num = (Number) obj; + String fullMetricName = getMetricName(key); + NewRelic.recordMetric(fullMetricName, num.floatValue()); + numMetrics++; + } + } + return numMetrics; + } + + @Override + public String getMetricBase() { + return prefix + registry + "/" + metricType + "/" + name; + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/GaugeMetric.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/GaugeMetric.java new file mode 100644 index 0000000000..401aec8cf7 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/GaugeMetric.java @@ -0,0 +1,60 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.solr; + +import com.codahale.metrics.Gauge; +import com.newrelic.api.agent.NewRelic; +import org.apache.solr.core.SolrInfoBean; + +public class GaugeMetric extends NRMetric { + + @SuppressWarnings("rawtypes") + Gauge metric; + String metricType; + String metricName; + + @SuppressWarnings("rawtypes") + public GaugeMetric(String mn, String mt, String r, Gauge m, String b) { + super(r, b); + metric = m; + metricType = mt; + metricName = mn; + } + + @SuppressWarnings("rawtypes") + public GaugeMetric(String mn, String mt, String r, Gauge m, String b, String tag) { + super(r, b); + metric = m; + metricType = mt; + metricName = mn; + this.contextTag = tag; + } + + @Override + public String getMetricName(String name) { + return getMetricBase() + "/" + name; + } + + @Override + public String getMetricBase() { + return prefix + registry + "/" + metricType + "/" + name; + } + + @Override + public int reportMetrics() { + int numMetrics = 0; + Object obj = metric.getValue(); + if (Number.class.isInstance(obj)) { + Number num = (Number) obj; + NewRelic.recordMetric(getMetricName(metricName), num.floatValue()); + numMetrics++; + } + return numMetrics; + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MeteredMetric.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MeteredMetric.java new file mode 100644 index 0000000000..e96d6c6789 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MeteredMetric.java @@ -0,0 +1,53 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.solr; + +import com.codahale.metrics.Metered; +import com.newrelic.api.agent.NewRelic; +import org.apache.solr.core.SolrInfoBean; + +public class MeteredMetric extends NRMetric { + + Metered metered; + String metricType; + String metricName; + + public MeteredMetric(String mn, String mt, String r, String b, Metered m) { + super(r, b); + metered = m; + metricType = mt; + metricName = mn; + } + + public MeteredMetric(String mn, String mt, String r, String b, Metered m, String tag) { + super(r, b); + metered = m; + metricType = mt; + metricName = mn; + this.contextTag = tag; + } + + @Override + public String getMetricName(String name) { + return getMetricBase() + "/" + name; + } + + @Override + public String getMetricBase() { + return prefix + registry + "/" + metricType + "/" + name; + } + + @Override + public int reportMetrics() { + long count = metered.getCount(); + String fullMetricName = getMetricName(metricName); + NewRelic.recordMetric(fullMetricName, count); + return 1; + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MetricUtil.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MetricUtil.java new file mode 100644 index 0000000000..352e38c259 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/MetricUtil.java @@ -0,0 +1,137 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.agent.instrumentation.solr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +public class MetricUtil { + + private static List desiredMetrics = new ArrayList<>(); + + private static List desiredPaths = new ArrayList<>(); + + private static ConcurrentHashMap metrics = new ConcurrentHashMap<>(); + + private static final String REGISTRY_PREFIX = "solr.core"; + + private static final HashMap remaps = new HashMap<>(); + + static { + desiredMetrics.add("filterCache"); + desiredMetrics.add("queryResultCache"); + desiredMetrics.add("documentCache"); + desiredPaths.add("updateHandler"); + remaps.put("cumulativeAdds", "cumulative_adds"); + remaps.put("cumulativeDeletesById", "cumulative_deletesById"); + remaps.put("cumulativeDeletesByQuery", "cumulative_deletesByQuery"); + remaps.put("cumulativeErrors", "cumulative_errors"); + } + + public static String getRemap(String key) { + if (remaps.containsKey(key)) { + return remaps.get(key); + } + return key; + } + + public static void addMetric(NRMetric metric) { + String metricBase = metric.getMetricBase(); + metrics.put(metricBase, metric); + } + + public static void removeMetric(String registry, String... metricPath) { + metrics.entrySet() + .stream() + .filter(entry -> entry.getValue().registry.equals(registry) && Arrays.stream(metricPath).anyMatch(path -> path.startsWith(entry.getValue().name))) + .forEach(x -> metrics.remove(x.getKey())); + } + + public static void swapRegistries(String sourceRegistry, String targetRegistry) { + metrics.entrySet() + .stream() + .filter(entry -> entry.getValue().registry.equals(getRegistry(sourceRegistry))) + .forEach(x -> { + String currentKey = x.getKey(); + NRMetric metric = x.getValue(); + metric.setRegistry(getRegistry(targetRegistry)); + addMetric(metric); + metrics.remove(currentKey); + }); + } + + public static void clearRegistry(String registry) { + metrics.entrySet() + .stream() + .filter(entry -> entry.getValue().registry.equals(registry)) + .forEach(x -> metrics.remove(x.getKey())); + } + + public static void clearAll() { + metrics.clear(); + } + + public static String getRegistry(String r) { + if (r.startsWith(REGISTRY_PREFIX)) { + return r.substring(REGISTRY_PREFIX.length() + 1); + } else { + return r; + } + } + + public static ConcurrentHashMap getMetrics() { + return metrics; + } + + public static String getDesired(String metricName, String[] metricPath) { + if (!isDesired(metricName, metricPath)) { + return null; + } + if (metricName != null && !metricName.isEmpty()) { + StringTokenizer st = new StringTokenizer(metricName, "."); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (desiredMetrics.contains(token)) { + return token; + } + } + } + if (metricPath != null) { + for (int i = 0; i < metricPath.length; i++) { + if (desiredPaths.contains(metricPath[i])) { + return metricPath[i]; + } + } + } + return null; + } + + public static boolean isDesired(String metricName, String[] metricPath) { + if (metricName != null && !metricName.isEmpty()) { + StringTokenizer st = new StringTokenizer(metricName, "."); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (desiredMetrics.contains(token)) { + return true; + } + } + } + if (metricPath != null) { + for (int i = 0; i < metricPath.length; i++) { + if (desiredPaths.contains(metricPath[i])) { + return true; + } + } + } + return false; + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/NRMetric.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/NRMetric.java new file mode 100644 index 0000000000..d0d9647d0c --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/NRMetric.java @@ -0,0 +1,55 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.solr; + +public abstract class NRMetric { + + protected static final String prefix = "JMX/solr/"; + + public NRMetric(String r, String b) { + registry = r; + name = b; + } + + protected String registry; + + protected String name; + + protected String contextTag; + + public String getRegistry() { + return registry; + } + + public void setRegistry(String registry) { + this.registry = registry; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getContextTag() { + return contextTag; + } + + public void setContextTag(String contextTag) { + this.contextTag = contextTag; + } + + public abstract String getMetricName(String name); + + public abstract int reportMetrics(); + + public abstract String getMetricBase(); + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrComponentRegistry.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrComponentRegistry.java new file mode 100644 index 0000000000..80a354bf05 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrComponentRegistry.java @@ -0,0 +1,107 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.agent.instrumentation.solr; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Our instrumentation point for Solr v9 no longer accepts a SolrInfoBean instance + * in the SolrMetricManager_Instrumentation.registerMetric method. Instead, it takes + * a SolrMetricsContext instance, which no longer exposes a getName method, which + * we use to generate metric names. This class is used to map getTag() values + * (from SolrMetricsContext) to corresponding SolrInfoBean.getName() values. + * This allows Solr 9.0 instrumentation to maintain metric name parity with Solr 8.x. + */ +public class SolrComponentRegistry { + + private static final ConcurrentHashMap tagToName = new ConcurrentHashMap<>(); + + /** + * Register a component's context tag to bean name mapping. + * If the name looks like a fully qualified class name, also updates any existing + * metrics that are currently using a fallback name and have a matching context tag. + * + * @param tag The context tag + * @param name The bean name from SolrInfoBean.getName() + */ + public static void registerComponent(String tag, String name) { + if (tag != null && name != null) { + String previousName = tagToName.put(tag, name); + + // If this looks like a fully qualified class name and it's a new/updated mapping, + // update existing metrics that might be using a fallback name + if (isFullyQualifiedClassName(name) && !name.equals(previousName)) { + updateMetricsWithBeanName(tag, name); + } + } + } + + /** + * Get the bean name for a given tag. + * + * @param tag The target tag + * @return The bean name, or null if not found + */ + public static String getNameForTag(String tag) { + return tagToName.get(tag); + } + + /** + * Remove a tag --> name mapping + * + * @param tag The tag to remove + */ + public static void removeComponent(String tag) { + if (tag != null) { + tagToName.remove(tag); + } + } + + /** + * Clear everything + */ + public static void clear() { + tagToName.clear(); + } + + /** + * Super simple method to check if a name looks like a fully qualified class name + * (contains a dot and starts with lowercase package). + * + * @param name The name to check + * @return true if it looks like a fully qualified class name + */ + private static boolean isFullyQualifiedClassName(String name) { + return name != null && name.length() > 0 && name.contains(".") && Character.isLowerCase(name.charAt(0)); + } + + /** + * Update existing metrics that are using a fallback name (e.g., "updateHandler") + * to use the proper fully qualified bean name. Only updates metrics with a matching context tag. + * + * @param tag The context tag to match + * @param beanName The fully qualified bean name to use + */ + private static void updateMetricsWithBeanName(String tag, String beanName) { + java.util.concurrent.ConcurrentHashMap metrics = MetricUtil.getMetrics(); + + for (NRMetric metric : metrics.values()) { + // Only update metrics that: + // Have a matching context tag + // Are using a simple fallback name (doesn't contain dots) + // Don't already have the target bean name + String currentName = metric.getName(); + String metricTag = metric.getContextTag(); + + if (tag != null && tag.equals(metricTag) && + currentName != null && !currentName.contains(".") && !beanName.equals(currentName)) { + metric.setName(beanName); + } + } + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrMetricCollector.java b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrMetricCollector.java new file mode 100644 index 0000000000..62be2c1fd2 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/com/agent/instrumentation/solr/SolrMetricCollector.java @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.solr; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SolrMetricCollector { + + private static final AtomicBoolean init = new AtomicBoolean(); + + public static void initialize() { + if (init.compareAndSet(false, true)) { + AgentBridge.instrumentation.registerCloseable(Weaver.getImplementationTitle(), + AgentBridge.privateApi.addSampler(getSolrJmxSampler(), 1, TimeUnit.MINUTES)); + } + } + + private static Runnable getSolrJmxSampler() { + return new Runnable() { + @Override + public void run() { + ConcurrentHashMap metrics = MetricUtil.getMetrics(); + Set keys = metrics.keySet(); + for (String key : keys) { + NRMetric metric = metrics.get(key); + if (metric != null) { + metric.reportMetrics(); + } + } + } + }; + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrCoreMetricManager_Instrumentation.java b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrCoreMetricManager_Instrumentation.java new file mode 100644 index 0000000000..d2a8c4b5e6 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrCoreMetricManager_Instrumentation.java @@ -0,0 +1,34 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.solr.metrics; + +import com.agent.instrumentation.solr.SolrComponentRegistry; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.apache.solr.core.SolrInfoBean; + +@Weave(originalName = "org.apache.solr.metrics.SolrCoreMetricManager", type = MatchType.ExactClass) +public abstract class SolrCoreMetricManager_Instrumentation { + + public void registerMetricProducer(String scope, SolrMetricProducer producer) { + // This creates the SolrMetricsContext instance + Weaver.callOriginal(); + + if (producer instanceof SolrInfoBean) { + SolrInfoBean infoBean = (SolrInfoBean) producer; + SolrMetricsContext context = producer.getSolrMetricsContext(); + + if (context != null) { + String tag = context.getTag(); + String name = infoBean.getName(); + SolrComponentRegistry.registerComponent(tag, name); + } + } + } +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricManager_Instrumentation.java b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricManager_Instrumentation.java new file mode 100644 index 0000000000..f2e94163a8 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricManager_Instrumentation.java @@ -0,0 +1,124 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.solr.metrics; + +import com.agent.instrumentation.solr.CacheMetric; +import com.agent.instrumentation.solr.GaugeMetric; +import com.agent.instrumentation.solr.MeteredMetric; +import com.agent.instrumentation.solr.MetricUtil; +import com.agent.instrumentation.solr.SolrComponentRegistry; +import com.agent.instrumentation.solr.SolrMetricCollector; +import com.codahale.metrics.Gauge; +import com.codahale.metrics.Meter; +import com.codahale.metrics.Metric; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import java.util.Set; +import java.util.logging.Level; + +@Weave(originalName = "org.apache.solr.metrics.SolrMetricManager", type = MatchType.ExactClass) +public abstract class SolrMetricManager_Instrumentation { + + public void registerMetric(SolrMetricsContext context, String registry, Metric metric, + SolrMetricManager.ResolutionStrategy strategy, String metricName, String... metricPath) { + if (MetricUtil.isDesired(metricName, metricPath)) { + SolrMetricCollector.initialize(); + + boolean isCacheMetric = metric instanceof MetricsMap; + boolean isGaugeMetric = metric instanceof Gauge; + + String desired = MetricUtil.getDesired(metricName, metricPath); + + String tag = context != null ? context.getTag() : null; + String beanName = getBeanName(context, desired); + + if (isCacheMetric) { + MetricsMap mMap = (MetricsMap) metric; + CacheMetric nrMetric = new CacheMetric(desired, MetricUtil.getRegistry(registry), mMap, beanName, tag); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Created CacheMetric of name {0}", metricName); + MetricUtil.addMetric(nrMetric); + } else if (isGaugeMetric) { + Gauge gauge = (Gauge) metric; + GaugeMetric gMetric = new GaugeMetric(metricName, desired, MetricUtil.getRegistry(registry), gauge, beanName, tag); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Created GaugeMetric of name {0}", metricName); + MetricUtil.addMetric(gMetric); + } + } + + Weaver.callOriginal(); + } + + public Meter meter(SolrMetricsContext context, String registry, String metricName, String[] metricPath) { + Meter meter = Weaver.callOriginal(); + if (MetricUtil.isDesired(metricName, metricPath)) { + String mName = MetricUtil.getRemap(metricName); + String desired = MetricUtil.getDesired(metricName, metricPath); + String tag = context != null ? context.getTag() : null; + String beanName = getBeanName(context, desired); + MeteredMetric meteredMetric = new MeteredMetric(mName, desired, MetricUtil.getRegistry(registry), beanName, meter, tag); + MetricUtil.addMetric(meteredMetric); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Added NRMetric from ({0}, {1}, {2})", context, registry, metricName); + } + return meter; + } + + public void removeRegistry(String registry) { + Weaver.callOriginal(); + MetricUtil.clearRegistry(registry); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Removed {0} metric registry", registry); + } + + public void clearRegistry(String registry) { + Weaver.callOriginal(); + MetricUtil.clearRegistry(registry); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Cleared {0} metric registry", registry); + } + + public Set clearMetrics(String registry, String... metricPath) { + Set removedMetrics = Weaver.callOriginal(); + if(removedMetrics != null) { + for (String removedMetric: removedMetrics) { + MetricUtil.removeMetric(registry, removedMetric); + } + NewRelic.getAgent().getLogger().log(Level.FINEST, "Cleared {0} metrics from {1} metric registry", removedMetrics.size(), registry); + } + return removedMetrics; + } + + public void swapRegistries(String registry1, String registry2) { + Weaver.callOriginal(); + MetricUtil.swapRegistries(registry1, registry2); + NewRelic.getAgent().getLogger().log(Level.FINEST, "Swapped {0} metric registry to {1} metric registry", registry1, registry2); + } + + /** + * Resolves a bean name from a SolrMetricsContext by looking up the context's tag + * in the SolrComponentRegistry. Falls back to the desired value if no mapping exists. + * + * @param context The SolrMetricsContext containing the tag to look up + * @param desired The fallback value to use if no mapping is found + * @return The resolved bean name, or the desired value if no mapping exists + */ + private String getBeanName(SolrMetricsContext context, String desired) { + String beanName = null; + if (context != null) { + String tag = context.getTag(); + beanName = SolrComponentRegistry.getNameForTag(tag); + } + + // Fallback if no mapping exists + if (beanName == null) { + beanName = desired; + } + + return beanName; + } +} diff --git a/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricReporter_Instrumentation.java b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricReporter_Instrumentation.java new file mode 100644 index 0000000000..c2828330e7 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/main/java/org/apache/solr/metrics/SolrMetricReporter_Instrumentation.java @@ -0,0 +1,40 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.solr.metrics; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.apache.solr.core.PluginInfo; +import org.apache.solr.metrics.reporters.SolrJmxReporter; + +import javax.management.MBeanServer; +import java.util.logging.Level; + +@Weave(originalName = "org.apache.solr.metrics.SolrMetricReporter", type = MatchType.BaseClass) +public abstract class SolrMetricReporter_Instrumentation { + + public void init(PluginInfo pluginInfo) { + Weaver.callOriginal(); + Logger logger = NewRelic.getAgent().getLogger(); + if (SolrJmxReporter.class.isInstance(this)) { + Object thisTemp = this; + SolrJmxReporter solrJMX = (SolrJmxReporter) thisTemp; + MBeanServer mBeanServer = solrJMX.getMBeanServer(); + logger.log(Level.FINEST, "SolrJmxReporter mBeanServer: {0}", mBeanServer); + if (mBeanServer != null) { + AgentBridge.privateApi.addMBeanServer(mBeanServer); + logger.log(Level.FINEST, "added mBeanServer: {0}", mBeanServer); + } + } + } + +} diff --git a/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrComponentRegistryTest.java b/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrComponentRegistryTest.java new file mode 100644 index 0000000000..0ba6c62f9e --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrComponentRegistryTest.java @@ -0,0 +1,190 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.agent.instrumentation.solr; + +import com.codahale.metrics.Meter; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SolrComponentRegistryTest { + + @Before + public void setUp() { + SolrComponentRegistry.clear(); + MetricUtil.clearAll(); + } + + @Test + public void testRegisterAndGetNameForTag() { + String tag = "SolrCore@123:DirectUpdateHandler2@456"; + String name = "org.apache.solr.update.DirectUpdateHandler2"; + SolrComponentRegistry.registerComponent(tag, name); + + assertEquals(name, SolrComponentRegistry.getNameForTag(tag)); + } + + @Test + public void testGetNameForTagReturnsNullForUnknownTag() { + String unknownTag = "unknown-tag"; + String result = SolrComponentRegistry.getNameForTag(unknownTag); + + assertNull(result); + } + + @Test + public void testFullyQualifiedClassNameUpdatesMatchingMetrics() { + String tag = "SolrCore@123:DirectUpdateHandler2@456"; + String fallbackName = "updateHandler"; + String qualifiedName = "org.apache.solr.update.DirectUpdateHandler2"; + + Meter meter = new Meter(); + MeteredMetric metric = new MeteredMetric("commits", "updateHandler", "testRegistry", fallbackName, meter, tag); + MetricUtil.addMetric(metric); + + assertEquals(fallbackName, metric.getName()); + + SolrComponentRegistry.registerComponent(tag, qualifiedName); + + assertEquals(qualifiedName, metric.getName()); + } + + @Test + public void testOnlyMetricsWithMatchingTagAreUpdated() { + String tag1 = "SolrCore@123:DirectUpdateHandler2@456"; + String tag2 = "SolrCore@789:UpdateRequestHandler@012"; + String qualifiedName = "org.apache.solr.update.DirectUpdateHandler2"; + + Meter meter1 = new Meter(); + Meter meter2 = new Meter(); + MeteredMetric metric1 = new MeteredMetric("commits", "updateHandler", "testRegistry1", "updateHandler", meter1, tag1); + MeteredMetric metric2 = new MeteredMetric("requests", "updateHandler", "testRegistry2", "updateHandler", meter2, tag2); + MetricUtil.addMetric(metric1); + MetricUtil.addMetric(metric2); + + SolrComponentRegistry.registerComponent(tag1, qualifiedName); + + assertEquals(qualifiedName, metric1.getName()); + assertEquals("updateHandler", metric2.getName()); // Should remain unchanged + } + + @Test + public void testMetricsWithDotsAreNotUpdated() { + String tag = "SolrCore@123:UpdateRequestHandler@456"; + String existingQualifiedName = "org.apache.solr.update.DirectUpdateHandler2"; + String newQualifiedName = "org.apache.solr.handler.UpdateRequestHandler"; + + Meter meter = new Meter(); + MeteredMetric metric = new MeteredMetric("commits", "updateHandler", "testRegistry", existingQualifiedName, meter, tag); + MetricUtil.addMetric(metric); + + SolrComponentRegistry.registerComponent(tag, newQualifiedName); + + assertEquals(existingQualifiedName, metric.getName()); + } + + @Test + public void testSimpleNamesDoNotTriggerUpdates() { + String tag = "SolrCore@123:SpellCheckComponent@456"; + String simpleName = "spellcheck"; // Not a fully qualified class name + + Meter meter = new Meter(); + MeteredMetric metric = new MeteredMetric("requests", "handler", "testRegistry", "handler", meter, tag); + MetricUtil.addMetric(metric); + + SolrComponentRegistry.registerComponent(tag, simpleName); + + assertEquals("handler", metric.getName()); + } + + @Test + public void testCacheMetricsAreNotAffectedByHandlerRegistration() { + String handlerTag = "SolrCore@123:UpdateRequestHandler@456"; + String cacheTag = "SolrCore@123:SolrIndexSearcher@789:CaffeineCache@012"; + String handlerName = "org.apache.solr.handler.UpdateRequestHandler"; + + Meter meter = new Meter(); + MeteredMetric cacheMetric = new MeteredMetric("hits", "filterCache", "testRegistry", "filterCache", meter, cacheTag); + MetricUtil.addMetric(cacheMetric); + + SolrComponentRegistry.registerComponent(handlerTag, handlerName); + + assertEquals("filterCache", cacheMetric.getName()); + } + + @Test + public void testMultipleMetricsWithSameTagAllGetUpdated() { + String tag = "SolrCore@123:DirectUpdateHandler2@456"; + String qualifiedName = "org.apache.solr.update.DirectUpdateHandler2"; + + Meter meter1 = new Meter(); + Meter meter2 = new Meter(); + Meter meter3 = new Meter(); + MeteredMetric metric1 = new MeteredMetric("commits", "updateHandler", "testRegistry1", "updateHandler", meter1, tag); + MeteredMetric metric2 = new MeteredMetric("rollbacks", "updateHandler", "testRegistry2", "updateHandler", meter2, tag); + MeteredMetric metric3 = new MeteredMetric("optimizes", "updateHandler", "testRegistry3", "updateHandler", meter3, tag); + MetricUtil.addMetric(metric1); + MetricUtil.addMetric(metric2); + MetricUtil.addMetric(metric3); + + SolrComponentRegistry.registerComponent(tag, qualifiedName); + + assertEquals(qualifiedName, metric1.getName()); + assertEquals(qualifiedName, metric2.getName()); + assertEquals(qualifiedName, metric3.getName()); + } + + @Test + public void testMetricsWithNullTagAreNotUpdated() { + String tag = "SolrCore@123:DirectUpdateHandler2@456"; + String qualifiedName = "org.apache.solr.update.DirectUpdateHandler2"; + + Meter meter = new Meter(); + MeteredMetric metric = new MeteredMetric("commits", "updateHandler", "testRegistry", "updateHandler", meter, null); + MetricUtil.addMetric(metric); + + SolrComponentRegistry.registerComponent(tag, qualifiedName); + + assertEquals("updateHandler", metric.getName()); + } + + @Test + public void testClearRemovesAllMappings() { + String tag1 = "tag1"; + String tag2 = "tag2"; + SolrComponentRegistry.registerComponent(tag1, "org.example.Class1"); + SolrComponentRegistry.registerComponent(tag2, "org.example.Class2"); + + SolrComponentRegistry.clear(); + + assertNull(SolrComponentRegistry.getNameForTag(tag1)); + assertNull(SolrComponentRegistry.getNameForTag(tag2)); + } + + @Test + public void testRegisterComponentWithNullValues() { + // No exceptions thrown... + SolrComponentRegistry.registerComponent(null, "someName"); + SolrComponentRegistry.registerComponent("someTag", null); + SolrComponentRegistry.registerComponent(null, null); + } + + @Test + public void testUpdatingExistingComponentMapping() { + String tag = "SolrCore@123:Handler@456"; + String oldName = "org.example.OldHandler"; + String newName = "org.example.NewHandler"; + + SolrComponentRegistry.registerComponent(tag, oldName); + assertEquals(oldName, SolrComponentRegistry.getNameForTag(tag)); + + SolrComponentRegistry.registerComponent(tag, newName); + + assertEquals(newName, SolrComponentRegistry.getNameForTag(tag)); + } +} \ No newline at end of file diff --git a/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrMetricManagerInstrumentationTests.java b/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrMetricManagerInstrumentationTests.java new file mode 100644 index 0000000000..10919e6b18 --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/test/java/com/agent/instrumentation/solr/SolrMetricManagerInstrumentationTests.java @@ -0,0 +1,219 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ +package com.agent.instrumentation.solr; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.NoOpPrivateApi; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; + +import org.apache.solr.core.SolrInfoBean; +import org.apache.solr.metrics.MetricsMap; +import org.apache.solr.metrics.SolrMetricManager; +import org.apache.solr.metrics.SolrMetricsContext; +import org.apache.solr.store.blockcache.Metrics; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "org.apache.solr.metrics" }) +public class SolrMetricManagerInstrumentationTests { + + private SolrMetricManager solrMetricManager; + private SolrMetricsContext solrMetricsContext; + + @Before + public void before() { + MetricUtil.clearRegistry("exampleRegistry"); + MetricUtil.clearRegistry("targetRegistry"); + + // Initialize SolrMetricManager and create a metrics context for testing + solrMetricManager = new SolrMetricManager(); + solrMetricsContext = new SolrMetricsContext(solrMetricManager, "exampleRegistry", "test-tag"); + + // Initialize the SolrComponentRegistry for the new instrumentation approach + SolrComponentRegistry.clear(); + } + + @Test + public void register() { + //Given + AgentBridge.privateApi = new NoOpPrivateApi(); + SolrInfoBean solrInfoBean = new Metrics(); + + // Register the component to populate the tag->name mapping + // This simulates what SolrCoreMetricManager_Instrumentation would do + solrInfoBean.initializeMetrics(solrMetricsContext, "hdfsBlockCache"); + SolrMetricsContext childContext = solrInfoBean.getSolrMetricsContext(); + SolrComponentRegistry.registerComponent(childContext.getTag(), solrInfoBean.getName()); + + MetricsMap metricsMap = new MetricsMap((detailed, map) -> map.put("exampleMetric", 100)); + + //When + solrMetricManager.registerMetric( + childContext, // SolrMetricsContext (not SolrInfoBean) + "exampleRegistry", // registry name + metricsMap, // metric + SolrMetricManager.ResolutionStrategy.REPLACE, // strategy (not boolean) + "filterCache", // metricName + "hdfsBlockCache" // metricPath + ); + + //Then + int metricsReported = MetricUtil.getMetrics().values().stream() + .map(NRMetric::reportMetrics) + .mapToInt(Integer::intValue) + .sum(); + assertEquals(1, metricsReported); + + String registryName = MetricUtil.getMetrics().values().stream() + .map(x -> x.registry) + .collect(Collectors.joining()); + assertEquals("exampleRegistry", registryName); + } + + @Test + public void removeRegistry() { + //Given + AgentBridge.privateApi = new NoOpPrivateApi(); + SolrInfoBean solrInfoBean = new Metrics(); + + solrInfoBean.initializeMetrics(solrMetricsContext, "hdfsBlockCache"); + SolrMetricsContext childContext = solrInfoBean.getSolrMetricsContext(); + SolrComponentRegistry.registerComponent(childContext.getTag(), solrInfoBean.getName()); + + MetricsMap metricsMap = new MetricsMap((detailed, map) -> map.put("exampleMetric", 100)); + + //When + solrMetricManager.registerMetric( + childContext, + "exampleRegistry", + metricsMap, + SolrMetricManager.ResolutionStrategy.REPLACE, + "filterCache", + "hdfsBlockCache" + ); + solrMetricManager.removeRegistry("exampleRegistry"); + + //Then + int metricsReported = MetricUtil.getMetrics().values().stream() + .map(NRMetric::reportMetrics) + .mapToInt(Integer::intValue) + .sum(); + assertEquals(0, metricsReported); + } + + @Test + public void clearRegistry() { + //Given + AgentBridge.privateApi = new NoOpPrivateApi(); + SolrInfoBean solrInfoBean = new Metrics(); + + solrInfoBean.initializeMetrics(solrMetricsContext, "hdfsBlockCache"); + SolrMetricsContext childContext = solrInfoBean.getSolrMetricsContext(); + SolrComponentRegistry.registerComponent(childContext.getTag(), solrInfoBean.getName()); + + MetricsMap metricsMap = new MetricsMap((detailed, map) -> map.put("exampleMetric", 100)); + + //When + solrMetricManager.registerMetric( + childContext, + "exampleRegistry", + metricsMap, + SolrMetricManager.ResolutionStrategy.REPLACE, + "filterCache", + "hdfsBlockCache" + ); + solrMetricManager.clearRegistry("exampleRegistry"); + + //Then + int metricsReported = MetricUtil.getMetrics().values().stream() + .map(NRMetric::reportMetrics) + .mapToInt(Integer::intValue) + .sum(); + assertEquals(0, metricsReported); + } + + @Test + public void clearMetrics() { + //Given + AgentBridge.privateApi = new NoOpPrivateApi(); + SolrInfoBean solrInfoBean = new Metrics(); + + solrInfoBean.initializeMetrics(solrMetricsContext, "hdfsBlockCache"); + SolrMetricsContext childContext = solrInfoBean.getSolrMetricsContext(); + SolrComponentRegistry.registerComponent(childContext.getTag(), solrInfoBean.getName()); + + MetricsMap metricsMap = new MetricsMap((detailed, map) -> map.put("exampleMetric", 100)); + + //When + solrMetricManager.registerMetric( + childContext, + "exampleRegistry", + metricsMap, + SolrMetricManager.ResolutionStrategy.REPLACE, + "filterCache", + "hdfsBlockCache" + ); + solrMetricManager.clearMetrics("exampleRegistry", "hdfsBlockCache"); + + //Then + int metricsReported = MetricUtil.getMetrics().values().stream() + .map(NRMetric::reportMetrics) + .mapToInt(Integer::intValue) + .sum(); + assertEquals(0, metricsReported); + } + + @Test + public void swapRegistries() { + //Given + AgentBridge.privateApi = new NoOpPrivateApi(); + SolrInfoBean solrInfoBean = new Metrics(); + + solrInfoBean.initializeMetrics(solrMetricsContext, "hdfsBlockCache"); + SolrMetricsContext childContext = solrInfoBean.getSolrMetricsContext(); + SolrComponentRegistry.registerComponent(childContext.getTag(), solrInfoBean.getName()); + + MetricsMap metricsMap = new MetricsMap((detailed, map) -> map.put("exampleMetric", 100)); + + //When + solrMetricManager.registerMetric( + childContext, + "exampleRegistry", + metricsMap, + SolrMetricManager.ResolutionStrategy.REPLACE, + "filterCache", + "hdfsBlockCache" + ); + solrMetricManager.swapRegistries("exampleRegistry", "targetRegistry"); + + //Then + int metricsReported = MetricUtil.getMetrics().values().stream() + .map(NRMetric::reportMetrics) + .mapToInt(Integer::intValue) + .sum(); + assertEquals(1, metricsReported); + + String registryName = MetricUtil.getMetrics().values().stream() + .map(x -> x.registry) + .collect(Collectors.joining()); + assertEquals("targetRegistry", registryName); + + String metricBase = MetricUtil.getMetrics().values().stream() + .map(NRMetric::getMetricBase) + .collect(Collectors.joining()); + assertEquals("JMX/solr/targetRegistry/filterCache/hdfsBlockCache", metricBase); + } +} diff --git a/instrumentation/solr-jmx-9.0.0/src/test/java/org/apache/solr/store/blockcache/Metrics.java b/instrumentation/solr-jmx-9.0.0/src/test/java/org/apache/solr/store/blockcache/Metrics.java new file mode 100644 index 0000000000..8b4fe9f50d --- /dev/null +++ b/instrumentation/solr-jmx-9.0.0/src/test/java/org/apache/solr/store/blockcache/Metrics.java @@ -0,0 +1,59 @@ +/* + * Replacement for org.apache.solr.store.blockcache.Metrics for Solr 9.0+ + * + * The original class was removed when HDFS support was moved to a separate module (SOLR-14660). + * This is a minimal test implementation that provides the same interface for testing purposes. + * + * Original class was in: org.apache.solr.store.blockcache.Metrics + * Extended: SolrCacheBase + * Implemented: SolrInfoBean, SolrMetricProducer + */ +package org.apache.solr.store.blockcache; + +import org.apache.solr.core.SolrInfoBean; +import org.apache.solr.metrics.SolrMetricsContext; + +/** + * A test implementation of SolrInfoBean for testing Solr 9.0+ metrics. + */ +public class Metrics implements SolrInfoBean { + private static final String NAME = "hdfsBlockCache"; + private static final String DESCRIPTION = "Provides metrics for the HdfsDirectoryFactory BlockCache."; + private SolrMetricsContext metricsContext; + + public Metrics() { + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDescription() { + return DESCRIPTION; + } + + @Override + public Category getCategory() { + return Category.CACHE; + } + + @Override + public void initializeMetrics(SolrMetricsContext parentContext, String scope) { + this.metricsContext = parentContext.getChildContext(this); + } + + @Override + public SolrMetricsContext getSolrMetricsContext() { + return metricsContext; + } + + @Override + public String toString() { + return "Metrics{" + + "name='" + NAME + '\'' + + ", category=" + Category.CACHE + + '}'; + } +} diff --git a/settings.gradle b/settings.gradle index f8a10508f9..7844d947a7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -385,6 +385,7 @@ include 'instrumentation:solr-6.4.0' include 'instrumentation:solr-7.0.0' include 'instrumentation:solr-jmx-7.0.0' include 'instrumentation:solr-jmx-7.4.0' +include 'instrumentation:solr-jmx-9.0.0' include 'instrumentation:spray-can-1.3.1' include 'instrumentation:spray-client-1.3.1' include 'instrumentation:spray-can-http-client-1.3.1'