diff --git a/docs/content/config/config.md b/docs/content/config/config.md index cd7f7af7b..e4903d889 100644 --- a/docs/content/config/config.md +++ b/docs/content/config/config.md @@ -89,6 +89,7 @@ When the same property is defined in multiple sources, the following precedence | io.prometheus.metrics.summary_quantile_errors | [Summary.Builder.quantile(double, double)]() | (5) | | io.prometheus.metrics.summary_max_age_seconds | [Summary.Builder.maxAgeSeconds()]() | | | io.prometheus.metrics.summary_number_of_age_buckets | [Summary.Builder.numberOfAgeBuckets()]() | | +| io.prometheus.metrics.use_otel_semconv | [MetricsProperties.useOtelSemconv()]() | (6) | @@ -100,7 +101,9 @@ not just for counters
(3) Comma-separated list. Example: `.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10`.
(4) Comma-separated list. Example: `0.5, 0.95, 0.99`.
(5) Comma-separated list. If specified, the list must have the same length as -`io.prometheus.metrics.summary_quantiles`. Example: `0.01, 0.005, 0.005`. +`io.prometheus.metrics.summary_quantiles`. Example: `0.01, 0.005, 0.005`.
+(6) Comma-separated list of OTel metric names. Use `*` to enable all. +Example: `jvm.gc.duration` or `*`. There's one special feature about metric properties: You can set a property for one specific metric only by specifying the metric name. Example: diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md index 804c1b09b..0855b3fe7 100644 --- a/docs/content/instrumentation/jvm.md +++ b/docs/content/instrumentation/jvm.md @@ -123,6 +123,29 @@ jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0 jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0 ``` +For more detailed GC metrics, enable the +[use_otel_semconv](https://prometheus.github.io/client_java/config/config/#metrics-properties) +configuration option by specifying `jvm.gc.duration` or `*` (for all +OTel metrics). This replaces the standard metric with a histogram +implemented according to the +[OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). + + + +```text +# HELP jvm_gc_duration_seconds Duration of JVM garbage collection actions. +# TYPE jvm_gc_duration_seconds histogram +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="0.01"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="0.1"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="1.0"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="10.0"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="+Inf"} 4 +jvm_gc_duration_seconds_count{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation"} 4 +jvm_gc_duration_seconds_sum{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation"} 0.029 +``` + + + ## JVM Memory Metrics JVM memory metrics are provided by diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java index a530f35e1..55a425b14 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java @@ -27,6 +27,7 @@ public class MetricsProperties { private static final String SUMMARY_QUANTILE_ERRORS = "summary_quantile_errors"; private static final String SUMMARY_MAX_AGE_SECONDS = "summary_max_age_seconds"; private static final String SUMMARY_NUMBER_OF_AGE_BUCKETS = "summary_number_of_age_buckets"; + private static final String USE_OTEL_SEMCONV = "use_otel_semconv"; /** * All known property suffixes that can be configured for metrics. @@ -46,7 +47,8 @@ public class MetricsProperties { SUMMARY_QUANTILES, SUMMARY_QUANTILE_ERRORS, SUMMARY_MAX_AGE_SECONDS, - SUMMARY_NUMBER_OF_AGE_BUCKETS + SUMMARY_NUMBER_OF_AGE_BUCKETS, + USE_OTEL_SEMCONV }; @Nullable private final Boolean exemplarsEnabled; @@ -62,6 +64,7 @@ public class MetricsProperties { @Nullable private final List summaryQuantileErrors; @Nullable private final Long summaryMaxAgeSeconds; @Nullable private final Integer summaryNumberOfAgeBuckets; + @Nullable private final List useOtelSemconv; public MetricsProperties( @Nullable Boolean exemplarsEnabled, @@ -91,6 +94,7 @@ public MetricsProperties( summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, + null, ""); } @@ -108,6 +112,7 @@ private MetricsProperties( @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, + @Nullable List useOtelSemconv, String configPropertyPrefix) { this.exemplarsEnabled = exemplarsEnabled; this.histogramNativeOnly = isHistogramNativeOnly(histogramClassicOnly, histogramNativeOnly); @@ -129,6 +134,8 @@ private MetricsProperties( : unmodifiableList(new ArrayList<>(summaryQuantileErrors)); this.summaryMaxAgeSeconds = summaryMaxAgeSeconds; this.summaryNumberOfAgeBuckets = summaryNumberOfAgeBuckets; + this.useOtelSemconv = + useOtelSemconv == null ? null : unmodifiableList(new ArrayList<>(useOtelSemconv)); validate(configPropertyPrefix); } @@ -353,6 +360,15 @@ public Integer getSummaryNumberOfAgeBuckets() { return summaryNumberOfAgeBuckets; } + /** + * List of OTel metric names for which OpenTelemetry Semantic Conventions should be used. Use + * {@code "*"} to enable for all metrics. Returns {@code null} if not configured. + */ + @Nullable + public List useOtelSemconv() { + return useOtelSemconv; + } + /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -373,6 +389,7 @@ static MetricsProperties load(String prefix, PropertySource propertySource) Util.loadDoubleList(prefix, SUMMARY_QUANTILE_ERRORS, propertySource), Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource), Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource), + Util.loadStringList(prefix, USE_OTEL_SEMCONV, propertySource), prefix); } @@ -394,6 +411,7 @@ public static class Builder { @Nullable private List summaryQuantileErrors; @Nullable private Long summaryMaxAgeSeconds; @Nullable private Integer summaryNumberOfAgeBuckets; + @Nullable private List useOtelSemconv; private Builder() {} @@ -411,7 +429,9 @@ public MetricsProperties build() { summaryQuantiles, summaryQuantileErrors, summaryMaxAgeSeconds, - summaryNumberOfAgeBuckets); + summaryNumberOfAgeBuckets, + useOtelSemconv, + ""); } /** See {@link MetricsProperties#getExemplarsEnabled()} */ @@ -495,5 +515,11 @@ public Builder summaryNumberOfAgeBuckets(@Nullable Integer summaryNumberOfAgeBuc this.summaryNumberOfAgeBuckets = summaryNumberOfAgeBuckets; return this; } + + /** See {@link MetricsProperties#useOtelSemconv()} */ + public Builder useOtelSemconv(String... useOtelSemconv) { + this.useOtelSemconv = Util.toStringList(useOtelSemconv); + return this; + } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index 055fe4aa3..5f4f2e3b7 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -1,6 +1,7 @@ package io.prometheus.metrics.config; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.annotation.Nullable; @@ -167,6 +168,17 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } + public boolean useOtelSemconv(String otelMetric) { + List list = getDefaultMetricProperties().useOtelSemconv(); + if (list == null || list.isEmpty()) { + return false; + } + if (list.contains("*")) { + return true; + } + return list.contains(otelMetric); + } + public static class Builder { private MetricsProperties defaultMetricsProperties = MetricsProperties.builder().build(); private final MetricPropertiesMap metricProperties = new MetricPropertiesMap(); diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java index 20bd75699..52a4aa017 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java @@ -31,6 +31,14 @@ static Boolean loadBoolean(String prefix, String propertyName, PropertySource pr return null; } + @Nullable + static List toStringList(@Nullable String... values) { + if (values == null) { + return null; + } + return Arrays.asList(values); + } + @Nullable static List toList(@Nullable double... values) { if (values == null) { diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 3e891202a..ee4d7cb99 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -128,4 +128,36 @@ void testMetricNameStartingWithNumber() { assertThat(result.getMetricProperties("123metric")).isSameAs(customProps); assertThat(result.getMetricProperties("_23metric")).isSameAs(customProps); } + + @Test + void useOtelSemconvReturnsFalseForMetricNotInList() { + PrometheusProperties props = buildProperties("jvm.gc.duration"); + assertThat(props.useOtelSemconv("other.metric")).isFalse(); + } + + @Test + void useOtelSemconvWildcardEnablesAll() { + PrometheusProperties props = buildProperties("*"); + assertThat(props.useOtelSemconv("any.metric")).isTrue(); + } + + @Test + void useOtelSemconvNullListReturnsFalse() { + PrometheusProperties props = PrometheusProperties.get(); + assertThat(props.useOtelSemconv("otel_y")).isFalse(); + } + + @Test + void useOtelSemconvSpecificMetricReturnsTrueForMatch() { + PrometheusProperties props = buildProperties("jvm.gc.duration", "jvm.memory.used"); + assertThat(props.useOtelSemconv("jvm.gc.duration")).isTrue(); + assertThat(props.useOtelSemconv("jvm.memory.used")).isTrue(); + assertThat(props.useOtelSemconv("other.metric")).isFalse(); + } + + private static PrometheusProperties buildProperties(String... otelSemconv) { + return PrometheusProperties.builder() + .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv(otelSemconv).build()) + .build(); + } } diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index a87b52a4f..8b824bca3 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -1,6 +1,8 @@ package io.prometheus.metrics.instrumentation.jvm; +import com.sun.management.GarbageCollectionNotificationInfo; import io.prometheus.metrics.config.PrometheusProperties; +import io.prometheus.metrics.core.metrics.Histogram; import io.prometheus.metrics.core.metrics.SummaryWithCallback; import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.Labels; @@ -10,6 +12,8 @@ import java.lang.management.ManagementFactory; import java.util.List; import javax.annotation.Nullable; +import javax.management.NotificationEmitter; +import javax.management.openmbean.CompositeData; /** * JVM Garbage Collector metrics. The {@link JvmGarbageCollectorMetrics} are registered as part of @@ -19,14 +23,14 @@ * JvmMetrics.builder().register(); * } * - * However, if you want only the {@link JvmGarbageCollectorMetrics} you can also register them + *

However, if you want only the {@link JvmGarbageCollectorMetrics} you can also register them * directly: * *

{@code
  * JvmGarbageCollectorMetrics.builder().register();
  * }
* - * Example metrics being exported: + *

Example metrics being exported: * *

  * # HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
@@ -40,6 +44,7 @@
 public class JvmGarbageCollectorMetrics {
 
   private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds";
+  private static final String JVM_GC_DURATION = "jvm.gc.duration";
 
   private final PrometheusProperties config;
   private final List garbageCollectorBeans;
@@ -55,7 +60,14 @@ private JvmGarbageCollectorMetrics(
   }
 
   private void register(PrometheusRegistry registry) {
+    if (config.useOtelSemconv(JVM_GC_DURATION)) {
+      registerOtel(registry);
+    } else {
+      registerPrometheus(registry);
+    }
+  }
 
+  private void registerPrometheus(PrometheusRegistry registry) {
     SummaryWithCallback.builder(config)
         .name(JVM_GC_COLLECTION_SECONDS)
         .help("Time spent in a given JVM garbage collector in seconds.")
@@ -75,6 +87,54 @@ private void register(PrometheusRegistry registry) {
         .register(registry);
   }
 
+  private void registerOtel(PrometheusRegistry registry) {
+    double[] buckets = {0.01, 0.1, 1, 10};
+
+    Histogram gcDurationHistogram =
+        Histogram.builder(config)
+            .name(JVM_GC_DURATION)
+            .unit(Unit.SECONDS)
+            .help("Duration of JVM garbage collection actions.")
+            .labelNames("jvm.gc.action", "jvm.gc.name", "jvm.gc.cause")
+            .classicUpperBounds(buckets)
+            .register(registry);
+
+    registerNotificationListener(gcDurationHistogram);
+  }
+
+  private void registerNotificationListener(Histogram gcDurationHistogram) {
+    for (GarbageCollectorMXBean gcBean : garbageCollectorBeans) {
+
+      if (!(gcBean instanceof NotificationEmitter)) {
+        continue;
+      }
+
+      ((NotificationEmitter) gcBean)
+          .addNotificationListener(
+              (notification, handback) -> {
+                if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals(
+                    notification.getType())) {
+                  return;
+                }
+
+                GarbageCollectionNotificationInfo info =
+                    GarbageCollectionNotificationInfo.from(
+                        (CompositeData) notification.getUserData());
+
+                observe(gcDurationHistogram, info);
+              },
+              null,
+              null);
+    }
+  }
+
+  private void observe(Histogram gcDurationHistogram, GarbageCollectionNotificationInfo info) {
+    double observedDuration = Unit.millisToSeconds(info.getGcInfo().getDuration());
+    gcDurationHistogram
+        .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause())
+        .observe(observedDuration);
+  }
+
   public static Builder builder() {
     return new Builder(PrometheusProperties.get());
   }
diff --git a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
index 0f928ef34..e8c114db5 100644
--- a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
+++ b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
@@ -1,26 +1,38 @@
 package io.prometheus.metrics.instrumentation.jvm;
 
+import static com.sun.management.GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION;
 import static io.prometheus.metrics.instrumentation.jvm.TestUtil.convertToOpenMetricsFormat;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentCaptor.forClass;
+import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import io.prometheus.metrics.config.MetricsProperties;
+import io.prometheus.metrics.config.PrometheusProperties;
 import io.prometheus.metrics.model.registry.MetricNameFilter;
 import io.prometheus.metrics.model.registry.PrometheusRegistry;
 import io.prometheus.metrics.model.snapshots.MetricSnapshots;
 import java.io.IOException;
 import java.lang.management.GarbageCollectorMXBean;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import javax.management.Notification;
+import javax.management.NotificationEmitter;
+import javax.management.NotificationListener;
+import javax.management.openmbean.*;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+import org.mockito.ArgumentCaptor;
 
 class JvmGarbageCollectorMetricsTest {
 
-  private final GarbageCollectorMXBean mockGcBean1 = Mockito.mock(GarbageCollectorMXBean.class);
-  private final GarbageCollectorMXBean mockGcBean2 = Mockito.mock(GarbageCollectorMXBean.class);
+  private final GarbageCollectorMXBean mockGcBean1 = mock(GarbageCollectorMXBean.class);
+  private final GarbageCollectorMXBean mockGcBean2 = mock(GarbageCollectorMXBean.class);
 
   @BeforeEach
   void setUp() {
@@ -58,7 +70,9 @@ void testGoodCase() throws IOException {
   @Test
   void testIgnoredMetricNotScraped() {
     MetricNameFilter filter =
-        MetricNameFilter.builder().nameMustNotBeEqualTo("jvm_gc_collection_seconds").build();
+        MetricNameFilter.builder()
+            .nameMustNotBeEqualTo("jvm_gc_collection_seconds", "jvm_gc_duration")
+            .build();
 
     PrometheusRegistry registry = new PrometheusRegistry();
     JvmGarbageCollectorMetrics.builder()
@@ -70,4 +84,170 @@ void testIgnoredMetricNotScraped() {
     verify(mockGcBean1, times(0)).getCollectionCount();
     assertThat(snapshots.size()).isZero();
   }
+
+  @Test
+  public void testNonOtelMetricsAbsentWhenUseOtelEnabled() {
+
+    PrometheusRegistry registry = new PrometheusRegistry();
+    PrometheusProperties properties =
+        PrometheusProperties.builder()
+            .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build())
+            .build();
+    JvmGarbageCollectorMetrics.builder(properties)
+        .garbageCollectorBeans(Arrays.asList(mockGcBean1, mockGcBean2))
+        .register(registry);
+    registry.scrape();
+
+    verify(mockGcBean1, times(0)).getCollectionTime();
+    verify(mockGcBean1, times(0)).getCollectionCount();
+  }
+
+  @Test
+  @SuppressWarnings("rawtypes")
+  public void testGCDurationHistogramLabels() throws Exception {
+    GarbageCollectorMXBean mockGcBean =
+        mock(
+            GarbageCollectorMXBean.class,
+            withSettings().extraInterfaces(NotificationEmitter.class));
+    when(mockGcBean.getName()).thenReturn("MyGC");
+
+    PrometheusProperties properties =
+        PrometheusProperties.builder()
+            .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build())
+            .build();
+
+    PrometheusRegistry registry = new PrometheusRegistry();
+    JvmGarbageCollectorMetrics.builder(properties)
+        .garbageCollectorBeans(Collections.singletonList(mockGcBean))
+        .register(registry);
+
+    NotificationListener listener;
+    ArgumentCaptor captor = forClass(NotificationListener.class);
+    verify((NotificationEmitter) mockGcBean)
+        .addNotificationListener(captor.capture(), isNull(), isNull());
+    listener = captor.getValue();
+
+    CompositeData notificationData = getNotificationData();
+
+    Notification notification =
+        new Notification(
+            GARBAGE_COLLECTION_NOTIFICATION, mockGcBean, 1, System.currentTimeMillis(), "gc");
+    notification.setUserData(notificationData);
+
+    listener.handleNotification(notification, null);
+
+    MetricSnapshots snapshots = registry.scrape();
+
+    String expected =
+        """
+    {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.01"} 0
+    {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.1"} 1
+    {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="1.0"} 1
+    {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="10.0"} 1
+    {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="+Inf"} 1
+    {"jvm.gc.duration_seconds_count","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 1
+    {"jvm.gc.duration_seconds_sum","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 0.1
+    """;
+
+    String metrics = convertToOpenMetricsFormat(snapshots);
+
+    assertThat(metrics).contains(expected);
+  }
+
+  private CompositeData getNotificationData() throws OpenDataException {
+    TabularType memoryTabularType = getMemoryTabularType();
+    TabularData memoryBefore = new TabularDataSupport(memoryTabularType);
+    TabularData memoryAfter = new TabularDataSupport(memoryTabularType);
+
+    CompositeData notificationData =
+        getGCNotificationData(memoryTabularType, memoryBefore, memoryAfter);
+    return notificationData;
+  }
+
+  private CompositeData getGCNotificationData(
+      TabularType memoryTabularType, TabularData memoryBefore, TabularData memoryAfter)
+      throws OpenDataException {
+    CompositeType gcInfoType = getGCInfoCompositeType(memoryTabularType);
+
+    Map gcInfoMap = getGcInfoMap(memoryBefore, memoryAfter);
+
+    CompositeData notificationData = getGcNotificationData(gcInfoType, gcInfoMap);
+    return notificationData;
+  }
+
+  private Map getGcInfoMap(TabularData memoryBefore, TabularData memoryAfter) {
+    Map gcInfoMap = new HashMap<>();
+    gcInfoMap.put("id", 0L);
+    gcInfoMap.put("startTime", 100L);
+    gcInfoMap.put("endTime", 200L);
+    gcInfoMap.put("duration", 100L);
+    gcInfoMap.put("memoryUsageBeforeGc", memoryBefore);
+    gcInfoMap.put("memoryUsageAfterGc", memoryAfter);
+    return gcInfoMap;
+  }
+
+  private CompositeType getGCInfoCompositeType(TabularType memoryTabularType)
+      throws OpenDataException {
+    return new CompositeType(
+        "sun.management.BaseGcInfoCompositeType",
+        "gcInfo",
+        new String[] {
+          "id", "startTime", "endTime", "duration", "memoryUsageBeforeGc", "memoryUsageAfterGc"
+        },
+        new String[] {
+          "id", "startTime", "endTime", "duration", "memoryUsageBeforeGc", "memoryUsageAfterGc"
+        },
+        new OpenType[] {
+          SimpleType.LONG,
+          SimpleType.LONG,
+          SimpleType.LONG,
+          SimpleType.LONG,
+          memoryTabularType,
+          memoryTabularType
+        });
+  }
+
+  private TabularType getMemoryTabularType() throws OpenDataException {
+    CompositeType memoryUsageType =
+        new CompositeType(
+            "java.lang.management.MemoryUsage",
+            "MemoryUsage",
+            new String[] {"init", "used", "committed", "max"},
+            new String[] {"init", "used", "committed", "max"},
+            new OpenType[] {SimpleType.LONG, SimpleType.LONG, SimpleType.LONG, SimpleType.LONG});
+
+    CompositeType memoryUsageEntryType =
+        new CompositeType(
+            "memoryUsageEntry",
+            "memoryUsageEntry",
+            new String[] {"key", "value"},
+            new String[] {"key", "value"},
+            new OpenType[] {SimpleType.STRING, memoryUsageType});
+
+    return new TabularType(
+        "memoryUsageTabular", "memoryUsageTabular", memoryUsageEntryType, new String[] {"key"});
+  }
+
+  private static CompositeData getGcNotificationData(
+      CompositeType gcInfoType, Map gcInfoMap) throws OpenDataException {
+    CompositeData gcInfoData = new CompositeDataSupport(gcInfoType, gcInfoMap);
+
+    CompositeType notificationType =
+        new CompositeType(
+            "sun.management.BaseGarbageCollectionNotifInfoCompositeType",
+            "GarbageCollectionNotificationInfo",
+            new String[] {"gcAction", "gcName", "gcCause", "gcInfo"},
+            new String[] {"gcAction", "gcName", "gcCause", "gcInfo"},
+            new OpenType[] {
+              SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType
+            });
+
+    Map notifMap = new HashMap<>();
+    notifMap.put("gcAction", "end of minor GC");
+    notifMap.put("gcName", "MyGC");
+    notifMap.put("gcCause", "testCause");
+    notifMap.put("gcInfo", gcInfoData);
+
+    return new CompositeDataSupport(notificationType, notifMap);
+  }
 }