Skip to content

Commit 8b9b62a

Browse files
committed
profiles: add JFR data export example
1 parent 3ce641a commit 8b9b62a

4 files changed

Lines changed: 298 additions & 1 deletion

File tree

exporters/otlp/profiles/build.gradle.kts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,30 @@ plugins {
33
// TODO (jack-berg): uncomment when ready to publish
44
// id("otel.publish-conventions")
55

6-
id("otel.animalsniffer-conventions")
6+
// animalsniffer is disabled on this module to allow use of the JFR API.
7+
// id("otel.animalsniffer-conventions")
78
}
89

910
description = "OpenTelemetry - Profiles Exporter"
1011
otelJava.moduleName.set("io.opentelemetry.exporter.otlp.profiles")
1112

1213
val versions: Map<String, String> by project
14+
15+
tasks {
16+
// this module uses the jdk.jfr.consumer API, which was backported into 1.8 but is '@since 9'
17+
// and therefore a bit of a pain to get gradle to compile against...
18+
compileJava {
19+
sourceCompatibility = "1.8"
20+
targetCompatibility = "1.8"
21+
options.release.set(null as Int?)
22+
}
23+
compileTestJava {
24+
sourceCompatibility = "1.8"
25+
targetCompatibility = "1.8"
26+
options.release.set(null as Int?)
27+
}
28+
}
29+
1330
dependencies {
1431
api(project(":sdk:common"))
1532
api(project(":exporters:common"))
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.otlp.profiles.jfr;
7+
8+
import io.opentelemetry.exporter.otlp.profiles.ProfilesDictionaryCompositor;
9+
import io.opentelemetry.exporter.otlp.profiles.SampleCompositionBuilder;
10+
import io.opentelemetry.exporter.otlp.profiles.SampleCompositionKey;
11+
import io.opentelemetry.exporter.otlp.profiles.SampleData;
12+
import java.time.Instant;
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.concurrent.TimeUnit;
16+
import jdk.jfr.consumer.RecordedEvent;
17+
18+
/**
19+
* Converter for batching a steam of recorded jfr.ExecutionSample events into a format suitable for
20+
* consumption in a ProfileData i.e. for OTLP export. Similar converters, or a more generalized
21+
* converter, are need for each JFR event type.
22+
*/
23+
public class JfrExecutionSampleEventConverter {
24+
25+
/*
26+
* The profiles signal encoding uses dictionary lookup tables to save space by deduplicating
27+
* repeated object occurrences. The dictionary compositor is used to assemble these tables.
28+
*/
29+
private final ProfilesDictionaryCompositor profilesDictionaryCompositor =
30+
new ProfilesDictionaryCompositor();
31+
32+
/*
33+
* stack frames are dictionary encoded in multiple steps.
34+
* first, frames are converted to Locations, each of which is placed in the dictionary.
35+
* Then the stack as a whole is represented as an array of those Locations,
36+
* and the Stack message itself is also placed in the dictionary.
37+
* This assembly is handled by a JfrLocationDataCompositor wrapping the dictionary
38+
*/
39+
private final JfrLocationDataCompositor locationCompositor =
40+
new JfrLocationDataCompositor(profilesDictionaryCompositor);
41+
42+
/*
43+
* Samples are occurrences of the same observation, with an optional value and timestamp.
44+
* In JFR, for each given event type, a SampleCompositionBuilder is used to split the
45+
* events (observations) by key (stack+metadata) and record the timestamps.
46+
* If processing multiple event types, a Map<EventType,SampleCompositionBuilder> would be used.
47+
*/
48+
private final SampleCompositionBuilder sampleCompositionBuilder = new SampleCompositionBuilder();
49+
50+
/**
51+
* Convert and add a JFR event, if of appropriate type.
52+
*
53+
* @param recordedEvent the event to process.
54+
*/
55+
public void accept(RecordedEvent recordedEvent) {
56+
if (!"jdk.ExecutionSample".equals(recordedEvent.getEventType().getName())) {
57+
return;
58+
}
59+
60+
int stackIndex = locationCompositor.putIfAbsent(recordedEvent.getStackTrace().getFrames());
61+
SampleCompositionKey key = new SampleCompositionKey(stackIndex, Collections.emptyList(), 0);
62+
Instant instant = recordedEvent.getStartTime();
63+
long epochNanos = TimeUnit.SECONDS.toNanos(instant.getEpochSecond()) + instant.getNano();
64+
sampleCompositionBuilder.add(key, null, epochNanos);
65+
}
66+
67+
/**
68+
* Gets the underlying dictionary storage.
69+
*
70+
* @return the ProfilesDictionaryCompositor used by this converter.
71+
*/
72+
public ProfilesDictionaryCompositor getProfilesDictionaryCompositor() {
73+
return profilesDictionaryCompositor;
74+
}
75+
76+
/**
77+
* Gets the samples assembled from the accepted events.
78+
*
79+
* @return the data samples.
80+
*/
81+
public List<SampleData> getSamples() {
82+
return sampleCompositionBuilder.build();
83+
}
84+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.otlp.profiles.jfr;
7+
8+
import io.opentelemetry.api.common.Attributes;
9+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableProfileData;
10+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableValueTypeData;
11+
import io.opentelemetry.exporter.otlp.profiles.OtlpGrpcProfileExporter;
12+
import io.opentelemetry.exporter.otlp.profiles.OtlpGrpcProfilesExporterBuilder;
13+
import io.opentelemetry.exporter.otlp.profiles.ProfileData;
14+
import io.opentelemetry.sdk.common.CompletableResultCode;
15+
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
16+
import io.opentelemetry.sdk.resources.Resource;
17+
import java.io.IOException;
18+
import java.nio.ByteBuffer;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.concurrent.TimeUnit;
24+
import jdk.jfr.consumer.RecordingFile;
25+
26+
/**
27+
* Simple example of how to wire up the profile signal OTLP exporter to convert and send the content
28+
* of a JFR recording file. This is not a supported CLI and is not intended to be configurable by
29+
* e.g. command line flags.
30+
*/
31+
public class JfrExportTool {
32+
33+
private JfrExportTool() {}
34+
35+
@SuppressWarnings("SystemOut")
36+
public static void main(String[] args) throws IOException {
37+
38+
Path jfrFilePath = Path.of("/tmp/demo.jfr"); // TODO set the JFR file location here
39+
ProfileData profileData = convertJfrFile(jfrFilePath);
40+
41+
// for test purposes https://github.com/elastic/devfiler/ provides a handy standalone backend.
42+
// by default devfiler listens on port 11000
43+
String destination = "127.0.0.1:11000"; // TODO set the location of the backend receiver here
44+
45+
OtlpGrpcProfilesExporterBuilder exporterBuilder = OtlpGrpcProfileExporter.builder();
46+
exporterBuilder.setEndpoint("http://" + destination);
47+
OtlpGrpcProfileExporter exporter = exporterBuilder.build();
48+
49+
CompletableResultCode completableResultCode = exporter.export(List.of(profileData));
50+
completableResultCode.join(1, TimeUnit.MINUTES);
51+
System.out.println(completableResultCode.isSuccess() ? "success" : "failure");
52+
}
53+
54+
/**
55+
* Read the content of the JFR recording file and convert it to a ProfileData object in
56+
* preparation for OTLP export.
57+
*
58+
* @param jfrFilePath the data source.
59+
* @return a ProfileData object constructed from the JFR recording.
60+
* @throws IOException if the conversion fails.
61+
*/
62+
public static ProfileData convertJfrFile(Path jfrFilePath) throws IOException {
63+
64+
JfrExecutionSampleEventConverter converter = new JfrExecutionSampleEventConverter();
65+
66+
RecordingFile recordingFile = new RecordingFile(jfrFilePath);
67+
while (recordingFile.hasMoreEvents()) {
68+
converter.accept(recordingFile.readEvent());
69+
}
70+
recordingFile.close();
71+
72+
String profileId = "0123456789abcdef0123456789abcdef";
73+
InstrumentationScopeInfo scopeInfo =
74+
InstrumentationScopeInfo.builder("testLib")
75+
.setVersion("1.0")
76+
.setSchemaUrl("http://url")
77+
.build();
78+
79+
return ImmutableProfileData.create(
80+
Resource.create(Attributes.empty()),
81+
scopeInfo,
82+
converter.getProfilesDictionaryCompositor().getProfileDictionaryData(),
83+
ImmutableValueTypeData.create(0, 0),
84+
converter.getSamples(),
85+
0,
86+
0,
87+
ImmutableValueTypeData.create(0, 0),
88+
0,
89+
profileId,
90+
0,
91+
"format",
92+
ByteBuffer.wrap(Files.readAllBytes(jfrFilePath)),
93+
Collections.emptyList());
94+
}
95+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.exporter.otlp.profiles.jfr;
7+
8+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableFunctionData;
9+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableLineData;
10+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableLocationData;
11+
import io.opentelemetry.exporter.otlp.internal.data.ImmutableStackData;
12+
import io.opentelemetry.exporter.otlp.profiles.FunctionData;
13+
import io.opentelemetry.exporter.otlp.profiles.LineData;
14+
import io.opentelemetry.exporter.otlp.profiles.LocationData;
15+
import io.opentelemetry.exporter.otlp.profiles.ProfilesDictionaryCompositor;
16+
import io.opentelemetry.exporter.otlp.profiles.StackData;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import jdk.jfr.consumer.RecordedFrame;
20+
21+
/**
22+
* Allows for the conversion and storage of JFR thread stacks in the dictionary encoding structure
23+
* used by OTLP profile signal exporters.
24+
*
25+
* <p>The compositor resembles a builder, though without the fluent API. Instead, mutation methods
26+
* return the index of the offered element, this information being required to construct any element
27+
* that references into the tables.
28+
*
29+
* <p>This class is not threadsafe and must be externally synchronized.
30+
*/
31+
public class JfrLocationDataCompositor {
32+
33+
private final ProfilesDictionaryCompositor profilesDictionaryCompositor;
34+
35+
/**
36+
* Wrap the given dictionary with additional JFR-specific stack data handling functionality.
37+
*
38+
* @param profilesDictionaryCompositor the underlying storage.
39+
*/
40+
public JfrLocationDataCompositor(ProfilesDictionaryCompositor profilesDictionaryCompositor) {
41+
this.profilesDictionaryCompositor = profilesDictionaryCompositor;
42+
}
43+
44+
/**
45+
* Stores the provided list of frames as a StackData element in the dictionary if an equivalent is
46+
* not already present, and returns its index.
47+
*
48+
* @param frameList the JFR stack data.
49+
* @return the index of the added or existing StackData element.
50+
*/
51+
public int putIfAbsent(List<RecordedFrame> frameList) {
52+
53+
List<Integer> locationIndices = frameList.stream().map(this::frameToLocation).toList();
54+
55+
StackData stackData = ImmutableStackData.create(locationIndices);
56+
int stackIndex = profilesDictionaryCompositor.putIfAbsent(stackData);
57+
return stackIndex;
58+
}
59+
60+
/**
61+
* Convert a single frame of a stack to a LocationData, store it and its components in the
62+
* dictionary and return its index.
63+
*
64+
* @param frame the source data
65+
* @return the LocationData storage index in the dictionary
66+
*/
67+
protected int frameToLocation(RecordedFrame frame) {
68+
69+
// the LocationData references several components which need creating and placing in their
70+
// respective dictionary tables
71+
72+
String name = nameFrom(frame);
73+
int nameStringIndex = profilesDictionaryCompositor.putIfAbsent(name);
74+
75+
FunctionData functionData = ImmutableFunctionData.create(nameStringIndex, 0, 0, 0);
76+
int functionIndex = profilesDictionaryCompositor.putIfAbsent(functionData);
77+
78+
int lineNumber = frame.getLineNumber() != -1 ? frame.getLineNumber() : 0;
79+
LineData lineData = ImmutableLineData.create(functionIndex, lineNumber, 0);
80+
81+
LocationData locationData =
82+
ImmutableLocationData.create(0, 0, List.of(lineData), Collections.emptyList());
83+
84+
int locationIndex = profilesDictionaryCompositor.putIfAbsent(locationData);
85+
return locationIndex;
86+
}
87+
88+
/**
89+
* Construct a name String from the frame. Note that the wire spec and semantic conventions don't
90+
* define a specific string format. Override this method to customize the conversion.
91+
*
92+
* @param frame the JFR frame data.
93+
* @return the name as a String.
94+
*/
95+
protected String nameFrom(RecordedFrame frame) {
96+
String name = frame.getMethod().getType() != null ? frame.getMethod().getType().getName() : "";
97+
name += ".";
98+
name += frame.getMethod().getName() != null ? frame.getMethod().getName() : "";
99+
return name;
100+
}
101+
}

0 commit comments

Comments
 (0)