Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# Shared communication
/communication/*.* @DataDog/apm-java
/communication/otlp-exporter/ @DataDog/apm-sdk-capabilities-java
/communication/otlp-exporter/ @DataDog/apm-sdk-capabilities-java
/communication/src/*/*/datadog/communication/*.* @DataDog/apm-java
/communication/src/*/*/datadog/communication/ddagent/ @DataDog/apm-java
/communication/src/*/*/datadog/communication/http/ @DataDog/apm-java
Expand Down Expand Up @@ -362,6 +362,7 @@

# @DataDog/profiling-java
/dd-java-agent/ddprof-lib/ @DataDog/profiling-java
/docker/Dockerfile.profcheck @DataDog/profiling-java
/dd-java-agent/agent-profiling/ @DataDog/profiling-java
/dd-java-agent/agent-crashtracking/ @DataDog/profiling-java
/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/jfr/ @DataDog/profiling-java
Expand Down
27 changes: 27 additions & 0 deletions dd-java-agent/agent-profiling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {

api project(':dd-java-agent:agent-profiling:profiling-ddprof')
api project(':dd-java-agent:agent-profiling:profiling-uploader')
api project(':dd-java-agent:agent-profiling:profiling-otel')
api project(':dd-java-agent:agent-profiling:profiling-controller')
implementation project(':dd-java-agent:agent-profiling:profiling-scrubber')
api project(':dd-java-agent:agent-profiling:profiling-controller-jfr')
Expand All @@ -42,13 +43,39 @@ configurations {
}
}

// jafar-tools is a fat jar bundling (an incomplete copy of) the jafar-parser classes; the agent
// bundles jafar-parser (via profiling-otel) instead and only needs the io.jafar.tools classes
// (used by profiling-scrubber) from the fat jar
configurations {
jafarTools
}
dependencies {
jafarTools("io.btrace:jafar-tools:${libs.versions.jafar.get()}") {
transitive = false
}
}
def jafarToolsJar = configurations.jafarTools

tasks.named("shadowJar", ShadowJar) {
dependencies deps.excludeShared

// drop the jafar-tools fat jar (parser classes come from jafar-parser via profiling-otel) and
// re-add only its unique io/jafar/tools classes to keep a single copy of every class
dependencies {
exclude(dependency('io.btrace:jafar-tools:.*'))
}
from({ zipTree(jafarToolsJar.singleFile) }) {
include 'io/jafar/tools/**'
}

// Exclude multi-release versioned classes from jafar-parser.
// These are duplicates of base classes for newer Java APIs and confuse
// the GraalVM native-image builder when the profiling jar is embedded in the agent.
exclude 'META-INF/versions/**'

// JfrToOtlpConverterCLI is the entry point of the standalone converter jar; the agent never
// loads it, so keep it out of the profiling content bundled into the agent jar
exclude 'com/datadog/profiling/otel/JfrToOtlpConverterCLI.class'
}

tasks.named("jar", Jar) {
Expand Down
228 changes: 228 additions & 0 deletions dd-java-agent/agent-profiling/profiling-otel/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
plugins {
`java-library`
id("dd-trace-java.module.internal-library")
id("com.gradleup.shadow")
id("me.champeau.jmh")
}

// Tests require Java 17+ (JMC 9.1.1); on JVM 8 (where -PcheckCoverage runs) tests are skipped,
// so all main classes would have 0% coverage. Exclude them from verification.
extra["excludedClassesCoverage"] = listOf(
"com.datadog.profiling.otel.JfrToOtlpConverter*",
"com.datadog.profiling.otel.JfrToOtlpConverterCLI",
"com.datadog.profiling.otel.jfr.*",
"com.datadog.profiling.otel.proto.*",
"com.datadog.profiling.otel.proto.dictionary.*",
)

jmh {
jmhVersion = libs.versions.jmh.get()

// Allow filtering benchmarks via command line
// Usage: ./gradlew jmh -PjmhIncludes="JfrToOtlpConverterBenchmark"
// Usage: ./gradlew jmh -PjmhIncludes=".*convertJfrToOtlp"
if (project.hasProperty("jmhIncludes")) {
val pattern = project.property("jmhIncludes") as String
includes = listOf(pattern)
}

// Profiling support
// Usage: ./gradlew jmh -PjmhProfile=true
// Generates flamegraph and allocation profile
if (project.hasProperty("jmhProfile")) {
profilers = listOf("gc", "stack")
jvmArgs = listOf(
"-XX:+UnlockDiagnosticVMOptions",
"-XX:+DebugNonSafepoints"
)
}

// async-profiler CPU flamegraph
// Usage: ./gradlew jmh -PjmhAsyncProfiler=/path/to/libasyncProfiler.dylib
// Output: /tmp/jmh-async-profile.html
if (project.hasProperty("jmhAsyncProfiler")) {
val lib = project.property("jmhAsyncProfiler") as String
jvmArgs = listOf(
"-XX:+UnlockDiagnosticVMOptions",
"-XX:+DebugNonSafepoints",
"-agentpath:$lib=start,event=cpu,file=/tmp/jmh-async-profile.html,flamegraph"
)
}
}

// OTLP validation tests removed - use profcheck validation instead (see validateOtlp task below)

repositories {
maven {
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
mavenContent {
snapshotsOnly()
// jafar is only published as a snapshot; restrict resolution to that group
includeGroup("io.btrace")
}
}
}

configure<datadog.gradle.plugin.testJvmConstraints.TestJvmConstraintsExtension> {
minJavaVersion = JavaVersion.VERSION_17
}

tasks.named<JavaCompile>("compileTestJava") {
// JMC 9.1.1 requires Java 17, and we need jdk.jfr.Event for stack trace testing
options.release.set(17)
javaCompiler.set(
javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(17)) }
)
}

tasks.named<JavaCompile>("compileJmhJava") {
// JMC 9.1.1 requires Java 17, and we need jdk.jfr.Event for JMH benchmarks
options.release.set(17)
javaCompiler.set(
javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(17)) }
)
}

// Create fat jar for standalone CLI usage
tasks.named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier.set("cli")
manifest {
attributes["Main-Class"] = "com.datadog.profiling.otel.JfrToOtlpConverterCLI"
}
// Exclude SLF4J service provider files to avoid warnings
exclude("META-INF/services/org.slf4j.spi.SLF4JServiceProvider")
}

// CLI task for converting JFR files
// Usage: ./gradlew :dd-java-agent:agent-profiling:profiling-otel:convertJfr --args="input.jfr output.pb"
// Usage: ./gradlew :dd-java-agent:agent-profiling:profiling-otel:convertJfr --args="--json input.jfr output.json"
tasks.register<JavaExec>("convertJfr") {
group = "application"
description = "Convert JFR recording to OTLP profiles format"
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("com.datadog.profiling.otel.JfrToOtlpConverterCLI")

// Uses Gradle's built-in --args parameter which properly handles spaces in paths
}

// Build profcheck Docker image
// Usage: ./gradlew :dd-java-agent:agent-profiling:profiling-otel:buildProfcheck
tasks.register<Exec>("buildProfcheck") {
group = "verification"
description = "Build profcheck Docker image for OTLP validation"
workingDir = rootDir
commandLine("docker", "build", "-f", "docker/Dockerfile.profcheck", "-t", "profcheck:latest", ".")

// Check if Docker is available
doFirst {
val process = ProcessBuilder("docker", "info").redirectErrorStream(true).start()
if (process.waitFor() != 0) {
throw org.gradle.api.GradleException("Docker is not available. Profcheck validation requires Docker to be running.")
}
}
}

// OTLP validation tests (ProfcheckValidationTest, OtlpCollectorValidationTest) are gated behind
// the -PrunOtlpValidation Gradle property. They require Docker and network access to pull/build
// images, which is not available in the standard CI test matrix. Enable manually:
// ./gradlew :dd-java-agent:agent-profiling:profiling-otel:test -PrunOtlpValidation
tasks.named<Test>("test") {
if (project.hasProperty("runOtlpValidation")) {
jvmArgs("-Drun.otlp.validation=true")

// Build profcheck image if Docker is available (for ProfcheckValidationTest)
doFirst {
val dockerAvailable = try {
val process = ProcessBuilder("docker", "info").redirectErrorStream(true).start()
process.waitFor() == 0
} catch (e: Exception) {
false
}

if (dockerAvailable) {
logger.lifecycle("Building profcheck Docker image for validation tests...")
val buildProcess = ProcessBuilder(
"docker",
"build",
"-f",
"$rootDir/docker/Dockerfile.profcheck",
"-t",
"profcheck:latest",
rootDir.toString()
).redirectErrorStream(true).start()
// drain the output while waiting — docker build routinely exceeds the OS pipe-buffer
// capacity (~64 KiB) and an unread full pipe deadlocks waitFor()
val outputBuilder = StringBuilder()
val drainThread = Thread {
buildProcess.inputStream.bufferedReader().forEachLine { line ->
synchronized(outputBuilder) { outputBuilder.appendLine(line) }
}
}
drainThread.isDaemon = true
drainThread.start()
val exitCode = buildProcess.waitFor()
drainThread.join(10_000)
if (exitCode != 0) {
val output = synchronized(outputBuilder) { outputBuilder.toString() }
throw org.gradle.api.GradleException("Failed to build profcheck Docker image (exit $exitCode):\n$output")
}
} else {
// without the image the docker-tagged validation tests cannot run
logger.warn("Docker not available: excluding tests tagged 'docker'.")
useJUnitPlatform {
excludeTags("docker")
}
}
}
}
}

// Validate OTLP output using profcheck
// Usage: ./gradlew :dd-java-agent:agent-profiling:profiling-otel:validateOtlp -PotlpFile=/path/to/output.pb
tasks.register<Exec>("validateOtlp") {
group = "verification"
description = "Validate OTLP profile using profcheck (requires Docker)"

// Ensure profcheck image exists
dependsOn("buildProfcheck")

doFirst {
if (!project.hasProperty("otlpFile")) {
throw org.gradle.api.GradleException("Property 'otlpFile' is required. Usage: -PotlpFile=/path/to/output.pb")
}

val otlpFilePath = project.property("otlpFile") as String
val otlpFile = file(otlpFilePath)

if (!otlpFile.exists()) {
throw org.gradle.api.GradleException("File not found: $otlpFilePath")
}

val parentDir = otlpFile.parentFile.absolutePath
val fileName = otlpFile.name

// Run profcheck in Docker with volume mount
commandLine(
"docker",
"run",
"--rm",
"-v",
"$parentDir:/data:ro",
"profcheck:latest",
"/data/$fileName"
)
}
}

dependencies {
implementation(libs.jafar.parser)
implementation(project(":internal-api"))
implementation(project(":components:json"))

testImplementation(libs.bundles.junit5)
testImplementation(libs.bundles.jmc)
testImplementation(libs.jmc.flightrecorder.writer)
testImplementation(libs.testcontainers)
testImplementation("org.testcontainers:junit-jupiter:1.21.3")
testImplementation(libs.testing.okhttp3)
}
Loading
Loading