Skip to content

Commit 1279280

Browse files
authored
Move opentelemetry-sdk-extension-jfr-events (#421)
1 parent a578088 commit 1279280

File tree

9 files changed

+390
-0
lines changed

9 files changed

+390
-0
lines changed

jfr-events/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenTelemetry Java Flight Recorder (JFR) Events
2+
3+
[![Javadocs][javadoc-image]][javadoc-url]
4+
5+
Create JFR events that can be recorded and viewed in Java Mission Control (JMC).
6+
* Creates Open Telemetry Tracing/Span events for spans
7+
* The thread and stracktrace will be of the thead ending the span which might be different than the thread creating the span.
8+
* Has the fields
9+
* Operation Name
10+
* Trace ID
11+
* Parent Span ID
12+
* Span ID
13+
* Creates Open Telemetry Tracing/Scope events for scopes
14+
* Thread will match the thread the scope was active in and the stacktrace will be when scope was closed
15+
* Multiple scopes might be collected for a single span
16+
* Has the fields
17+
* Trace ID
18+
* Span ID
19+
* Supports the Open Source version of JFR in Java 11.
20+
* Might support back port to OpenJDK 8, but not tested and classes are built with JDK 11 bytecode.
21+
22+
[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-extension-jfr-events.svg
23+
[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-extension-jfr-events

jfr-events/build.gradle.kts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
id("otel.publish-conventions")
4+
}
5+
6+
description = "OpenTelemetry JFR Events"
7+
8+
dependencies {
9+
implementation("io.opentelemetry:opentelemetry-sdk")
10+
}
11+
12+
tasks {
13+
withType(JavaCompile::class) {
14+
options.release.set(11)
15+
}
16+
17+
test {
18+
val testJavaVersion: String? by project
19+
if (testJavaVersion == "8") {
20+
enabled = false
21+
}
22+
23+
// Disabled due to https://bugs.openjdk.java.net/browse/JDK-8245283
24+
configure<JacocoTaskExtension> {
25+
enabled = false
26+
}
27+
}
28+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.jfr;
7+
8+
import io.opentelemetry.api.trace.Span;
9+
import io.opentelemetry.context.Context;
10+
import io.opentelemetry.context.ContextStorage;
11+
import io.opentelemetry.context.Scope;
12+
import javax.annotation.Nullable;
13+
14+
public final class JfrContextStorageWrapper implements ContextStorage {
15+
16+
private final ContextStorage wrapped;
17+
18+
public JfrContextStorageWrapper(ContextStorage wrapped) {
19+
this.wrapped = wrapped;
20+
}
21+
22+
@Override
23+
public Scope attach(Context toAttach) {
24+
Scope scope = wrapped.attach(toAttach);
25+
ScopeEvent event = new ScopeEvent(Span.fromContext(toAttach).getSpanContext());
26+
event.begin();
27+
return () -> {
28+
if (event.shouldCommit()) {
29+
event.commit();
30+
}
31+
scope.close();
32+
};
33+
}
34+
35+
@Override
36+
@Nullable
37+
public Context current() {
38+
return wrapped.current();
39+
}
40+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.jfr;
7+
8+
import io.opentelemetry.api.trace.SpanContext;
9+
import io.opentelemetry.context.Context;
10+
import io.opentelemetry.context.internal.shaded.WeakConcurrentMap;
11+
import io.opentelemetry.sdk.common.CompletableResultCode;
12+
import io.opentelemetry.sdk.trace.ReadWriteSpan;
13+
import io.opentelemetry.sdk.trace.ReadableSpan;
14+
import io.opentelemetry.sdk.trace.SpanProcessor;
15+
16+
/**
17+
* Span processor to create new JFR events for the Span as they are started, and commit on end.
18+
*
19+
* <p>NOTE: The JfrSpanProcessor measures the timing of spans, avoid if possible to wrap it with any
20+
* other SpanProcessor which may affect timings. When possible, register it first before any other
21+
* processors to allow the most accurate measurements.
22+
*/
23+
public final class JfrSpanProcessor implements SpanProcessor {
24+
25+
private final WeakConcurrentMap<SpanContext, SpanEvent> spanEvents =
26+
new WeakConcurrentMap.WithInlinedExpunction<>();
27+
28+
private volatile boolean closed;
29+
30+
@Override
31+
public void onStart(Context parentContext, ReadWriteSpan span) {
32+
if (closed) {
33+
return;
34+
}
35+
if (span.getSpanContext().isValid()) {
36+
SpanEvent event = new SpanEvent(span.toSpanData());
37+
event.begin();
38+
spanEvents.put(span.getSpanContext(), event);
39+
}
40+
}
41+
42+
@Override
43+
public boolean isStartRequired() {
44+
return true;
45+
}
46+
47+
@Override
48+
public void onEnd(ReadableSpan rs) {
49+
SpanEvent event = spanEvents.remove(rs.getSpanContext());
50+
if (!closed && event != null && event.shouldCommit()) {
51+
event.commit();
52+
}
53+
}
54+
55+
@Override
56+
public boolean isEndRequired() {
57+
return true;
58+
}
59+
60+
@Override
61+
public CompletableResultCode shutdown() {
62+
closed = true;
63+
return CompletableResultCode.ofSuccess();
64+
}
65+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.jfr;
7+
8+
import io.opentelemetry.api.trace.SpanContext;
9+
import jdk.jfr.Category;
10+
import jdk.jfr.Description;
11+
import jdk.jfr.Event;
12+
import jdk.jfr.Label;
13+
import jdk.jfr.Name;
14+
15+
@Name("io.opentelemetry.context.Scope")
16+
@Label("Scope")
17+
@Category("Open Telemetry Tracing")
18+
@Description(
19+
"Open Telemetry trace event corresponding to the span currently "
20+
+ "in scope/active on this thread.")
21+
class ScopeEvent extends Event {
22+
23+
private final String traceId;
24+
private final String spanId;
25+
26+
ScopeEvent(SpanContext spanContext) {
27+
this.traceId = spanContext.getTraceId();
28+
this.spanId = spanContext.getSpanId();
29+
}
30+
31+
@Label("Trace Id")
32+
public String getTraceId() {
33+
return traceId;
34+
}
35+
36+
@Label("Span Id")
37+
public String getSpanId() {
38+
return spanId;
39+
}
40+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.jfr;
7+
8+
import io.opentelemetry.sdk.trace.data.SpanData;
9+
import jdk.jfr.Category;
10+
import jdk.jfr.Description;
11+
import jdk.jfr.Event;
12+
import jdk.jfr.Label;
13+
import jdk.jfr.Name;
14+
15+
@Label("Span")
16+
@Name("io.opentelemetry.trace.Span")
17+
@Category("Open Telemetry Tracing")
18+
@Description("Open Telemetry trace event corresponding to a span.")
19+
class SpanEvent extends Event {
20+
21+
private final String operationName;
22+
private final String traceId;
23+
private final String spanId;
24+
private final String parentId;
25+
26+
SpanEvent(SpanData spanData) {
27+
this.operationName = spanData.getName();
28+
this.traceId = spanData.getTraceId();
29+
this.spanId = spanData.getSpanId();
30+
this.parentId = spanData.getParentSpanId();
31+
}
32+
33+
@Label("Operation Name")
34+
public String getOperationName() {
35+
return operationName;
36+
}
37+
38+
@Label("Trace Id")
39+
public String getTraceId() {
40+
return traceId;
41+
}
42+
43+
@Label("Span Id")
44+
public String getSpanId() {
45+
return spanId;
46+
}
47+
48+
@Label("Parent Id")
49+
public String getParentId() {
50+
return parentId;
51+
}
52+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Capture Spans and Scopes as events in JFR recordings.
8+
*
9+
* @see io.opentelemetry.sdk.extension.jfr.JfrSpanProcessor
10+
*/
11+
@ParametersAreNonnullByDefault
12+
package io.opentelemetry.sdk.extension.jfr;
13+
14+
import javax.annotation.ParametersAreNonnullByDefault;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.sdk.extension.jfr;
7+
8+
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
9+
10+
import io.opentelemetry.api.trace.Span;
11+
import io.opentelemetry.api.trace.Tracer;
12+
import io.opentelemetry.context.ContextStorage;
13+
import io.opentelemetry.context.Scope;
14+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
15+
import java.io.IOException;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.util.List;
19+
import jdk.jfr.Recording;
20+
import jdk.jfr.consumer.RecordedEvent;
21+
import jdk.jfr.consumer.RecordingFile;
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
class JfrSpanProcessorTest {
27+
28+
private static final String OPERATION_NAME = "Test Span";
29+
30+
private SdkTracerProvider sdkTracerProvider;
31+
private Tracer tracer;
32+
33+
@BeforeEach
34+
void setUp() {
35+
sdkTracerProvider =
36+
SdkTracerProvider.builder().addSpanProcessor(new JfrSpanProcessor()).build();
37+
tracer = sdkTracerProvider.get("JfrSpanProcessorTest");
38+
}
39+
40+
@AfterEach
41+
void tearDown() {
42+
sdkTracerProvider.shutdown();
43+
}
44+
45+
static {
46+
ContextStorage.addWrapper(JfrContextStorageWrapper::new);
47+
}
48+
49+
/**
50+
* Test basic single span.
51+
*
52+
* @throws java.io.IOException on io error
53+
*/
54+
@Test
55+
public void basicSpan() throws IOException {
56+
Path output = Files.createTempFile("test-basic-span", ".jfr");
57+
58+
try {
59+
Recording recording = new Recording();
60+
recording.start();
61+
Span span;
62+
63+
try (recording) {
64+
65+
span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan();
66+
span.end();
67+
68+
recording.dump(output);
69+
}
70+
71+
List<RecordedEvent> events = RecordingFile.readAllEvents(output);
72+
assertThat(events).hasSize(1);
73+
assertThat(events)
74+
.extracting(e -> e.getValue("traceId"))
75+
.isEqualTo(span.getSpanContext().getTraceId());
76+
assertThat(events)
77+
.extracting(e -> e.getValue("spanId"))
78+
.isEqualTo(span.getSpanContext().getSpanId());
79+
assertThat(events).extracting(e -> e.getValue("operationName")).isEqualTo(OPERATION_NAME);
80+
} finally {
81+
Files.delete(output);
82+
}
83+
}
84+
85+
/**
86+
* Test basic single span with a scope.
87+
*
88+
* @throws java.io.IOException on io error
89+
* @throws java.lang.InterruptedException interrupted sleep
90+
*/
91+
@Test
92+
public void basicSpanWithScope() throws IOException, InterruptedException {
93+
Path output = Files.createTempFile("test-basic-span-with-scope", ".jfr");
94+
95+
try {
96+
Recording recording = new Recording();
97+
recording.start();
98+
Span span;
99+
100+
try (recording) {
101+
span = tracer.spanBuilder(OPERATION_NAME).setNoParent().startSpan();
102+
try (Scope s = span.makeCurrent()) {
103+
Thread.sleep(10);
104+
}
105+
span.end();
106+
107+
recording.dump(output);
108+
}
109+
110+
List<RecordedEvent> events = RecordingFile.readAllEvents(output);
111+
assertThat(events).hasSize(2);
112+
assertThat(events)
113+
.extracting(e -> e.getValue("traceId"))
114+
.isEqualTo(span.getSpanContext().getTraceId());
115+
assertThat(events)
116+
.extracting(e -> e.getValue("spanId"))
117+
.isEqualTo(span.getSpanContext().getSpanId());
118+
assertThat(events)
119+
.filteredOn(e -> "Span".equals(e.getEventType().getLabel()))
120+
.extracting(e -> e.getValue("operationName"))
121+
.isEqualTo(OPERATION_NAME);
122+
123+
} finally {
124+
Files.delete(output);
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)