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",