Skip to content

Commit 0f1f915

Browse files
authored
Introduce meter conventions and apply to some JVM metrics (#6682)
Enable specifying a convention to control well-known metrics' names and tags. Instrumentation can use this so its users can swap the convention applied without having to rewrite the instrumentation or resort to `MeterFilter` which can be more brittle. The first use of this is in some of the JVM metrics instrumentations. Specifically, the instrumentation that covers the currently stable OpenTelemetry semantic conventions has been updated to utilize MeterConvention. The default convention remains the same, although re-implemented using MeterConvention. An additional implementation for each binder was added that matches the current version (1.37.0) of the OpenTelemetry semantic conventions for JVM metrics. This allows users to configure Micrometer's JVM metrics instrumentation to use the stable OTel semantic conventions without the need for a `MeterFilter`. Initial documentation has been added for this, which could be made more thorough in the future.
1 parent 39821d9 commit 0f1f915

30 files changed

+1325
-80
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
** xref:concepts/long-task-timers.adoc[Long Task Timers]
1515
** xref:concepts/histogram-quantiles.adoc[Histograms and Percentiles]
1616
** xref:concepts/meter-provider.adoc[Meter Provider]
17+
** xref:concepts/meter-convention.adoc[Meter Convention]
1718
* xref:implementations.adoc[Implementations]
1819
** xref:implementations/appOptics.adoc[AppOptics]
1920
** xref:implementations/atlas.adoc[Atlas]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[[meter-convention]]
2+
= Meter Convention
3+
4+
While `MeterFilter` can be used to customize the name and tags (collectively, the convention) of Meters in any instrumentation, it can be more convenient and robust to customize the convention for a common instrumentation more directly.
5+
This is the purpose of the `MeterConvention`.
6+
7+
For examples of its usage in instrumentation provided by Micrometer as well as examples of different implementations of a convention for an instrumentation, see the xref:../reference/jvm.adoc#meter-conventions[JVM metrics].

docs/modules/ROOT/pages/reference/jvm.adoc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ To use the following `ExecutorService` instances, `--add-opens java.base/java.ut
5252
* `Executors.newThreadPerTaskExecutor()`
5353
* `Executors.newVirtualThreadPerTaskExecutor()`
5454

55+
[[meter-conventions]]
56+
== Meter Conventions
57+
58+
See also the general xref:../concepts/meter-convention.adoc[Meter Conventions] documentation.
59+
60+
The following `MeterBinder` implementations have a constructor to customize the convention used for some or all of the meters they produce.
61+
This can be used as an alternative to customizing the Meter names and tags with a `MeterFilter`.
62+
63+
* `ClassLoaderMetrics`
64+
* `JvmMemoryMetrics`
65+
* `JvmThreadMetrics`
66+
* `ProcessorMetrics`
67+
68+
=== OpenTelemetry Semantic Conventions
69+
70+
We provide `MeterConvention` implementations of the applicable https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/[OpenTelemetry semantic conventions for JVM metrics] that have been marked stable as of the version 1.37.0.
71+
These can be passed to the constructors of the above binders to change the convention from the default.
72+
Use the following classes:
73+
74+
* `OpenTelemetryJvmClassLoadingMeterConventions`
75+
* `OpenTelemetryJvmMemoryMeterConventions`
76+
* `OpenTelemetryJvmThreadMeterConventions`
77+
* `OpenTelemetryJvmCpuMeterConventions`
78+
5579
== Java 21 Metrics
5680

5781
=== Virtual Threads
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.binder;
17+
18+
import io.micrometer.core.instrument.Meter;
19+
import io.micrometer.core.instrument.Tags;
20+
import io.micrometer.core.instrument.config.NamingConvention;
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationConvention;
23+
import org.jspecify.annotations.NonNull;
24+
import org.jspecify.annotations.NullUnmarked;
25+
import org.jspecify.annotations.Nullable;
26+
27+
/**
28+
* Convention describing a {@link Meter} used for a specific instrumentation. Different
29+
* implementations of this convention may change the convention used by the same
30+
* instrumentation logic. If the instrumentation is timing an operation, consider using
31+
* the {@link Observation} API and {@link ObservationConvention} instead.
32+
* {@link MeterConvention} is for instrumentation that is instrumented directly with the
33+
* metrics API.
34+
* <p>
35+
* This is a distinct concept from {@link NamingConvention}. The name provided by the
36+
* {@link MeterConvention} is the canonical name, which will potentially be transformed by
37+
* a {@link NamingConvention} for a specific metrics backend or format.
38+
*
39+
* @param <C> context type used to derive tags
40+
* @since 1.16.0
41+
*/
42+
// Work around NullAway. C might be Void, and thus we pass null to getTags.
43+
@NullUnmarked
44+
public interface MeterConvention<C extends @Nullable Object> {
45+
46+
/**
47+
* Canonical name of the meter.
48+
* @return meter name
49+
*/
50+
@NonNull String getName();
51+
52+
/**
53+
* Tags specific to this meter convention. Generally they should be combined with any
54+
* common tags if this convention is associated with other conventions that share
55+
* tags.
56+
* @param context optional context which may be used for deriving tags
57+
* @return tags instance to use with the meter described by this convention
58+
*/
59+
default @NonNull Tags getTags(C context) {
60+
return Tags.empty();
61+
}
62+
63+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.binder;
17+
18+
import io.micrometer.core.instrument.Tags;
19+
import org.jspecify.annotations.Nullable;
20+
21+
import java.util.Objects;
22+
import java.util.function.Function;
23+
24+
/**
25+
* Basic implementation of a {@link MeterConvention}.
26+
*
27+
* @param <C> context type from which tags can be derived
28+
* @since 1.16.0
29+
*/
30+
public class SimpleMeterConvention<C extends @Nullable Object> implements MeterConvention<C> {
31+
32+
private final String name;
33+
34+
private final @Nullable Tags tags;
35+
36+
private final @Nullable Function<C, Tags> tagsFunction;
37+
38+
/**
39+
* Create a convention with a name and no tags.
40+
* @param name meter name
41+
*/
42+
public SimpleMeterConvention(String name) {
43+
this(name, Tags.empty());
44+
}
45+
46+
/**
47+
* Create a convention with a name and fixed tags.
48+
* @param name meter name
49+
* @param tags tags associated with the meter
50+
*/
51+
public SimpleMeterConvention(String name, Tags tags) {
52+
this.name = name;
53+
this.tags = tags;
54+
this.tagsFunction = null;
55+
}
56+
57+
/**
58+
* Create a convention with a name and tags derived from a function.
59+
* @param name meter name
60+
* @param tagsFunction derive tags from the context with this function
61+
*/
62+
public SimpleMeterConvention(String name, Function<C, Tags> tagsFunction) {
63+
this.name = name;
64+
this.tags = null;
65+
this.tagsFunction = Objects.requireNonNull(tagsFunction);
66+
}
67+
68+
@Override
69+
public String getName() {
70+
return name;
71+
}
72+
73+
@Override
74+
public Tags getTags(C context) {
75+
return tags == null ? Objects.requireNonNull(tagsFunction).apply(context) : tags;
76+
}
77+
78+
}

micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,75 @@
1515
*/
1616
package io.micrometer.core.instrument.binder.jvm;
1717

18-
import io.micrometer.core.instrument.FunctionCounter;
19-
import io.micrometer.core.instrument.Gauge;
20-
import io.micrometer.core.instrument.MeterRegistry;
21-
import io.micrometer.core.instrument.Tag;
18+
import io.micrometer.core.instrument.*;
2219
import io.micrometer.core.instrument.binder.BaseUnits;
2320
import io.micrometer.core.instrument.binder.MeterBinder;
24-
import org.jspecify.annotations.NullMarked;
21+
import io.micrometer.core.instrument.binder.MeterConvention;
22+
import io.micrometer.core.instrument.binder.jvm.convention.JvmClassLoadingMeterConventions;
23+
import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmClassLoadingMeterConventions;
2524

2625
import java.lang.management.ClassLoadingMXBean;
2726
import java.lang.management.ManagementFactory;
2827

29-
import static java.util.Collections.emptyList;
30-
31-
@NullMarked
28+
/**
29+
* Binder providing metrics related to JVM class loading.
30+
*
31+
* @see ClassLoadingMXBean
32+
*/
3233
public class ClassLoaderMetrics implements MeterBinder {
3334

34-
private final Iterable<Tag> tags;
35+
private final JvmClassLoadingMeterConventions conventions;
3536

37+
/**
38+
* Class loader metrics with the default convention.
39+
*/
3640
public ClassLoaderMetrics() {
37-
this(emptyList());
41+
this(new MicrometerJvmClassLoadingMeterConventions());
42+
}
43+
44+
/**
45+
* Class loader metrics using the default convention with extra tags added.
46+
* @param extraTags additional tags to add to metrics registered by this binder
47+
*/
48+
public ClassLoaderMetrics(Iterable<Tag> extraTags) {
49+
this(new MicrometerJvmClassLoadingMeterConventions(Tags.of(extraTags)));
3850
}
3951

40-
public ClassLoaderMetrics(Iterable<Tag> tags) {
41-
this.tags = tags;
52+
/**
53+
* Class loader metrics registered by this binder will use the provided convention.
54+
* @param conventions custom convention to apply
55+
* @since 1.16.0
56+
*/
57+
public ClassLoaderMetrics(JvmClassLoadingMeterConventions conventions) {
58+
this.conventions = conventions;
4259
}
4360

4461
@Override
4562
public void bindTo(MeterRegistry registry) {
4663
ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean();
4764

48-
Gauge.builder("jvm.classes.loaded", classLoadingBean, ClassLoadingMXBean::getLoadedClassCount)
49-
.tags(tags)
65+
MeterConvention<Object> currentClassCountConvention = conventions.currentClassCountConvention();
66+
Gauge.builder(currentClassCountConvention.getName(), classLoadingBean, ClassLoadingMXBean::getLoadedClassCount)
67+
.tags(currentClassCountConvention.getTags(null))
5068
.description("The number of classes that are currently loaded in the Java virtual machine")
5169
.baseUnit(BaseUnits.CLASSES)
5270
.register(registry);
5371

54-
FunctionCounter.builder("jvm.classes.unloaded", classLoadingBean, ClassLoadingMXBean::getUnloadedClassCount)
55-
.tags(tags)
72+
MeterConvention<Object> unloadedConvention = conventions.unloadedConvention();
73+
FunctionCounter
74+
.builder(unloadedConvention.getName(), classLoadingBean, ClassLoadingMXBean::getUnloadedClassCount)
75+
.tags(unloadedConvention.getTags(null))
5676
.description("The number of classes unloaded in the Java virtual machine")
5777
.baseUnit(BaseUnits.CLASSES)
5878
.register(registry);
79+
80+
MeterConvention<Object> loadedConvention = conventions.loadedConvention();
81+
FunctionCounter
82+
.builder(loadedConvention.getName(), classLoadingBean, ClassLoadingMXBean::getTotalLoadedClassCount)
83+
.tags(loadedConvention.getTags(null))
84+
.description("The number of classes loaded in the Java virtual machine")
85+
.baseUnit(BaseUnits.CLASSES)
86+
.register(registry);
5987
}
6088

6189
}

micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
import io.micrometer.core.instrument.Tags;
2222
import io.micrometer.core.instrument.binder.BaseUnits;
2323
import io.micrometer.core.instrument.binder.MeterBinder;
24-
import org.jspecify.annotations.NullMarked;
24+
import io.micrometer.core.instrument.binder.MeterConvention;
25+
import io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions;
26+
import io.micrometer.core.instrument.binder.jvm.convention.micrometer.MicrometerJvmMemoryMeterConventions;
2527

2628
import java.lang.management.*;
2729

2830
import static io.micrometer.core.instrument.binder.jvm.JvmMemory.getUsageValue;
29-
import static java.util.Collections.emptyList;
3031

3132
/**
3233
* Record metrics that report utilization of various memory and buffer pools.
@@ -36,17 +37,36 @@
3637
* @see MemoryPoolMXBean
3738
* @see BufferPoolMXBean
3839
*/
39-
@NullMarked
4040
public class JvmMemoryMetrics implements MeterBinder {
4141

42-
private final Iterable<Tag> tags;
42+
private final Tags tags;
43+
44+
private final JvmMemoryMeterConventions conventions;
4345

4446
public JvmMemoryMetrics() {
45-
this(emptyList());
47+
this(Tags.empty(), new MicrometerJvmMemoryMeterConventions());
48+
}
49+
50+
/**
51+
* Uses the default convention with the provided extra tags.
52+
* @param extraTags tags to add to each meter's tags produced by this binder
53+
*/
54+
public JvmMemoryMetrics(Iterable<Tag> extraTags) {
55+
this(extraTags, new MicrometerJvmMemoryMeterConventions(Tags.of(extraTags)));
4656
}
4757

48-
public JvmMemoryMetrics(Iterable<Tag> tags) {
49-
this.tags = tags;
58+
/**
59+
* Memory metrics with extra tags and a specific convention applied to meters. The
60+
* supplied extra tags are not combined with the convention. Provide a convention that
61+
* applies the extra tags if that is the desired outcome. The convention only applies
62+
* to some meters.
63+
* @param extraTags these will be added to meters not covered by the convention
64+
* @param conventions custom conventions for applicable metrics
65+
* @since 1.16.0
66+
*/
67+
public JvmMemoryMetrics(Iterable<? extends Tag> extraTags, JvmMemoryMeterConventions conventions) {
68+
this.tags = Tags.of(extraTags);
69+
this.conventions = conventions;
5070
}
5171

5272
@Override
@@ -74,24 +94,29 @@ public void bindTo(MeterRegistry registry) {
7494
}
7595

7696
for (MemoryPoolMXBean memoryPoolBean : ManagementFactory.getPlatformMXBeans(MemoryPoolMXBean.class)) {
77-
String area = MemoryType.HEAP.equals(memoryPoolBean.getType()) ? "heap" : "nonheap";
78-
Iterable<Tag> tagsWithId = Tags.concat(tags, "id", memoryPoolBean.getName(), "area", area);
79-
80-
Gauge.builder("jvm.memory.used", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getUsed))
81-
.tags(tagsWithId)
97+
MeterConvention<MemoryPoolMXBean> memoryUsedConvention = conventions.getMemoryUsedConvention();
98+
Gauge
99+
.builder(memoryUsedConvention.getName(), memoryPoolBean,
100+
(mem) -> getUsageValue(mem, MemoryUsage::getUsed))
101+
.tags(memoryUsedConvention.getTags(memoryPoolBean))
82102
.description("The amount of used memory")
83103
.baseUnit(BaseUnits.BYTES)
84104
.register(registry);
85105

106+
MeterConvention<MemoryPoolMXBean> memoryCommittedConvention = conventions.getMemoryCommittedConvention();
86107
Gauge
87-
.builder("jvm.memory.committed", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getCommitted))
88-
.tags(tagsWithId)
108+
.builder(memoryCommittedConvention.getName(), memoryPoolBean,
109+
(mem) -> getUsageValue(mem, MemoryUsage::getCommitted))
110+
.tags(memoryCommittedConvention.getTags(memoryPoolBean))
89111
.description("The amount of memory in bytes that is committed for the Java virtual machine to use")
90112
.baseUnit(BaseUnits.BYTES)
91113
.register(registry);
92114

93-
Gauge.builder("jvm.memory.max", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getMax))
94-
.tags(tagsWithId)
115+
MeterConvention<MemoryPoolMXBean> memoryMaxConvention = conventions.getMemoryMaxConvention();
116+
Gauge
117+
.builder(memoryMaxConvention.getName(), memoryPoolBean,
118+
(mem) -> getUsageValue(mem, MemoryUsage::getMax))
119+
.tags(memoryMaxConvention.getTags(memoryPoolBean))
95120
.description("The maximum amount of memory in bytes that can be used for memory management")
96121
.baseUnit(BaseUnits.BYTES)
97122
.register(registry);

0 commit comments

Comments
 (0)