-
Notifications
You must be signed in to change notification settings - Fork 335
Support OTLP runtime metrics with OTel-native naming #11318
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
base: master
Are you sure you want to change the base?
Changes from all commits
d308063
5e8567f
4a2901f
6a04bcc
953c871
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| package datadog.opentelemetry.shim.metrics; | ||
|
|
||
| import com.sun.management.OperatingSystemMXBean; | ||
| import io.opentelemetry.api.common.AttributeKey; | ||
| import io.opentelemetry.api.common.Attributes; | ||
| import io.opentelemetry.api.metrics.Meter; | ||
| import java.lang.management.BufferPoolMXBean; | ||
| import java.lang.management.ClassLoadingMXBean; | ||
| import java.lang.management.ManagementFactory; | ||
| import java.lang.management.MemoryMXBean; | ||
| import java.lang.management.MemoryPoolMXBean; | ||
| import java.lang.management.MemoryUsage; | ||
| import java.lang.management.ThreadMXBean; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.function.ToLongFunction; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Registers JVM runtime metrics with OTel-native names against the agent's MeterProvider. See | ||
| * https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/. | ||
| */ | ||
| public final class JvmOtlpRuntimeMetrics { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to move this class to another module. The responsibility of the The best place atm to put this is under the |
||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(JvmOtlpRuntimeMetrics.class); | ||
| private static final String INSTRUMENTATION_SCOPE = "datadog.jvm.runtime"; | ||
| private static final AttributeKey<String> MEMORY_TYPE = AttributeKey.stringKey("jvm.memory.type"); | ||
| private static final AttributeKey<String> MEMORY_POOL = | ||
| AttributeKey.stringKey("jvm.memory.pool.name"); | ||
| private static final AttributeKey<String> BUFFER_POOL = | ||
| AttributeKey.stringKey("jvm.buffer.pool.name"); | ||
| private static final Attributes HEAP_ATTRS = Attributes.of(MEMORY_TYPE, "heap"); | ||
| private static final Attributes NON_HEAP_ATTRS = Attributes.of(MEMORY_TYPE, "non_heap"); | ||
|
|
||
| private static final AtomicBoolean started = new AtomicBoolean(false); | ||
|
|
||
| /** Registers all JVM runtime metric instruments on the OTel MeterProvider. */ | ||
| public static void start() { | ||
| if (!started.compareAndSet(false, true)) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| Meter meter = OtelMeterProvider.INSTANCE.get(INSTRUMENTATION_SCOPE); | ||
| registerMemoryMetrics(meter); | ||
| registerBufferMetrics(meter); | ||
| registerThreadMetrics(meter); | ||
| registerClassLoadingMetrics(meter); | ||
| registerCpuMetrics(meter); | ||
| log.debug("Started OTLP runtime metrics with OTel-native naming (jvm.*)"); | ||
| } catch (Exception e) { | ||
| log.error("Failed to start JVM OTLP runtime metrics", e); | ||
| } | ||
| } | ||
|
|
||
| // jvm.gc.duration is excluded — spec requires Histogram, JMX only exposes cumulative time. | ||
|
|
||
| /** | ||
| * jvm.memory.used, jvm.memory.committed, jvm.memory.limit, jvm.memory.init, | ||
| * jvm.memory.used_after_last_gc — all UpDownCounter per spec. | ||
| */ | ||
| private static void registerMemoryMetrics(Meter meter) { | ||
| MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); | ||
| List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans(); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.memory.used") | ||
| .setDescription("Measure of memory used.") | ||
| .setUnit("By") | ||
| .buildWithCallback( | ||
|
Comment on lines
+68
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When OTLP runtime metrics are enabled ( Useful? React with 👍 / 👎. |
||
| measurement -> { | ||
| measurement.record(memoryBean.getHeapMemoryUsage().getUsed(), HEAP_ATTRS); | ||
| measurement.record(memoryBean.getNonHeapMemoryUsage().getUsed(), NON_HEAP_ATTRS); | ||
| for (MemoryPoolMXBean pool : pools) { | ||
| measurement.record(pool.getUsage().getUsed(), poolAttributes(pool)); | ||
| } | ||
| }); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.memory.committed") | ||
| .setDescription("Measure of memory committed.") | ||
| .setUnit("By") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| measurement.record(memoryBean.getHeapMemoryUsage().getCommitted(), HEAP_ATTRS); | ||
| measurement.record(memoryBean.getNonHeapMemoryUsage().getCommitted(), NON_HEAP_ATTRS); | ||
| for (MemoryPoolMXBean pool : pools) { | ||
| measurement.record(pool.getUsage().getCommitted(), poolAttributes(pool)); | ||
| } | ||
| }); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.memory.limit") | ||
| .setDescription("Measure of max obtainable memory.") | ||
| .setUnit("By") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| long heapMax = memoryBean.getHeapMemoryUsage().getMax(); | ||
| if (heapMax > 0) { | ||
| measurement.record(heapMax, HEAP_ATTRS); | ||
| } | ||
| long nonHeapMax = memoryBean.getNonHeapMemoryUsage().getMax(); | ||
| if (nonHeapMax > 0) { | ||
| measurement.record(nonHeapMax, NON_HEAP_ATTRS); | ||
| } | ||
| for (MemoryPoolMXBean pool : pools) { | ||
| long max = pool.getUsage().getMax(); | ||
| if (max > 0) { | ||
| measurement.record(max, poolAttributes(pool)); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.memory.init") | ||
| .setDescription("Measure of initial memory requested.") | ||
| .setUnit("By") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| long heapInit = memoryBean.getHeapMemoryUsage().getInit(); | ||
| if (heapInit > 0) { | ||
| measurement.record(heapInit, HEAP_ATTRS); | ||
| } | ||
| long nonHeapInit = memoryBean.getNonHeapMemoryUsage().getInit(); | ||
| if (nonHeapInit > 0) { | ||
| measurement.record(nonHeapInit, NON_HEAP_ATTRS); | ||
| } | ||
| }); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.memory.used_after_last_gc") | ||
| .setDescription("Measure of memory used after the most recent garbage collection event.") | ||
| .setUnit("By") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| for (MemoryPoolMXBean pool : pools) { | ||
| MemoryUsage collectionUsage = pool.getCollectionUsage(); | ||
| if (collectionUsage != null && collectionUsage.getUsed() >= 0) { | ||
| measurement.record(collectionUsage.getUsed(), poolAttributes(pool)); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** jvm.buffer.* (UpDownCounter, Development) — direct + mapped pool metrics. */ | ||
| private static void registerBufferMetrics(Meter meter) { | ||
| List<BufferPoolMXBean> bufferPools = | ||
| ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class); | ||
| bufferPoolMetric( | ||
| meter, | ||
| "jvm.buffer.memory.used", | ||
| "Measure of memory used by buffers.", | ||
| "By", | ||
| bufferPools, | ||
| BufferPoolMXBean::getMemoryUsed); | ||
| bufferPoolMetric( | ||
| meter, | ||
| "jvm.buffer.memory.limit", | ||
| "Measure of total memory capacity of buffers.", | ||
| "By", | ||
| bufferPools, | ||
| BufferPoolMXBean::getTotalCapacity); | ||
| bufferPoolMetric( | ||
| meter, | ||
| "jvm.buffer.count", | ||
| "Number of buffers in the pool.", | ||
| "{buffer}", | ||
| bufferPools, | ||
| BufferPoolMXBean::getCount); | ||
| } | ||
|
|
||
| /** jvm.thread.count (UpDownCounter, Stable). */ | ||
| private static void registerThreadMetrics(Meter meter) { | ||
| ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); | ||
| meter | ||
| .upDownCounterBuilder("jvm.thread.count") | ||
| .setDescription("Number of executing platform threads.") | ||
| .setUnit("{thread}") | ||
| .buildWithCallback(measurement -> measurement.record(threadBean.getThreadCount())); | ||
| } | ||
|
|
||
| /** | ||
| * jvm.class.loaded (Counter), jvm.class.unloaded (Counter), jvm.class.count (UpDownCounter) — all | ||
| * Stable per spec. | ||
| */ | ||
| private static void registerClassLoadingMetrics(Meter meter) { | ||
| ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean(); | ||
| meter | ||
| .counterBuilder("jvm.class.loaded") | ||
| .setDescription("Number of classes loaded since JVM start.") | ||
| .setUnit("{class}") | ||
| .buildWithCallback( | ||
| measurement -> measurement.record(classLoadingBean.getTotalLoadedClassCount())); | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.class.count") | ||
| .setDescription("Number of classes currently loaded.") | ||
| .setUnit("{class}") | ||
| .buildWithCallback( | ||
| measurement -> measurement.record(classLoadingBean.getLoadedClassCount())); | ||
|
|
||
| meter | ||
| .counterBuilder("jvm.class.unloaded") | ||
| .setDescription("Number of classes unloaded since JVM start.") | ||
| .setUnit("{class}") | ||
| .buildWithCallback( | ||
| measurement -> measurement.record(classLoadingBean.getUnloadedClassCount())); | ||
| } | ||
|
|
||
| /** | ||
| * jvm.cpu.time (Counter), jvm.cpu.count (UpDownCounter), jvm.cpu.recent_utilization (Gauge) — all | ||
| * Stable per spec. | ||
| */ | ||
| private static void registerCpuMetrics(Meter meter) { | ||
| java.lang.management.OperatingSystemMXBean rawOsBean = | ||
| ManagementFactory.getOperatingSystemMXBean(); | ||
| OperatingSystemMXBean osBean = | ||
| rawOsBean instanceof OperatingSystemMXBean ? (OperatingSystemMXBean) rawOsBean : null; | ||
|
|
||
| if (osBean != null) { | ||
| meter | ||
| .counterBuilder("jvm.cpu.time") | ||
| .ofDoubles() | ||
| .setDescription("CPU time used by the process as reported by the JVM.") | ||
| .setUnit("s") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| long nanos = osBean.getProcessCpuTime(); | ||
| if (nanos >= 0) { | ||
| measurement.record(nanos / 1e9); | ||
| } | ||
| }); | ||
|
|
||
| meter | ||
| .gaugeBuilder("jvm.cpu.recent_utilization") | ||
| .setDescription("Recent CPU utilization for the process as reported by the JVM.") | ||
| .setUnit("1") | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| double cpuLoad = osBean.getProcessCpuLoad(); | ||
| if (cpuLoad >= 0) { | ||
| measurement.record(cpuLoad); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| meter | ||
| .upDownCounterBuilder("jvm.cpu.count") | ||
| .setDescription("Number of processors available to the JVM.") | ||
| .setUnit("{cpu}") | ||
| .buildWithCallback( | ||
| measurement -> measurement.record(Runtime.getRuntime().availableProcessors())); | ||
| } | ||
|
|
||
| /** | ||
| * Builds an UpDownCounter that iterates each platform buffer pool and records {@code getter} with | ||
| * the {@code jvm.buffer.pool.name} attribute. Skips negative readings. | ||
| */ | ||
| private static void bufferPoolMetric( | ||
| Meter meter, | ||
| String name, | ||
| String description, | ||
| String unit, | ||
| List<BufferPoolMXBean> bufferPools, | ||
| ToLongFunction<BufferPoolMXBean> getter) { | ||
| meter | ||
| .upDownCounterBuilder(name) | ||
| .setDescription(description) | ||
| .setUnit(unit) | ||
| .buildWithCallback( | ||
| measurement -> { | ||
| for (BufferPoolMXBean pool : bufferPools) { | ||
| long value = getter.applyAsLong(pool); | ||
| if (value >= 0) { | ||
| measurement.record(value, Attributes.of(BUFFER_POOL, pool.getName())); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** Returns Attributes carrying jvm.memory.type and jvm.memory.pool.name for the given pool. */ | ||
| private static Attributes poolAttributes(MemoryPoolMXBean pool) { | ||
| return Attributes.of( | ||
| MEMORY_TYPE, pool.getType().name().toLowerCase(Locale.ROOT), | ||
| MEMORY_POOL, pool.getName()); | ||
| } | ||
|
|
||
| private JvmOtlpRuntimeMetrics() {} | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JMX has some unfortunate side-effects which mean we can't start it at the same time as the tracer.
I would move
JvmOtlpRuntimeMetricsout fromotel-shimand into theagent-jmxfetchmodule. That way you can start in fromJMXFetchalong with the other runtime metrics. This would also let you benefit from the existing code that delays startingJMXFetchuntil the appropriate time.