Skip to content

Commit a02c0ed

Browse files
kittylystanuraaga
andauthored
Add agent to bridge JFR Streaming into metrics (#115)
* Initial commit * Add to main build * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Initial commit * Add to main build * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Initial commit * Add to main build * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Initial commit * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Get testcontainers imports set correctly * BROKE: Prepare for rebase * Finish migrating to 1.7.0 * BROKE: Test failing with NoSuchMethodError * BROKE Tidy up docs * FAIL: Pushing so other folks can have a look * Unit tests are green * Initial commit * Add to main build * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Initial commit * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Get testcontainers imports set correctly * BROKE: Prepare for rebase * Initial commit * Add to main build * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Initial commit * Send data as deltas, get it into a sample backend (New Relic) * Debug build issue * Fix constants * Package rename * Finish migrating to 1.7.0 * BROKE: Test failing with NoSuchMethodError * BROKE Tidy up docs * FAIL: Pushing so other folks can have a look * Unit tests are green * Fix broken build.gradle.kts * Update jfr-streaming/build.gradle.kts Co-authored-by: Anuraag Agrawal <[email protected]> * BROKE: Versions * Fix Javadoc task to version 17 as well * Remove un-needed version * Fix AttributeKey * Work around Spotless not supporting type patterns yet * Spotless Apply finaly works * Real javaagent * Provide programmatic endpoint, don't require test to depend on shadowJar * Temporarily remove agent to land this faster * Package rearrage * Update jfr-streaming/build.gradle.kts Co-authored-by: Anuraag Agrawal <[email protected]> * Update jfr-streaming/build.gradle.kts Co-authored-by: Anuraag Agrawal <[email protected]> * Update jfr-streaming/build.gradle.kts Co-authored-by: Anuraag Agrawal <[email protected]> * Encapsulate more constants * Minor tidyups * Update jfr-streaming/build.gradle.kts Co-authored-by: Anuraag Agrawal <[email protected]> * Remove shadow Jar * Remove unnecessary dependencies from build * Change names and tidy up * Basic Javadoc * Fix descriptions Co-authored-by: Anuraag Agrawal <[email protected]>
1 parent 7184f00 commit a02c0ed

27 files changed

+1196
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Compiled class file
22
*.class
33

4+
# No jars
5+
*.jar
6+
47
# Log file
58
*.log
69

jfr-streaming/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# jfrs-otlp
2+
JFR Streaming to OTLP Bridge
3+
4+
* Java 17 only.
5+
6+
* Build it with `./gradlew :jfr-streaming:build`
7+
8+
The main entry point is the `JfrMetrics` class in the package `io.opentelemetry.contrib.jfr.metrics`

jfr-streaming/build.gradle.kts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
}
4+
5+
dependencies {
6+
implementation("io.opentelemetry:opentelemetry-api-metrics")
7+
8+
testImplementation("io.opentelemetry:opentelemetry-sdk-metrics-testing")
9+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
10+
}
11+
12+
tasks {
13+
withType(JavaCompile::class) {
14+
options.release.set(17)
15+
}
16+
17+
withType<Javadoc>().configureEach {
18+
with(options as StandardJavadocDocletOptions) {
19+
source = "17"
20+
}
21+
}
22+
23+
test {
24+
useJUnitPlatform()
25+
}
26+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics;
7+
8+
import io.opentelemetry.api.metrics.MeterProvider;
9+
import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler;
10+
import io.opentelemetry.contrib.jfr.metrics.internal.ThreadGrouper;
11+
import io.opentelemetry.contrib.jfr.metrics.internal.container.ContainerConfigurationHandler;
12+
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.ContextSwitchRateHandler;
13+
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.LongLockHandler;
14+
import io.opentelemetry.contrib.jfr.metrics.internal.cpu.OverallCPULoadHandler;
15+
import io.opentelemetry.contrib.jfr.metrics.internal.memory.G1GarbageCollectionHandler;
16+
import io.opentelemetry.contrib.jfr.metrics.internal.memory.GCHeapSummaryHandler;
17+
import io.opentelemetry.contrib.jfr.metrics.internal.memory.ObjectAllocationInNewTLABHandler;
18+
import io.opentelemetry.contrib.jfr.metrics.internal.memory.ObjectAllocationOutsideTLABHandler;
19+
import io.opentelemetry.contrib.jfr.metrics.internal.network.NetworkReadHandler;
20+
import io.opentelemetry.contrib.jfr.metrics.internal.network.NetworkWriteHandler;
21+
import java.util.*;
22+
import java.util.stream.Stream;
23+
24+
final class HandlerRegistry {
25+
private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.6.1";
26+
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.contrib.jfr";
27+
private static final String INSTRUMENTATION_VERSION = "1.7.0-SNAPSHOT";
28+
29+
private final List<RecordedEventHandler> mappers;
30+
31+
private HandlerRegistry(List<? extends RecordedEventHandler> mappers) {
32+
this.mappers = new ArrayList<>(mappers);
33+
}
34+
35+
static HandlerRegistry createDefault(MeterProvider meterProvider) {
36+
var otelMeter = meterProvider.get(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION, null);
37+
38+
var grouper = new ThreadGrouper();
39+
var filtered =
40+
List.of(
41+
new ObjectAllocationInNewTLABHandler(otelMeter, grouper),
42+
new ObjectAllocationOutsideTLABHandler(otelMeter, grouper),
43+
new NetworkReadHandler(otelMeter, grouper),
44+
new NetworkWriteHandler(otelMeter, grouper),
45+
new G1GarbageCollectionHandler(otelMeter),
46+
new GCHeapSummaryHandler(otelMeter),
47+
new ContextSwitchRateHandler(otelMeter),
48+
new OverallCPULoadHandler(otelMeter),
49+
new ContainerConfigurationHandler(otelMeter),
50+
new LongLockHandler(otelMeter, grouper));
51+
filtered.forEach(RecordedEventHandler::init);
52+
53+
return new HandlerRegistry(filtered);
54+
}
55+
56+
/** @return a stream of all entries in this registry. */
57+
Stream<RecordedEventHandler> all() {
58+
return mappers.stream();
59+
}
60+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics;
7+
8+
import io.opentelemetry.api.metrics.MeterProvider;
9+
import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler;
10+
import java.util.concurrent.Executors;
11+
import java.util.function.Consumer;
12+
import java.util.logging.Level;
13+
import java.util.logging.Logger;
14+
import jdk.jfr.EventSettings;
15+
import jdk.jfr.consumer.RecordingStream;
16+
17+
/** The entry point class for the JFR-over-OpenTelemetry support. */
18+
public final class JfrMetrics {
19+
private JfrMetrics() {}
20+
21+
private static final Logger logger = Logger.getLogger(JfrMetrics.class.getName());
22+
23+
/**
24+
* Enables and starts a JFR recording stream on a background thread. The thread converts a subset
25+
* of JFR events to OpenTelemetry metrics.
26+
*
27+
* @param meterProvider - the OpenTelemetry metric provider that will harvest the generated
28+
* metrics.
29+
*/
30+
public static void enable(MeterProvider meterProvider) {
31+
var jfrMonitorService = Executors.newSingleThreadExecutor();
32+
var toMetricRegistry = HandlerRegistry.createDefault(meterProvider);
33+
34+
jfrMonitorService.submit(
35+
() -> {
36+
try (var recordingStream = new RecordingStream()) {
37+
var enableMappedEvent = eventEnablerFor(recordingStream);
38+
toMetricRegistry.all().forEach(enableMappedEvent);
39+
recordingStream.setReuse(false);
40+
logger.log(Level.FINE, "Starting recording stream...");
41+
recordingStream.start(); // run forever
42+
}
43+
});
44+
}
45+
46+
private static Consumer<RecordedEventHandler> eventEnablerFor(RecordingStream recordingStream) {
47+
return handler -> {
48+
EventSettings eventSettings = recordingStream.enable(handler.getEventName());
49+
handler.getPollingDuration().ifPresent(eventSettings::withPeriod);
50+
handler.getThreshold().ifPresent(eventSettings::withThreshold);
51+
recordingStream.onEvent(handler.getEventName(), handler);
52+
};
53+
}
54+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics.internal;
7+
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import java.util.Optional;
11+
import jdk.jfr.consumer.RecordedEvent;
12+
13+
public abstract class AbstractThreadDispatchingHandler implements RecordedEventHandler {
14+
// Will need pruning code for fast-cycling thread frameworks to prevent memory leaks
15+
protected final Map<String, RecordedEventHandler> perThread = new HashMap<>();
16+
protected final ThreadGrouper grouper;
17+
18+
public AbstractThreadDispatchingHandler(ThreadGrouper grouper) {
19+
this.grouper = grouper;
20+
}
21+
22+
public void reset() {
23+
perThread.clear();
24+
}
25+
26+
public abstract String getEventName();
27+
28+
public abstract RecordedEventHandler createPerThreadSummarizer(String threadName);
29+
30+
@Override
31+
public void accept(RecordedEvent ev) {
32+
final Optional<String> possibleGroupedThreadName = grouper.groupedName(ev);
33+
possibleGroupedThreadName.ifPresent(
34+
groupedThreadName -> {
35+
perThread.computeIfAbsent(groupedThreadName, name -> createPerThreadSummarizer(name));
36+
perThread.get(groupedThreadName).accept(ev);
37+
});
38+
}
39+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics.internal;
7+
8+
import io.opentelemetry.api.common.AttributeKey;
9+
10+
public final class Constants {
11+
private Constants() {}
12+
13+
public static final String ONE = "1";
14+
public static final String KILOBYTES = "KB";
15+
public static final String MILLISECONDS = "ms";
16+
public static final String PERCENTAGE = "%age";
17+
public static final String READ = "read";
18+
public static final String WRITE = "write";
19+
public static final AttributeKey<String> ATTR_THREAD_NAME = AttributeKey.stringKey("thread.name");
20+
public static final AttributeKey<String> ATTR_ARENA_NAME = AttributeKey.stringKey("arena");
21+
public static final AttributeKey<String> ATTR_NETWORK_MODE = AttributeKey.stringKey("mode");
22+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics.internal;
7+
8+
import java.time.Duration;
9+
import java.util.Optional;
10+
import java.util.function.Consumer;
11+
import java.util.function.Predicate;
12+
import jdk.jfr.consumer.RecordedEvent;
13+
14+
/** Convenience/Tag interface for defining how JFR events should turn into metrics. */
15+
public interface RecordedEventHandler extends Consumer<RecordedEvent>, Predicate<RecordedEvent> {
16+
17+
/**
18+
* JFR event name (e.g. jdk.ObjectAllocationInNewTLAB)
19+
*
20+
* @return String representation of JFR event name
21+
*/
22+
String getEventName();
23+
24+
/**
25+
* Test to see if this event is interesting to this mapper
26+
*
27+
* @param event - event instance to see if we're interested
28+
* @return true if event is interesting, false otherwise
29+
*/
30+
default boolean test(RecordedEvent event) {
31+
return event.getEventType().getName().equalsIgnoreCase(getEventName());
32+
}
33+
34+
/**
35+
* Optionally returns a polling duration for JFR events, if present
36+
*
37+
* @return {@link Optional} of {@link Duration} representing polling duration; empty {@link
38+
* Optional} if no polling
39+
*/
40+
default Optional<Duration> getPollingDuration() {
41+
return Optional.empty();
42+
}
43+
44+
/**
45+
* Optionally returns a threshold length for JFR events, if present
46+
*
47+
* @return {@link Optional} of {@link Duration} representing threshold; empty {@link Optional} if
48+
* no threshold
49+
*/
50+
default Optional<Duration> getThreshold() {
51+
return Optional.empty();
52+
}
53+
54+
/**
55+
* Initialize the handler. Default implementation is a no-op
56+
*
57+
* @return
58+
*/
59+
default RecordedEventHandler init() {
60+
return this;
61+
}
62+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics.internal;
7+
8+
import java.util.Optional;
9+
import jdk.jfr.consumer.RecordedEvent;
10+
import jdk.jfr.consumer.RecordedThread;
11+
12+
public class ThreadGrouper {
13+
// FIXME doesn't actually do any grouping, but should be safe for now
14+
public Optional<String> groupedName(RecordedEvent ev) {
15+
Object thisField = ev.getValue("eventThread");
16+
if (thisField != null && thisField instanceof RecordedThread) {
17+
return Optional.of(((RecordedThread) thisField).getJavaName());
18+
}
19+
return Optional.empty();
20+
}
21+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jfr.metrics.internal.container;
7+
8+
import static io.opentelemetry.contrib.jfr.metrics.internal.Constants.ONE;
9+
10+
import io.opentelemetry.api.metrics.Meter;
11+
import io.opentelemetry.contrib.jfr.metrics.internal.RecordedEventHandler;
12+
import jdk.jfr.consumer.RecordedEvent;
13+
14+
public final class ContainerConfigurationHandler implements RecordedEventHandler {
15+
private static final String EVENT_NAME = "jdk.ContainerConfiguration";
16+
private static final String METRIC_NAME = "runtime.jvm.cpu.limit";
17+
18+
private static final String EFFECTIVE_CPU_COUNT = "effectiveCpuCount";
19+
20+
private final Meter otelMeter;
21+
private volatile long value = 0L;
22+
23+
public ContainerConfigurationHandler(Meter otelMeter) {
24+
this.otelMeter = otelMeter;
25+
}
26+
27+
public ContainerConfigurationHandler init() {
28+
otelMeter
29+
.upDownCounterBuilder(METRIC_NAME)
30+
.ofDoubles()
31+
.setUnit(ONE)
32+
.buildWithCallback(codm -> codm.observe(value));
33+
34+
return this;
35+
}
36+
37+
@Override
38+
public String getEventName() {
39+
return EVENT_NAME;
40+
}
41+
42+
@Override
43+
public void accept(RecordedEvent ev) {
44+
if (ev.hasField(EFFECTIVE_CPU_COUNT)) {
45+
value = ev.getLong(EFFECTIVE_CPU_COUNT);
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)