Skip to content

Support for extra metrics #1124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions collector/src/main/java/io/prometheus/jmx/JmxCollector.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public enum Mode {

private final Mode mode;

static class ExtraMetric {
String name;
Object value;
String description;
}

static class Rule {
Pattern pattern;
String name;
Expand All @@ -80,6 +86,7 @@ static class Rule {
public static class MetricCustomizer {
MBeanFilter mbeanFilter;
List<String> attributesAsLabels;
List<ExtraMetric> extraMetrics;
}

public static class MBeanFilter {
Expand Down Expand Up @@ -355,15 +362,37 @@ private Config loadConfig(Map<String, Object> yamlConfig) throws MalformedObject
}
mbeanFilter.properties = (Map<String, String>) mbeanFilterYaml.getOrDefault("properties", new HashMap<>());

List<String> attributesAsLabels =
List<String> attributesAsLabelsYaml =
(List<String>) metricCustomizerYaml.get("attributesAsLabels");
if (attributesAsLabels == null) {
List<Map<String, Object>> extraMetricsYaml =
(List<Map<String, Object>>) metricCustomizerYaml.get("extraMetrics");
if (attributesAsLabelsYaml == null && extraMetricsYaml == null) {
throw new IllegalArgumentException(
"Must provide attributesAsLabels, if metricCustomizers is given: " + metricCustomizersYaml);
"Must provide attributesAsLabels or extraMetrics, if metricCustomizers is given: " + metricCustomizersYaml);
}
MetricCustomizer metricCustomizer = new MetricCustomizer();
metricCustomizer.mbeanFilter = mbeanFilter;
metricCustomizer.attributesAsLabels = attributesAsLabels;
metricCustomizer.attributesAsLabels = attributesAsLabelsYaml;

if (extraMetricsYaml != null) {
List<ExtraMetric> extraMetrics = new ArrayList<>();
for (Map<String, Object> extraMetricYaml : extraMetricsYaml) {
ExtraMetric extraMetric = new ExtraMetric();
extraMetric.name = (String) extraMetricYaml.get("name");
if (extraMetric.name == null) {
throw new IllegalArgumentException(
"Must provide name, if extraMetric is given: " + extraMetricsYaml);
}
extraMetric.value = extraMetricYaml.get("value");
if (extraMetric.value == null) {
throw new IllegalArgumentException(
"Must provide value, if extraMetric is given: " + extraMetricsYaml);
}
extraMetric.description = (String) extraMetricYaml.get("description");
extraMetrics.add(extraMetric);
}
metricCustomizer.extraMetrics = extraMetrics;
}
cfg.metricCustomizers.add(metricCustomizer);
}
} else {
Expand Down
25 changes: 23 additions & 2 deletions collector/src/main/java/io/prometheus/jmx/JmxScraper.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,22 @@ private void scrapeBean(MBeanServerConnection beanConn, ObjectName mBeanName) {
JmxCollector.MetricCustomizer metricCustomizer = getMetricCustomizer(mBeanName);
Map<String, String> attributesAsLabelsWithValues = Collections.emptyMap();
if (metricCustomizer != null) {
attributesAsLabelsWithValues =
getAttributesAsLabelsWithValues(metricCustomizer, attributes);
if (metricCustomizer.attributesAsLabels != null) {
attributesAsLabelsWithValues =
getAttributesAsLabelsWithValues(metricCustomizer, attributes);
}
for (JmxCollector.ExtraMetric extraMetric : getExtraMetrics(metricCustomizer)) {
processBeanValue(
mBeanName,
mBeanDomain,
jmxMBeanPropertyCache.getKeyPropertyList(mBeanName),
attributesAsLabelsWithValues,
new LinkedList<>(),
extraMetric.name,
"UNKNOWN",
extraMetric.description,
extraMetric.value);
}
}

for (Object object : attributes) {
Expand Down Expand Up @@ -313,6 +327,13 @@ private void scrapeBean(MBeanServerConnection beanConn, ObjectName mBeanName) {
}
}

private List<JmxCollector.ExtraMetric> getExtraMetrics(
JmxCollector.MetricCustomizer metricCustomizer) {
return metricCustomizer.extraMetrics != null
? metricCustomizer.extraMetrics
: Collections.emptyList();
}

private Map<String, String> getAttributesAsLabelsWithValues(JmxCollector.MetricCustomizer metricCustomizer, AttributeList attributes) {
Map<String, Object> attributeMap = attributes.asList().stream()
.collect(Collectors.toMap(Attribute::getName, Attribute::getValue));
Expand Down
31 changes: 31 additions & 0 deletions collector/src/test/java/io/prometheus/jmx/JmxCollectorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static void classSetUp() throws Exception {
Bool.registerBean(mbs);
Camel.registerBean(mbs);
CustomValue.registerBean(mbs);
StringValue.registerBean(mbs);
}

@Before
Expand Down Expand Up @@ -179,6 +180,36 @@ public void testMetricCustomizers() throws Exception {
.001);
}

@Test
public void testMetricCustomizersExtraMetrics() throws Exception {
new JmxCollector(
"\n---\nincludeObjectNames: [`io.prometheus.jmx:type=stringValue`]\nmetricCustomizers:\n - mbeanFilter:\n domain: io.prometheus.jmx\n properties:\n type: stringValue\n extraMetrics:\n - name: isActive\n value: true\n description: This is a boolean value indicating if the scenario is still active or is completed."
.replace('`', '"'))
.register(prometheusRegistry);
assertEquals(
1.0,
getSampleValue(
"io_prometheus_jmx_stringValue_isActive",
new String[] {},
new String[] {}),
.001);
}

@Test
public void testMetricCustomizersAttributesAsLabelsExtraMetrics() throws Exception {
new JmxCollector(
"\n---\nincludeObjectNames: [`io.prometheus.jmx:type=stringValue`]\nmetricCustomizers:\n - mbeanFilter:\n domain: io.prometheus.jmx\n properties:\n type: stringValue\n attributesAsLabels:\n - Text\n extraMetrics:\n - name: isActive\n value: true\n description: This is a boolean value indicating if the scenario is still active or is completed."
.replace('`', '"'))
.register(prometheusRegistry);
assertEquals(
1.0,
getSampleValue(
"io_prometheus_jmx_stringValue_isActive",
new String[] {"Text"},
new String[] {"value"}),
.001);
}

/*
@Test
public void testHelpFromPattern() throws Exception {
Expand Down
46 changes: 46 additions & 0 deletions collector/src/test/java/io/prometheus/jmx/StringValueMBean.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023-present The Prometheus jmx_exporter 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.
*/
package io.prometheus.jmx;

import javax.management.MBeanServer;
import javax.management.ObjectName;

/** Class to implement StringValueMBean */
public interface StringValueMBean {

/**
* Method to get the text
*
* @return text
*/
String getText();
}

/** Class to implement StringValue */
class StringValue implements StringValueMBean {

@Override
public String getText() {
return "value";
}

public static void registerBean(MBeanServer mbs) throws javax.management.JMException {
ObjectName mbeanName =
new ObjectName("io.prometheus.jmx:type=stringValue");
StringValueMBean mbean = new StringValue();
mbs.registerMBean(mbean, mbeanName);
}
}
8 changes: 6 additions & 2 deletions docs/content/1.1.0/http-mode/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ labels | A map of label name to label value pairs. Capture groups fro
help | Help text for the metric. Capture groups from `pattern` can be used. `name` must be set to use this. Defaults to the mBean attribute description, domain, and name of the attribute.
cache | Whether to cache bean name expressions to rule computation (match and mismatch). Not recommended for rules matching on bean value, as only the value from the first scrape will be cached and re-used. This can increase performance when collecting a lot of mbeans. Defaults to `false`.
type | The type of the metric, can be `GAUGE`, `COUNTER` or `UNTYPED`. `name` must be set to use this. Defaults to `UNTYPED`.
metricCustomizers | A list of objects that contain `mbeanFilter` and `attributesAsLabels`. For those mBeans that match the filter, the items in the `attributesAsLabels` list will be added as attributes to the existing metrics.
metricCustomizers | A list of objects that contain `mbeanFilter`, and at least one of `attributesAsLabels` and `extraMetrics`. For those mBeans that match the filter, the items in the `attributesAsLabels` list will be added as attributes to the existing, or new metrics, and items in the `extraMetrics` will generate new metrics.
mbeanFilter | A map of the criteria by which mBeans are filtered. It contains `domain` and `properties`.
domain | Domain of an mBean. Mandatory if `metricCustomizers` defined.
properties | Properties of an mBean. Optional
attributesAsLabels | List of elements to be added as attributes to existing metrics. Mandatory if `metricCustomizers` defined.
attributesAsLabels | List of elements to be added as attributes to existing metrics. Mandatory if `metricCustomizers` defined, and `extraMetrics` not.
extraMetrics | A list of map of elements in order to create a new metric. It contains `name`, `value` and `description`. Mandatory if `metricCustomizers` defined, and `attributesAsLabels` not.
name | The name of the new metric. Mandatory if `extraMetrics` defined.
value | The value of the new metric. It needs to be boolean or number. Mandatory if `extraMetrics` defined.
description | The description of the new metric. Used in the HELP section of the logs and indicates what purpose the metric serves. Optional

Metric names and label names are sanitized. All characters other than `[a-zA-Z0-9:_]` are replaced with underscores,
and adjacent underscores are collapsed. There's no limitations on label values or the help text.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (C) 2023-present The Prometheus jmx_exporter 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.
*/

package io.prometheus.jmx.test.core;

import io.prometheus.jmx.test.support.ExporterPath;
import io.prometheus.jmx.test.support.ExporterTestEnvironment;
import io.prometheus.jmx.test.support.TestSupport;
import io.prometheus.jmx.test.support.http.HttpClient;
import io.prometheus.jmx.test.support.http.HttpHeader;
import io.prometheus.jmx.test.support.http.HttpResponse;
import io.prometheus.jmx.test.support.metrics.Metric;
import io.prometheus.jmx.test.support.metrics.MetricsContentType;
import io.prometheus.jmx.test.support.metrics.MetricsParser;
import org.testcontainers.containers.Network;
import org.verifyica.api.ArgumentContext;
import org.verifyica.api.ClassContext;
import org.verifyica.api.Trap;
import org.verifyica.api.Verifyica;

import java.io.IOException;
import java.util.*;
import java.util.stream.Stream;

import static io.prometheus.jmx.test.support.Assertions.assertCommonMetricsResponse;
import static io.prometheus.jmx.test.support.Assertions.assertHealthyResponse;
import static org.assertj.core.api.Assertions.assertThat;

public class MetricCustomizersAttributesAsLabelsExtraMetricsTest {

@Verifyica.ArgumentSupplier(parallelism = Integer.MAX_VALUE)
public static Stream<ExporterTestEnvironment> arguments() {
return ExporterTestEnvironment.createExporterTestEnvironments();
}

@Verifyica.Prepare
public static void prepare(ClassContext classContext) {
TestSupport.getOrCreateNetwork(classContext);
}

@Verifyica.BeforeAll
public void beforeAll(ArgumentContext argumentContext) {
Class<?> testClass = argumentContext.classContext().testClass();
Network network = TestSupport.getOrCreateNetwork(argumentContext);
TestSupport.initializeExporterTestEnvironment(argumentContext, network, testClass);
}

@Verifyica.Test
@Verifyica.Order(1)
public void testHealthy(ExporterTestEnvironment exporterTestEnvironment) throws
IOException {
String url = exporterTestEnvironment.getUrl(ExporterPath.HEALTHY);

HttpResponse httpResponse = HttpClient.sendRequest(url);

assertHealthyResponse(httpResponse);
}

@Verifyica.Test
public void testDefaultTextMetrics(ExporterTestEnvironment exporterTestEnvironment)
throws IOException {
String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS);

HttpResponse httpResponse = HttpClient.sendRequest(url);

assertMetricsResponse(exporterTestEnvironment, httpResponse, MetricsContentType.DEFAULT);
}

@Verifyica.Test
public void testOpenMetricsTextMetrics(ExporterTestEnvironment exporterTestEnvironment)
throws IOException {
String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS);

HttpResponse httpResponse =
HttpClient.sendRequest(
url,
HttpHeader.ACCEPT,
MetricsContentType.OPEN_METRICS_TEXT_METRICS.toString());

assertMetricsResponse(
exporterTestEnvironment,
httpResponse,
MetricsContentType.OPEN_METRICS_TEXT_METRICS);
}

@Verifyica.Test
public void testPrometheusTextMetrics(ExporterTestEnvironment exporterTestEnvironment)
throws IOException {
String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS);

HttpResponse httpResponse =
HttpClient.sendRequest(
url,
HttpHeader.ACCEPT,
MetricsContentType.PROMETHEUS_TEXT_METRICS.toString());

assertMetricsResponse(
exporterTestEnvironment, httpResponse,
MetricsContentType.PROMETHEUS_TEXT_METRICS);
}

@Verifyica.Test
public void testPrometheusProtobufMetrics(ExporterTestEnvironment exporterTestEnvironment)
throws IOException {
String url = exporterTestEnvironment.getUrl(ExporterPath.METRICS);

HttpResponse httpResponse =
HttpClient.sendRequest(
url,
HttpHeader.ACCEPT,
MetricsContentType.PROMETHEUS_PROTOBUF_METRICS.toString());

assertMetricsResponse(
exporterTestEnvironment,
httpResponse,
MetricsContentType.PROMETHEUS_PROTOBUF_METRICS);
}

@Verifyica.AfterAll
public void afterAll(ArgumentContext argumentContext) throws Throwable {
List<Trap> traps = new ArrayList<>();

traps.add(new Trap(() -> TestSupport.destroyExporterTestEnvironment(argumentContext)));
traps.add(new Trap(() -> TestSupport.destroyNetwork(argumentContext)));

Trap.assertEmpty(traps);
}

@Verifyica.Conclude
public static void conclude(ClassContext classContext) throws Throwable {
new Trap(() -> TestSupport.destroyNetwork(classContext)).assertEmpty();
}

private void assertMetricsResponse(
ExporterTestEnvironment exporterTestEnvironment,
HttpResponse httpResponse,
MetricsContentType metricsContentType) {
assertCommonMetricsResponse(httpResponse, metricsContentType);

Collection<Metric> metrics = MetricsParser.parseCollection(httpResponse);

metrics.stream()
.filter(metric -> metric.name().equals("io_prometheus_jmx_stringValue_isActive"))
.forEach(
metric -> {
assertThat(metric.value())
.isEqualTo(1);
assertThat(metric.labels())
.containsEntry("Text", "value");
});
}
}
Loading