Skip to content
Merged
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 dd-java-agent/agent-profiling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ excludedClassesCoverage += [
'com.datadog.profiling.agent.CompositeController.CompositeOngoingRecording',
'com.datadog.profiling.agent.ProfilingAgent',
'com.datadog.profiling.agent.ProfilingAgent.ShutdownHook',
'com.datadog.profiling.agent.ProfilingAgent.DataDumper'
'com.datadog.profiling.agent.ProfilingAgent.DataDumper',
'com.datadog.profiling.agent.ProfilerFlare'
]

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ abstract class ProcessManager extends Specification {

def setup() {
testedProcesses.each {
assert it.alive: "Process $it is not availble on test beginning"
assert it.alive: "Process $it is not available on test beginning"
}

synchronized (outputThreads.testLogMessages) {
Expand Down
18 changes: 18 additions & 0 deletions dd-smoke-tests/tracer-flare/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apply from: "$rootDir/gradle/java.gradle"
description = 'Tracer Flare Smoke Tests.'

jar {
manifest {
attributes('Main-Class': 'datadog.smoketest.flare.SimpleApp')
}
}

dependencies {
testImplementation project(':dd-smoke-tests')
}

tasks.withType(Test).configureEach {
dependsOn "jar"

jvmArgs "-Ddatadog.smoketest.tracer-flare.jar.path=${tasks.jar.archiveFile.get()}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package datadog.smoketest.flare;

public class SimpleApp {
public static void main(String[] args) throws InterruptedException {
System.out.println("SimpleApp starting - waiting for flare generation");

// Keep the app running indefinitely
// The flare will be triggered after 10 seconds (configured in test)
// The test will wait for the flare and then terminate the process
while (true) {
System.out.println("SimpleApp running...");
Thread.sleep(5000);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
package datadog.smoketest

import spock.lang.Shared

import java.nio.file.FileSystems
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

/**
* This smoke test can be extended with additional configurations of the agent and assertions on the files
* expected to be present in the resulting flare(s). Checking individual file contents should be done a per-reporter
* basis.
*
* For DD Employees - if you update this file alongside changes to an existing reporter or the creation of a new one,
* please also document it in the
* <a href="https://datadoghq.atlassian.net/wiki/spaces/APMINT/pages/3389554943/Java+Tracer+Flare">Tracer Flare Wiki</a>
*/
class TracerFlareSmokeTest extends AbstractSmokeTest {

// Time in seconds after which flare is triggered
private static final int FLARE_TRIGGER_SECONDS = 15
// Number of processes to run in parallel for testing
private static final int NUMBER_OF_PROCESSES = 2

protected int numberOfProcesses() {
NUMBER_OF_PROCESSES
}

@Shared
final flareDirs = [
File.createTempDir("flare-test-profiling-enabled-", ""),
File.createTempDir("flare-test-profiling-disabled-", "")
]

def cleanupSpec() {
flareDirs.each { dir ->
if (dir.exists()) {
dir.deleteDir()
}
}
}

@Override
ProcessBuilder createProcessBuilder(int processIndex) {
String jarPath = System.getProperty("datadog.smoketest.tracer-flare.jar.path")
File flareDir = flareDirs[processIndex]

def command = [javaPath()]

if (processIndex == 0) {
// Process 0: Profiling enabled (default)
command.addAll(defaultJavaProperties)
} else {
// Process 1: Profiling disabled
def filteredProperties = defaultJavaProperties.findAll { prop ->
!prop.startsWith("-Ddd.profiling.")
}
command.addAll(filteredProperties)
command.add("-Ddd.profiling.enabled=false")
}

// Configure flare generation
command.addAll([
"-Ddd.triage.report.trigger=${FLARE_TRIGGER_SECONDS}s",
"-Ddd.triage.report.dir=${flareDir.absolutePath}",
"-Ddd.trace.debug=true",
// Enable debug to get more files
'-jar',
jarPath
] as String[])

new ProcessBuilder(command).tap {
it.directory(new File(buildDirectory))
}
}

// Core files that should always be present
private static final CORE_FILES = [
"flare_info.txt",
"tracer_version.txt",
"initial_config.txt",
"dynamic_config.txt",
"jvm_args.txt",
"classpath.txt",
"library_path.txt",
"threads.txt",
// Should be present with triage=true
// Files from CoreTracer
"tracer_health.txt",
"span_metrics.txt",
// Files from InstrumenterFlare (always registered)
"instrumenter_state.txt",
"instrumenter_metrics.txt",
// Files from DI
"dynamic_instrumentation.txt"
] as Set<String>

// Optional files that may or may not be present depending on conditions
private static final OPTIONAL_FILES = [
"boot_classpath.txt",
// Only if JVM supports it (Java 8)
"tracer.log",
// Only if logging is configured
"tracer_begin.log",
// Alternative log format
"tracer_end.log",
// Alternative log format
"flare_errors.txt",
// Only if there were errors
"pending_traces.txt" // Only if there were traces pending transmission
] as Set<String>

// Profiling-related files
private static final PROFILING_FILES = [
"profiler_config.txt",
// Only if profiling is enabled
"profiling_template_override.jfp" // Only if template override is configured
] as Set<String>

// Flare file naming pattern constants
private static final String FLARE_FILE_PREFIX = "dd-java-flare-"
private static final String FLARE_FILE_EXTENSION = ".zip"

/**
* Checks if a filename matches the expected flare file pattern
*/
private static boolean isFlareFile(String fileName) {
fileName.startsWith(FLARE_FILE_PREFIX) && fileName.endsWith(FLARE_FILE_EXTENSION)
}

def "tracer generates flare with profiling enabled (default)"() {
when:
// Wait for flare file to be created using filesystem watcher
// The flare is triggered after FLARE_TRIGGER_SECONDS, plus some write time
def flareFile = waitForFlareFile(flareDirs[0])
def zipContents = extractZipContents(flareFile)

then:
// Verify core files are present
CORE_FILES.each { file ->
assert file in zipContents : "Missing required core file: ${file}"
}

// Verify profiling files are present (profiling is enabled in defaultJavaProperties)
assert "profiler_config.txt" in zipContents : "Missing profiler_config.txt when profiling is enabled"

// Check for unexpected files and fail if found
validateNoUnexpectedFiles(zipContents, CORE_FILES + OPTIONAL_FILES + PROFILING_FILES)
}

def "tracer generates flare with profiling disabled"() {
when:
// Wait for flare file to be created independently for process 1
// Each test should be independent and not rely on timing from other tests
def flareFile = waitForFlareFile(flareDirs[1])
def zipContents = extractZipContents(flareFile)

then:
// Verify core files are present
CORE_FILES.each { file ->
assert file in zipContents : "Missing required core file: ${file}"
}

// Verify NO profiling files are present when profiling is disabled
PROFILING_FILES.each { file ->
assert !(file in zipContents) : "Found profiling file '${file}' when profiling is disabled"
}

// Check for unexpected files and fail if found (profiling files excluded from expected)
validateNoUnexpectedFiles(zipContents, CORE_FILES + OPTIONAL_FILES)
}

private static void validateNoUnexpectedFiles(Set<String> zipContents, Set<String> expectedFiles) {
def unexpectedFiles = zipContents - expectedFiles
assert !unexpectedFiles : "Found unexpected files in flare: ${unexpectedFiles}"
}

private static Set<String> extractZipContents(File zipFile) {
def fileNames = []

zipFile.withInputStream { stream ->
new ZipInputStream(stream).withCloseable { zis ->
ZipEntry entry
while ((entry = zis.nextEntry) != null) {
if (!entry.directory) {
fileNames << entry.name
}
zis.closeEntry()
}
}
}

fileNames
}

/**
* Waits for a flare file to be created in the specified directory using filesystem watching.
*
* @param flareDir The directory to watch for flare files
* @param timeoutSeconds Maximum time to wait for the file
* @return The created flare file
* @throws AssertionError if no flare file is created within the timeout
*/
private static File waitForFlareFile(File flareDir, int timeoutSeconds = FLARE_TRIGGER_SECONDS + 5) {
Path dirPath = flareDir.toPath()
WatchService watchService = FileSystems.getDefault().newWatchService()

try {
def existingFile = findFlareFileIfExists(flareDir)
if (existingFile) {
return existingFile
}

dirPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE)
long deadlineMillis = System.currentTimeMillis() + (timeoutSeconds * 1000)

while (System.currentTimeMillis() < deadlineMillis) {
long remainingMillis = deadlineMillis - System.currentTimeMillis()
if (remainingMillis <= 0) {
break
}

WatchKey key = watchService.poll(remainingMillis, TimeUnit.MILLISECONDS)
if (key == null) {
existingFile = findFlareFileIfExists(flareDir)
if (existingFile) {
return existingFile
}
break
}

for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event
Path fileName = pathEvent.context()

if (isFlareFile(fileName.toString())) {
return new File(flareDir, fileName.toString())
}
}

boolean valid = key.reset()
if (!valid) {
throw new AssertionError("Watch directory ${flareDir} is no longer accessible")
}
}

existingFile = findFlareFileIfExists(flareDir)
if (existingFile) {
return existingFile
}

throw new AssertionError("No flare file created in ${flareDir} within ${timeoutSeconds} seconds")

} finally {
watchService.close()
}
}

/**
* Attempts to find an existing flare file in the directory.
* Returns null if no flare file exists.
*/
private static File findFlareFileIfExists(File flareDir) {
def flareFiles = flareDir.listFiles({ File dir, String name ->
isFlareFile(name)
} as FilenameFilter)

return flareFiles?.size() > 0 ? flareFiles.first() : null
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ include(
":dd-smoke-tests:springboot-tomcat",
":dd-smoke-tests:springboot-tomcat-jsp",
":dd-smoke-tests:springboot-velocity",
":dd-smoke-tests:tracer-flare",
":dd-smoke-tests:vertx-3.4",
":dd-smoke-tests:vertx-3.9",
":dd-smoke-tests:vertx-3.9-resteasy",
Expand Down