From 1279280fa2b31e3db3ff4eaefc3f7bf5b8702cd4 Mon Sep 17 00:00:00 2001 From: jack-berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 24 Aug 2022 12:35:28 -0500 Subject: [PATCH] Move opentelemetry-sdk-extension-jfr-events (#421) --- jfr-events/README.md | 23 ++++ jfr-events/build.gradle.kts | 28 ++++ .../jfr/JfrContextStorageWrapper.java | 40 ++++++ .../sdk/extension/jfr/JfrSpanProcessor.java | 65 +++++++++ .../sdk/extension/jfr/ScopeEvent.java | 40 ++++++ .../sdk/extension/jfr/SpanEvent.java | 52 +++++++ .../sdk/extension/jfr/package-info.java | 14 ++ .../extension/jfr/JfrSpanProcessorTest.java | 127 ++++++++++++++++++ settings.gradle.kts | 1 + 9 files changed, 390 insertions(+) create mode 100644 jfr-events/README.md create mode 100644 jfr-events/build.gradle.kts create mode 100644 jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java create mode 100644 jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java create mode 100644 jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java create mode 100644 jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java create mode 100644 jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java create mode 100644 jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java diff --git a/jfr-events/README.md b/jfr-events/README.md new file mode 100644 index 00000000..f50d85f6 --- /dev/null +++ b/jfr-events/README.md @@ -0,0 +1,23 @@ +# OpenTelemetry Java Flight Recorder (JFR) Events + +[![Javadocs][javadoc-image]][javadoc-url] + +Create JFR events that can be recorded and viewed in Java Mission Control (JMC). +* Creates Open Telemetry Tracing/Span events for spans + * The thread and stracktrace will be of the thead ending the span which might be different than the thread creating the span. + * Has the fields + * Operation Name + * Trace ID + * Parent Span ID + * Span ID +* Creates Open Telemetry Tracing/Scope events for scopes + * Thread will match the thread the scope was active in and the stacktrace will be when scope was closed + * Multiple scopes might be collected for a single span + * Has the fields + * Trace ID + * Span ID +* Supports the Open Source version of JFR in Java 11. + * Might support back port to OpenJDK 8, but not tested and classes are built with JDK 11 bytecode. + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-extension-jfr-events.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-extension-jfr-events diff --git a/jfr-events/build.gradle.kts b/jfr-events/build.gradle.kts new file mode 100644 index 00000000..b196a35d --- /dev/null +++ b/jfr-events/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry JFR Events" + +dependencies { + implementation("io.opentelemetry:opentelemetry-sdk") +} + +tasks { + withType(JavaCompile::class) { + options.release.set(11) + } + + test { + val testJavaVersion: String? by project + if (testJavaVersion == "8") { + enabled = false + } + + // Disabled due to https://bugs.openjdk.java.net/browse/JDK-8245283 + configure { + enabled = false + } + } +} diff --git a/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java new file mode 100644 index 00000000..b9d587dd --- /dev/null +++ b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageWrapper.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import javax.annotation.Nullable; + +public final class JfrContextStorageWrapper implements ContextStorage { + + private final ContextStorage wrapped; + + public JfrContextStorageWrapper(ContextStorage wrapped) { + this.wrapped = wrapped; + } + + @Override + public Scope attach(Context toAttach) { + Scope scope = wrapped.attach(toAttach); + ScopeEvent event = new ScopeEvent(Span.fromContext(toAttach).getSpanContext()); + event.begin(); + return () -> { + if (event.shouldCommit()) { + event.commit(); + } + scope.close(); + }; + } + + @Override + @Nullable + public Context current() { + return wrapped.current(); + } +} diff --git a/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java new file mode 100644 index 00000000..e3e9e914 --- /dev/null +++ b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * Span processor to create new JFR events for the Span as they are started, and commit on end. + * + *

NOTE: The JfrSpanProcessor measures the timing of spans, avoid if possible to wrap it with any + * other SpanProcessor which may affect timings. When possible, register it first before any other + * processors to allow the most accurate measurements. + */ +public final class JfrSpanProcessor implements SpanProcessor { + + private final WeakConcurrentMap spanEvents = + new WeakConcurrentMap.WithInlinedExpunction<>(); + + private volatile boolean closed; + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + if (closed) { + return; + } + if (span.getSpanContext().isValid()) { + SpanEvent event = new SpanEvent(span.toSpanData()); + event.begin(); + spanEvents.put(span.getSpanContext(), event); + } + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan rs) { + SpanEvent event = spanEvents.remove(rs.getSpanContext()); + if (!closed && event != null && event.shouldCommit()) { + event.commit(); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + closed = true; + return CompletableResultCode.ofSuccess(); + } +} diff --git a/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java new file mode 100644 index 00000000..cf40bb4e --- /dev/null +++ b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.api.trace.SpanContext; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; +import jdk.jfr.Name; + +@Name("io.opentelemetry.context.Scope") +@Label("Scope") +@Category("Open Telemetry Tracing") +@Description( + "Open Telemetry trace event corresponding to the span currently " + + "in scope/active on this thread.") +class ScopeEvent extends Event { + + private final String traceId; + private final String spanId; + + ScopeEvent(SpanContext spanContext) { + this.traceId = spanContext.getTraceId(); + this.spanId = spanContext.getSpanId(); + } + + @Label("Trace Id") + public String getTraceId() { + return traceId; + } + + @Label("Span Id") + public String getSpanId() { + return spanId; + } +} diff --git a/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java new file mode 100644 index 00000000..1e239b92 --- /dev/null +++ b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import io.opentelemetry.sdk.trace.data.SpanData; +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; +import jdk.jfr.Name; + +@Label("Span") +@Name("io.opentelemetry.trace.Span") +@Category("Open Telemetry Tracing") +@Description("Open Telemetry trace event corresponding to a span.") +class SpanEvent extends Event { + + private final String operationName; + private final String traceId; + private final String spanId; + private final String parentId; + + SpanEvent(SpanData spanData) { + this.operationName = spanData.getName(); + this.traceId = spanData.getTraceId(); + this.spanId = spanData.getSpanId(); + this.parentId = spanData.getParentSpanId(); + } + + @Label("Operation Name") + public String getOperationName() { + return operationName; + } + + @Label("Trace Id") + public String getTraceId() { + return traceId; + } + + @Label("Span Id") + public String getSpanId() { + return spanId; + } + + @Label("Parent Id") + public String getParentId() { + return parentId; + } +} diff --git a/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java new file mode 100644 index 00000000..ac7889ff --- /dev/null +++ b/jfr-events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Capture Spans and Scopes as events in JFR recordings. + * + * @see io.opentelemetry.sdk.extension.jfr.JfrSpanProcessor + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.sdk.extension.jfr; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java b/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java new file mode 100644 index 00000000..49902d59 --- /dev/null +++ b/jfr-events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java @@ -0,0 +1,127 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.jfr; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class JfrSpanProcessorTest { + + private static final String OPERATION_NAME = "Test Span"; + + private SdkTracerProvider sdkTracerProvider; + private Tracer tracer; + + @BeforeEach + void setUp() { + sdkTracerProvider = + SdkTracerProvider.builder().addSpanProcessor(new JfrSpanProcessor()).build(); + tracer = sdkTracerProvider.get("JfrSpanProcessorTest"); + } + + @AfterEach + void tearDown() { + sdkTracerProvider.shutdown(); + } + + static { + ContextStorage.addWrapper(JfrContextStorageWrapper::new); + } + + /** + * Test basic single span. + * + * @throws java.io.IOException on io error + */ + @Test + public void basicSpan() throws IOException { + Path output = Files.createTempFile("test-basic-span", ".jfr"); + + try { + Recording recording = new Recording(); + recording.start(); + Span span; + + try (recording) { + + span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan(); + span.end(); + + recording.dump(output); + } + + List events = RecordingFile.readAllEvents(output); + assertThat(events).hasSize(1); + assertThat(events) + .extracting(e -> e.getValue("traceId")) + .isEqualTo(span.getSpanContext().getTraceId()); + assertThat(events) + .extracting(e -> e.getValue("spanId")) + .isEqualTo(span.getSpanContext().getSpanId()); + assertThat(events).extracting(e -> e.getValue("operationName")).isEqualTo(OPERATION_NAME); + } finally { + Files.delete(output); + } + } + + /** + * Test basic single span with a scope. + * + * @throws java.io.IOException on io error + * @throws java.lang.InterruptedException interrupted sleep + */ + @Test + public void basicSpanWithScope() throws IOException, InterruptedException { + Path output = Files.createTempFile("test-basic-span-with-scope", ".jfr"); + + try { + Recording recording = new Recording(); + recording.start(); + Span span; + + try (recording) { + span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan(); + try (Scope s = span.makeCurrent()) { + Thread.sleep(10); + } + span.end(); + + recording.dump(output); + } + + List events = RecordingFile.readAllEvents(output); + assertThat(events).hasSize(2); + assertThat(events) + .extracting(e -> e.getValue("traceId")) + .isEqualTo(span.getSpanContext().getTraceId()); + assertThat(events) + .extracting(e -> e.getValue("spanId")) + .isEqualTo(span.getSpanContext().getSpanId()); + assertThat(events) + .filteredOn(e -> "Span".equals(e.getEventType().getLabel())) + .extracting(e -> e.getValue("operationName")) + .isEqualTo(OPERATION_NAME); + + } finally { + Files.delete(output); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index f1d93936..c1398323 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,6 +42,7 @@ include(":aws-xray") include(":consistent-sampling") include(":dependencyManagement") include(":example") +include(":jfr-events") include(":jfr-streaming") include(":micrometer-meter-provider") include(":jmx-metrics")