diff --git a/sdk_extensions/jfr_events/README.md b/sdk_extensions/jfr_events/README.md
new file mode 100644
index 00000000000..ccf15c981fa
--- /dev/null
+++ b/sdk_extensions/jfr_events/README.md
@@ -0,0 +1,4 @@
+OpenTelemetry SDK Extension JFR Events
+======================================================
+
+* Java 11 compatible.
diff --git a/sdk_extensions/jfr_events/build.gradle b/sdk_extensions/jfr_events/build.gradle
new file mode 100644
index 00000000000..3ba659157af
--- /dev/null
+++ b/sdk_extensions/jfr_events/build.gradle
@@ -0,0 +1,19 @@
+plugins {
+ id 'java'
+
+ id "ru.vyarus.animalsniffer"
+}
+
+description = 'OpenTelemetry SDK Extension JFR'
+ext.moduleName = 'io.opentelemetry.sdk.extension.jfr'
+
+dependencies {
+ implementation project(':opentelemetry-api'),
+ project(':opentelemetry-sdk')
+
+ signature "org.codehaus.mojo.signature:java18:1.0@signature"
+}
+
+tasks.withType(JavaCompile) {
+ it.options.release = 11
+}
diff --git a/sdk_extensions/jfr_events/nbproject/project.properties b/sdk_extensions/jfr_events/nbproject/project.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageProvider.java b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageProvider.java
new file mode 100644
index 00000000000..9b9f2f81f34
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrContextStorageProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.opentelemetry.sdk.extension.jfr;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.ContextStorage;
+import io.opentelemetry.context.ContextStorageProvider;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.trace.Span;
+
+public class JfrContextStorageProvider implements ContextStorageProvider {
+
+ @Override
+ public ContextStorage get() {
+ ContextStorage parentStorage = ContextStorage.get();
+ return new ContextStorage() {
+ @Override
+ public Scope attach(Context toAttach) {
+ Scope scope = parentStorage.attach(toAttach);
+ ScopeEvent event = new ScopeEvent(Span.fromContext(toAttach).getSpanContext());
+ event.begin();
+ return () -> {
+ event.commit();
+ scope.close();
+ };
+ }
+
+ @Override
+ public Context current() {
+ return parentStorage.current();
+ }
+ };
+ }
+}
diff --git a/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java
new file mode 100644
index 00000000000..d657adecc57
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessor.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.opentelemetry.sdk.extension.jfr;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.trace.ReadWriteSpan;
+import static java.util.Objects.nonNull;
+
+import io.opentelemetry.sdk.trace.ReadableSpan;
+import io.opentelemetry.sdk.trace.SpanProcessor;
+import io.opentelemetry.trace.SpanContext;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Span processor to create new JFR events for the Span as they are started, and
+ * commit on end.
+ *
+ *
+ * NOTE: JfrSpanProcessor must be running synchronously to ensure that duration
+ * is correctly captured.
+ */
+public class JfrSpanProcessor implements SpanProcessor {
+
+ private final Map spanEvents = new ConcurrentHashMap<>();
+
+ @Override
+ public void onStart(ReadWriteSpan span, Context parentContext) {
+ 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 (nonNull(event) && event.shouldCommit()) {
+ event.commit();
+ }
+ }
+
+ @Override
+ public boolean isEndRequired() {
+ return true;
+ }
+
+ @Override
+ public CompletableResultCode shutdown() {
+ spanEvents.forEach((id, event) -> event.commit());
+ spanEvents.clear();
+ return CompletableResultCode.ofSuccess();
+ }
+
+ @Override
+ public CompletableResultCode forceFlush() {
+ return CompletableResultCode.ofSuccess();
+ }
+}
diff --git a/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java
new file mode 100644
index 00000000000..a544ab2ab58
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/ScopeEvent.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.sdk.extension.jfr;
+
+import io.opentelemetry.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 {
+
+ @Label("Trace Id")
+ private final String traceId;
+
+ @Label("Span Id")
+ private final String spanId;
+
+ ScopeEvent(SpanContext spanContext) {
+ this.traceId = spanContext.getTraceIdAsHexString();
+ this.spanId = spanContext.getSpanIdAsHexString();
+ }
+
+ public String getTraceId() {
+ return traceId;
+ }
+
+ public String getSpanId() {
+ return spanId;
+ }
+}
diff --git a/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java
new file mode 100644
index 00000000000..9bd7121a8a7
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/SpanEvent.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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 {
+
+ SpanEvent(SpanData spanData) {
+ this.operationName = spanData.getName();
+ this.traceId = spanData.getTraceId();
+ this.spanId = spanData.getSpanId();
+ this.parentId = spanData.getParentSpanId();
+ }
+
+ @Label("Operation Name")
+ private final String operationName;
+
+ @Label("Trace Id")
+ private final String traceId;
+
+ @Label("Span Id")
+ private final String spanId;
+
+ @Label("Parent Id")
+ private final String parentId;
+
+ public String getOperationName() {
+ return operationName;
+ }
+
+ public String getTraceId() {
+ return traceId;
+ }
+
+ public String getSpanId() {
+ return spanId;
+ }
+
+ public String getParentId() {
+ return parentId;
+ }
+}
diff --git a/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java b/sdk_extensions/jfr_events/src/main/java/io/opentelemetry/sdk/extension/jfr/package-info.java
new file mode 100644
index 00000000000..ac7889ff0e5
--- /dev/null
+++ b/sdk_extensions/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/sdk_extensions/jfr_events/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider b/sdk_extensions/jfr_events/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider
new file mode 100644
index 00000000000..859f599db4a
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/main/resources/META-INF/services/io.opentelemetry.context.ContextStorageProvider
@@ -0,0 +1 @@
+io.opentelemetry.sdk.extension.jfr.JfrContextStorageProvider
diff --git a/sdk_extensions/jfr_events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java b/sdk_extensions/jfr_events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java
new file mode 100644
index 00000000000..1b98582a123
--- /dev/null
+++ b/sdk_extensions/jfr_events/src/test/java/io/opentelemetry/sdk/extension/jfr/JfrSpanProcessorTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2020, OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opentelemetry.sdk.extension.jfr;
+
+import io.opentelemetry.OpenTelemetry;
+import static org.junit.Assert.assertEquals;
+
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.trace.Tracer;
+import io.opentelemetry.trace.Span;
+import io.opentelemetry.trace.TracingContextUtils;
+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.Test;
+
+public class JfrSpanProcessorTest {
+
+ private static final String OPERATION_NAME = "Test Span";
+ private final Tracer tracer;
+
+ /** Simple test to validate JFR events for Span and Scope. */
+ public JfrSpanProcessorTest() {
+ tracer = OpenTelemetry.getGlobalTracer("JfrSpanProcessorTest");
+ OpenTelemetrySdk.getTracerManagement().addSpanProcessor(new JfrSpanProcessor());
+ }
+
+ /**
+ * 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);
+ assertEquals(1, events.size());
+ events.stream()
+ .forEach(
+ e -> {
+ assertEquals(span.getSpanContext().getTraceIdAsHexString(), e.getValue("traceId"));
+ assertEquals(span.getSpanContext().getSpanIdAsHexString(), e.getValue("spanId"));
+ assertEquals(OPERATION_NAME, e.getValue("operationName"));
+ });
+
+ } finally {
+ Files.delete(output);
+ }
+ }
+
+ /**
+ * Test basic single span with a scope.
+ *
+ * @throws java.io.IOException on io error
+ */
+ @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 = TracingContextUtils.currentContextWith(span)) {
+ Thread.sleep(10);
+ }
+ span.end();
+
+ recording.dump(output);
+ }
+
+ List events = RecordingFile.readAllEvents(output);
+ assertEquals(2, events.size());
+ events.stream()
+ .forEach(
+ e -> {
+ assertEquals(span.getSpanContext().getTraceIdAsHexString(), e.getValue("traceId"));
+ assertEquals(span.getSpanContext().getSpanIdAsHexString(), e.getValue("spanId"));
+ if ("Span".equals(e.getEventType().getLabel())) {
+ assertEquals(OPERATION_NAME, e.getValue("operationName"));
+ }
+ });
+
+ } finally {
+ Files.delete(output);
+ }
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 04c9c9c7cef..0a5ee72a87a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -51,6 +51,7 @@ include ":opentelemetry-all",
":opentelemetry-sdk-extension-testbed",
":opentelemetry-sdk-extension-tracing-incubator",
":opentelemetry-sdk-extension-jaeger-remote-sampler",
+ ":opentelemetry-sdk-extension-jfr-events",
":opentelemetry-sdk-extension-zpages",
":opentelemetry-bom",
":opentelemetry-testing-internal",