diff --git a/jmx-metrics/README.md b/jmx-metrics/README.md index 65e277ee..f8be9b5c 100644 --- a/jmx-metrics/README.md +++ b/jmx-metrics/README.md @@ -67,13 +67,14 @@ capable of being specified via the `otel.jmx.target.system` property as a comma- mutually exclusive with `otel.jmx.groovy.script`. The currently supported target systems are: | `otel.jmx.target.system` | -| ------------------------ | +|--------------------------| | [`jvm`](./docs/target-systems/jvm.md) | -| [`activemq`](./docs/target-systems/activemq.md)| +| [`activemq`](./docs/target-systems/activemq.md) | | [`cassandra`](./docs/target-systems/cassandra.md) | | [`kafka`](./docs/target-systems/kafka.md) | | [`kafka-consumer`](./docs/target-systems/kafka-consumer.md) | | [`kafka-producer`](./docs/target-systems/kafka-producer.md) | +| [`solr`](./docs/target-systems/solr.md) | | [`tomcat`](./docs/target-systems/tomcat.md) | ### JMX Query Helpers diff --git a/jmx-metrics/docs/target-systems/solr.md b/jmx-metrics/docs/target-systems/solr.md new file mode 100644 index 00000000..d059dfff --- /dev/null +++ b/jmx-metrics/docs/target-systems/solr.md @@ -0,0 +1,84 @@ +# Solr Metrics + +The JMX Metric Gatherer provides built in Solr metric gathering capabilities. +Details about using JMX with Solr can be found here: https://solr.apache.org/guide/6_6/using-jmx-with-solr.html + +## Metrics + +### Core Metrics + +* Name: `solr.document.count` +* Description: The total number of indexed documents. +* Unit: `{documents}` +* Labels: `core` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `solr.index.size` +* Description: The total index size. +* Unit: `by` +* Labels: `core` +* Instrument Type: ObservableLongUpDownCounter + + +* Name: `solr.request.count` +* Description: The number of queries made. +* Unit: `{queries}` +* Labels: `core`, `type`, `handler` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.request.time.average` +* Description: The average time of a query, based on Solr's histogram configuration. +* Unit: `ms` +* Labels: `core`, `type`, `handler` +* Instrument Type: ObservableDoubleValue + + +* Name: `solr.request.error.count` +* Description: The number of queries resulting in an error. +* Unit: `{queries}` +* Labels: `core`, `type`, `handler` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.request.timeout.count` +* Description: The number of queries resulting in a timeout. +* Unit: `{queries}` +* Labels: `core`, `type`, `handler` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.cache.eviction.count` +* Description: The number of evictions from a cache. +* Unit: `{evictions}` +* Labels: `core`, `cache` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.cache.hit.count` +* Description: The number of hits from a cache. +* Unit: `{hits}` +* Labels: `core`, `cache` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.cache.insert.count` +* Description: The number of inserts from a cache. +* Unit: `{inserts}` +* Labels: `core`, `cache` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.cache.lookup.count` +* Description: The number of lookups from a cache. +* Unit: `{lookups}` +* Labels: `core`, `cache` +* Instrument Type: ObservableLongCounter + + +* Name: `solr.cache.size` +* Description: The size of the cache occupied in memory. +* Unit: `by` +* Labels: `core`, `cache` +* Instrument Type: ObservableLongUpDownCounter diff --git a/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/SolrIntegrationTest.java b/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/SolrIntegrationTest.java new file mode 100644 index 00000000..57dba7a5 --- /dev/null +++ b/jmx-metrics/src/integrationTest/java/io/opentelemetry/contrib/jmxmetrics/target_systems/SolrIntegrationTest.java @@ -0,0 +1,252 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxmetrics.target_systems; + +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.contrib.jmxmetrics.AbstractIntegrationTest; +import io.opentelemetry.proto.metrics.v1.Metric; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; + +class SolrIntegrationTest extends AbstractIntegrationTest { + + SolrIntegrationTest() { + super(/* configFromStdin= */ false, "target-systems/solr.properties"); + } + + @Container + GenericContainer solr = + new GenericContainer<>("solr:8.8.2") + .withNetwork(Network.SHARED) + .withEnv("LOCAL_JMX", "no") + .withEnv("ENABLE_REMOTE_JMX_OPTS", "true") + .withEnv("RMI_PORT", "9990") + .withCommand("solr-precreate", "gettingstarted") + .withNetworkAliases("solr") + .withExposedPorts(9990) + .withStartupTimeout(Duration.ofMinutes(2)) + .waitingFor(Wait.forListeningPort()); + + @Test + void endToEnd() { + waitAndAssertMetrics( + metric -> + assertSumWithAttributes( + metric, + "solr.document.count", + "The total number of indexed documents.", + "{documents}", + attrs -> attrs.containsOnly(entry("core", "gettingstarted"))), + metric -> + assertSumWithAttributes( + metric, + "solr.index.size", + "The total index size.", + "by", + attrs -> attrs.containsOnly(entry("core", "gettingstarted"))), + metric -> + assertSolrRequestSumMetric( + metric, "solr.request.count", "The number of queries made.", "{queries}"), + metric -> + assertSolrRequestGaugeMetric( + metric, + "solr.request.time.average", + "The average time of a query, based on Solr's histogram configuration.", + "ms"), + metric -> + assertSolrRequestSumMetric( + metric, + "solr.request.error.count", + "The number of queries resulting in an error.", + "{queries}"), + metric -> + assertSolrRequestSumMetric( + metric, + "solr.request.timeout.count", + "The number of queries resulting in a timeout.", + "{queries}"), + metric -> + assertSolrCacheSumMetric( + metric, + "solr.cache.eviction.count", + "The number of evictions from a cache.", + "{evictions}"), + metric -> + assertSolrCacheSumMetric( + metric, "solr.cache.hit.count", "The number of hits for a cache.", "{hits}"), + metric -> + assertSolrCacheSumMetric( + metric, + "solr.cache.insert.count", + "The number of inserts to a cache.", + "{inserts}"), + metric -> + assertSolrCacheSumMetric( + metric, + "solr.cache.lookup.count", + "The number of lookups to a cache.", + "{lookups}"), + metric -> + assertSolrCacheSumMetric( + metric, "solr.cache.size", "The size of the cache occupied in memory.", "by")); + } + + private void assertSolrRequestSumMetric( + Metric metric, String name, String description, String unit) { + assertSumWithAttributes( + metric, + name, + description, + unit, + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), entry("handler", "/get"), entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/csv"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/query"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/graph"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "update"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/debug/dump"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/json"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/stream"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/export"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/json/docs"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), entry("handler", "/sql"), entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/select"), + entry("type", "QUERY"))); + } + + private void assertSolrRequestGaugeMetric( + Metric metric, String name, String description, String unit) { + assertGaugeWithAttributes( + metric, + name, + description, + unit, + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), entry("handler", "/get"), entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/csv"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/query"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/graph"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "update"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/debug/dump"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/json"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/stream"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/export"), + entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/update/json/docs"), + entry("type", "UPDATE")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), entry("handler", "/sql"), entry("type", "QUERY")), + attrs -> + attrs.containsExactly( + entry("core", "gettingstarted"), + entry("handler", "/select"), + entry("type", "QUERY"))); + } + + private void assertSolrCacheSumMetric( + Metric metric, String name, String description, String unit) { + assertSumWithAttributes( + metric, + name, + description, + unit, + attrs -> + attrs.containsExactly(entry("core", "gettingstarted"), entry("cache", "searcher"))); + } +} diff --git a/jmx-metrics/src/integrationTest/resources/target-systems/solr.properties b/jmx-metrics/src/integrationTest/resources/target-systems/solr.properties new file mode 100644 index 00000000..9135ae3c --- /dev/null +++ b/jmx-metrics/src/integrationTest/resources/target-systems/solr.properties @@ -0,0 +1,7 @@ +otel.jmx.interval.milliseconds = 3000 +otel.metrics.exporter = otlp +otel.jmx.service.url = service:jmx:rmi:///jndi/rmi://solr:9990/jmxrmi +otel.jmx.target.system = solr + +# these will be overridden by cmd line +otel.exporter.otlp.endpoint = http://host.testcontainers.internal diff --git a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java index be3a7b50..b14cf3a4 100644 --- a/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java +++ b/jmx-metrics/src/main/groovy/io/opentelemetry/contrib/jmxmetrics/JmxConfig.java @@ -34,7 +34,14 @@ class JmxConfig { static final List AVAILABLE_TARGET_SYSTEMS = Arrays.asList( - "activemq", "cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); + "activemq", + "cassandra", + "jvm", + "kafka", + "kafka-consumer", + "kafka-producer", + "solr", + "tomcat"); final String serviceUrl; final String groovyScript; diff --git a/jmx-metrics/src/main/resources/target-systems/solr.groovy b/jmx-metrics/src/main/resources/target-systems/solr.groovy new file mode 100644 index 00000000..a0b15f68 --- /dev/null +++ b/jmx-metrics/src/main/resources/target-systems/solr.groovy @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def beanSolrCoreSearcherNumDocs = otel.mbean("solr:dom1=core,dom2=*,category=SEARCHER,scope=searcher,name=numDocs") +otel.instrument(beanSolrCoreSearcherNumDocs, "solr.document.count", "The total number of indexed documents.", "{documents}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }], + "Value", otel.&longUpDownCounterCallback) + +def beanSolrCoreIndexSize = otel.mbean("solr:dom1=core,dom2=*,category=INDEX,name=sizeInBytes") +otel.instrument(beanSolrCoreIndexSize, "solr.index.size", "The total index size.", "by", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }], + "Value", otel.&longUpDownCounterCallback) + +def beanSolrCoreRequests = otel.mbeans(["solr:dom1=core,dom2=*,category=QUERY,scope=*,name=requests", + "solr:dom1=core,dom2=*,category=UPDATE,scope=*,name=requests"]) +otel.instrument(beanSolrCoreRequests, "solr.request.count", "The number of queries made.", "{queries}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "type" : { mbean -> mbean.name().getKeyProperty("category") }, + "handler" : { mbean -> mbean.name().getKeyProperty("scope") }], + "Count", otel.&longCounterCallback) + +def beanSolrCoreRequestTimes = otel.mbeans(["solr:dom1=core,dom2=*,category=QUERY,scope=*,name=requestTimes", + "solr:dom1=core,dom2=*,category=UPDATE,scope=*,name=requestTimes"]) +otel.instrument(beanSolrCoreRequestTimes, "solr.request.time.average", + "The average time of a query, based on Solr's histogram configuration.", + "ms", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "type" : { mbean -> mbean.name().getKeyProperty("category") }, + "handler" : { mbean -> mbean.name().getKeyProperty("scope") }], + "Mean", otel.&doubleValueCallback) + +def beanSolrCoreErrors = otel.mbeans(["solr:dom1=core,dom2=*,category=QUERY,scope=*,name=errors", + "solr:dom1=core,dom2=*,category=UPDATE,scope=*,name=errors"]) +otel.instrument(beanSolrCoreErrors, "solr.request.error.count", "The number of queries resulting in an error.", "{queries}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "type" : { mbean -> mbean.name().getKeyProperty("category") }, + "handler" : { mbean -> mbean.name().getKeyProperty("scope") }], + "Count", otel.&longCounterCallback) + +def beanSolrCoreTimeouts = otel.mbeans(["solr:dom1=core,dom2=*,category=QUERY,scope=*,name=timeouts", + "solr:dom1=core,dom2=*,category=UPDATE,scope=*,name=timeouts"]) +otel.instrument(beanSolrCoreTimeouts, "solr.request.timeout.count", "The number of queries resulting in a timeout.", "{queries}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "type" : { mbean -> mbean.name().getKeyProperty("category") }, + "handler" : { mbean -> mbean.name().getKeyProperty("scope") }], + "Count", otel.&longCounterCallback) + +def beanSolrCoreQueryResultsCache = otel.mbean("solr:dom1=core,dom2=*,category=CACHE,scope=*,name=queryResultCache") +otel.instrument(beanSolrCoreQueryResultsCache, "solr.cache.eviction.count", "The number of evictions from a cache.", "{evictions}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "cache" : { mbean -> mbean.name().getKeyProperty("scope") }], + "cumulative_evictions", otel.&longCounterCallback) +otel.instrument(beanSolrCoreQueryResultsCache, "solr.cache.hit.count", "The number of hits for a cache.", "{hits}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "cache" : { mbean -> mbean.name().getKeyProperty("scope") }], + "cumulative_hits", otel.&longCounterCallback) +otel.instrument(beanSolrCoreQueryResultsCache, "solr.cache.insert.count", "The number of inserts to a cache.", "{inserts}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "cache" : { mbean -> mbean.name().getKeyProperty("scope") }], + "cumulative_inserts", otel.&longCounterCallback) +otel.instrument(beanSolrCoreQueryResultsCache, "solr.cache.lookup.count", "The number of lookups to a cache.", "{lookups}", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "cache" : { mbean -> mbean.name().getKeyProperty("scope") }], + "cumulative_lookups", otel.&longCounterCallback) +otel.instrument(beanSolrCoreQueryResultsCache, "solr.cache.size", "The size of the cache occupied in memory.", "by", + ["core" : { mbean -> mbean.name().getKeyProperty("dom2") }, + "cache" : { mbean -> mbean.name().getKeyProperty("scope") }], + "size", otel.&longUpDownCounterCallback) diff --git a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java index ccab6853..24e3a41b 100644 --- a/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java +++ b/jmx-metrics/src/test/java/io/opentelemetry/contrib/jmxmetrics/JmxConfigTest.java @@ -18,7 +18,14 @@ class JmxConfigTest { void staticValues() { assertThat(JmxConfig.AVAILABLE_TARGET_SYSTEMS) .containsOnly( - "activemq", "cassandra", "jvm", "kafka", "kafka-consumer", "kafka-producer", "tomcat"); + "activemq", + "cassandra", + "jvm", + "kafka", + "kafka-consumer", + "kafka-producer", + "solr", + "tomcat"); } @Test @@ -115,8 +122,8 @@ class JmxConfigTest { assertThatThrownBy(config::validate) .isInstanceOf(ConfigurationException.class) .hasMessage( - "[jvm, unavailabletargetsystem] must specify targets from " - + "[activemq, cassandra, jvm, kafka, kafka-consumer, kafka-producer, tomcat]"); + "[jvm, unavailabletargetsystem] must specify targets from [activemq, cassandra, jvm, " + + "kafka, kafka-consumer, kafka-producer, solr, tomcat]"); } @Test