Skip to content

Commit 0a4e9d0

Browse files
authored
Create a flare contents smoke test (#9440)
* Initial smoke tests for tracer flare functionality Squashed 9 commits * Replace naive wait with file watching
1 parent 890497c commit 0a4e9d0

File tree

6 files changed

+313
-2
lines changed

6 files changed

+313
-2
lines changed

dd-java-agent/agent-profiling/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ excludedClassesCoverage += [
1010
'com.datadog.profiling.agent.CompositeController.CompositeOngoingRecording',
1111
'com.datadog.profiling.agent.ProfilingAgent',
1212
'com.datadog.profiling.agent.ProfilingAgent.ShutdownHook',
13-
'com.datadog.profiling.agent.ProfilingAgent.DataDumper'
13+
'com.datadog.profiling.agent.ProfilingAgent.DataDumper',
14+
'com.datadog.profiling.agent.ProfilerFlare'
1415
]
1516

1617
dependencies {

dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ abstract class ProcessManager extends Specification {
6060

6161
def setup() {
6262
testedProcesses.each {
63-
assert it.alive: "Process $it is not availble on test beginning"
63+
assert it.alive: "Process $it is not available on test beginning"
6464
}
6565

6666
synchronized (outputThreads.testLogMessages) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apply from: "$rootDir/gradle/java.gradle"
2+
description = 'Tracer Flare Smoke Tests.'
3+
4+
jar {
5+
manifest {
6+
attributes('Main-Class': 'datadog.smoketest.flare.SimpleApp')
7+
}
8+
}
9+
10+
dependencies {
11+
testImplementation project(':dd-smoke-tests')
12+
}
13+
14+
tasks.withType(Test).configureEach {
15+
dependsOn "jar"
16+
17+
jvmArgs "-Ddatadog.smoketest.tracer-flare.jar.path=${tasks.jar.archiveFile.get()}"
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package datadog.smoketest.flare;
2+
3+
public class SimpleApp {
4+
public static void main(String[] args) throws InterruptedException {
5+
System.out.println("SimpleApp starting - waiting for flare generation");
6+
7+
// Keep the app running indefinitely
8+
// The flare will be triggered after 10 seconds (configured in test)
9+
// The test will wait for the flare and then terminate the process
10+
while (true) {
11+
System.out.println("SimpleApp running...");
12+
Thread.sleep(5000);
13+
}
14+
}
15+
}
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package datadog.smoketest
2+
3+
import spock.lang.Shared
4+
5+
import java.nio.file.FileSystems
6+
import java.nio.file.Path
7+
import java.nio.file.StandardWatchEventKinds
8+
import java.nio.file.WatchEvent
9+
import java.nio.file.WatchKey
10+
import java.nio.file.WatchService
11+
import java.util.concurrent.TimeUnit
12+
import java.util.zip.ZipEntry
13+
import java.util.zip.ZipInputStream
14+
15+
/**
16+
* This smoke test can be extended with additional configurations of the agent and assertions on the files
17+
* expected to be present in the resulting flare(s). Checking individual file contents should be done a per-reporter
18+
* basis.
19+
*
20+
* For DD Employees - if you update this file alongside changes to an existing reporter or the creation of a new one,
21+
* please also document it in the
22+
* <a href="https://datadoghq.atlassian.net/wiki/spaces/APMINT/pages/3389554943/Java+Tracer+Flare">Tracer Flare Wiki</a>
23+
*/
24+
class TracerFlareSmokeTest extends AbstractSmokeTest {
25+
26+
// Time in seconds after which flare is triggered
27+
private static final int FLARE_TRIGGER_SECONDS = 15
28+
// Number of processes to run in parallel for testing
29+
private static final int NUMBER_OF_PROCESSES = 2
30+
31+
protected int numberOfProcesses() {
32+
NUMBER_OF_PROCESSES
33+
}
34+
35+
@Shared
36+
final flareDirs = [
37+
File.createTempDir("flare-test-profiling-enabled-", ""),
38+
File.createTempDir("flare-test-profiling-disabled-", "")
39+
]
40+
41+
def cleanupSpec() {
42+
flareDirs.each { dir ->
43+
if (dir.exists()) {
44+
dir.deleteDir()
45+
}
46+
}
47+
}
48+
49+
@Override
50+
ProcessBuilder createProcessBuilder(int processIndex) {
51+
String jarPath = System.getProperty("datadog.smoketest.tracer-flare.jar.path")
52+
File flareDir = flareDirs[processIndex]
53+
54+
def command = [javaPath()]
55+
56+
if (processIndex == 0) {
57+
// Process 0: Profiling enabled (default)
58+
command.addAll(defaultJavaProperties)
59+
} else {
60+
// Process 1: Profiling disabled
61+
def filteredProperties = defaultJavaProperties.findAll { prop ->
62+
!prop.startsWith("-Ddd.profiling.")
63+
}
64+
command.addAll(filteredProperties)
65+
command.add("-Ddd.profiling.enabled=false")
66+
}
67+
68+
// Configure flare generation
69+
command.addAll([
70+
"-Ddd.triage.report.trigger=${FLARE_TRIGGER_SECONDS}s",
71+
"-Ddd.triage.report.dir=${flareDir.absolutePath}",
72+
"-Ddd.trace.debug=true",
73+
// Enable debug to get more files
74+
'-jar',
75+
jarPath
76+
] as String[])
77+
78+
new ProcessBuilder(command).tap {
79+
it.directory(new File(buildDirectory))
80+
}
81+
}
82+
83+
// Core files that should always be present
84+
private static final CORE_FILES = [
85+
"flare_info.txt",
86+
"tracer_version.txt",
87+
"initial_config.txt",
88+
"dynamic_config.txt",
89+
"jvm_args.txt",
90+
"classpath.txt",
91+
"library_path.txt",
92+
"threads.txt",
93+
// Should be present with triage=true
94+
// Files from CoreTracer
95+
"tracer_health.txt",
96+
"span_metrics.txt",
97+
// Files from InstrumenterFlare (always registered)
98+
"instrumenter_state.txt",
99+
"instrumenter_metrics.txt",
100+
// Files from DI
101+
"dynamic_instrumentation.txt"
102+
] as Set<String>
103+
104+
// Optional files that may or may not be present depending on conditions
105+
private static final OPTIONAL_FILES = [
106+
"boot_classpath.txt",
107+
// Only if JVM supports it (Java 8)
108+
"tracer.log",
109+
// Only if logging is configured
110+
"tracer_begin.log",
111+
// Alternative log format
112+
"tracer_end.log",
113+
// Alternative log format
114+
"flare_errors.txt",
115+
// Only if there were errors
116+
"pending_traces.txt" // Only if there were traces pending transmission
117+
] as Set<String>
118+
119+
// Profiling-related files
120+
private static final PROFILING_FILES = [
121+
"profiler_config.txt",
122+
// Only if profiling is enabled
123+
"profiling_template_override.jfp" // Only if template override is configured
124+
] as Set<String>
125+
126+
// Flare file naming pattern constants
127+
private static final String FLARE_FILE_PREFIX = "dd-java-flare-"
128+
private static final String FLARE_FILE_EXTENSION = ".zip"
129+
130+
/**
131+
* Checks if a filename matches the expected flare file pattern
132+
*/
133+
private static boolean isFlareFile(String fileName) {
134+
fileName.startsWith(FLARE_FILE_PREFIX) && fileName.endsWith(FLARE_FILE_EXTENSION)
135+
}
136+
137+
def "tracer generates flare with profiling enabled (default)"() {
138+
when:
139+
// Wait for flare file to be created using filesystem watcher
140+
// The flare is triggered after FLARE_TRIGGER_SECONDS, plus some write time
141+
def flareFile = waitForFlareFile(flareDirs[0])
142+
def zipContents = extractZipContents(flareFile)
143+
144+
then:
145+
// Verify core files are present
146+
CORE_FILES.each { file ->
147+
assert file in zipContents : "Missing required core file: ${file}"
148+
}
149+
150+
// Verify profiling files are present (profiling is enabled in defaultJavaProperties)
151+
assert "profiler_config.txt" in zipContents : "Missing profiler_config.txt when profiling is enabled"
152+
153+
// Check for unexpected files and fail if found
154+
validateNoUnexpectedFiles(zipContents, CORE_FILES + OPTIONAL_FILES + PROFILING_FILES)
155+
}
156+
157+
def "tracer generates flare with profiling disabled"() {
158+
when:
159+
// Wait for flare file to be created independently for process 1
160+
// Each test should be independent and not rely on timing from other tests
161+
def flareFile = waitForFlareFile(flareDirs[1])
162+
def zipContents = extractZipContents(flareFile)
163+
164+
then:
165+
// Verify core files are present
166+
CORE_FILES.each { file ->
167+
assert file in zipContents : "Missing required core file: ${file}"
168+
}
169+
170+
// Verify NO profiling files are present when profiling is disabled
171+
PROFILING_FILES.each { file ->
172+
assert !(file in zipContents) : "Found profiling file '${file}' when profiling is disabled"
173+
}
174+
175+
// Check for unexpected files and fail if found (profiling files excluded from expected)
176+
validateNoUnexpectedFiles(zipContents, CORE_FILES + OPTIONAL_FILES)
177+
}
178+
179+
private static void validateNoUnexpectedFiles(Set<String> zipContents, Set<String> expectedFiles) {
180+
def unexpectedFiles = zipContents - expectedFiles
181+
assert !unexpectedFiles : "Found unexpected files in flare: ${unexpectedFiles}"
182+
}
183+
184+
private static Set<String> extractZipContents(File zipFile) {
185+
def fileNames = []
186+
187+
zipFile.withInputStream { stream ->
188+
new ZipInputStream(stream).withCloseable { zis ->
189+
ZipEntry entry
190+
while ((entry = zis.nextEntry) != null) {
191+
if (!entry.directory) {
192+
fileNames << entry.name
193+
}
194+
zis.closeEntry()
195+
}
196+
}
197+
}
198+
199+
fileNames
200+
}
201+
202+
/**
203+
* Waits for a flare file to be created in the specified directory using filesystem watching.
204+
*
205+
* @param flareDir The directory to watch for flare files
206+
* @param timeoutSeconds Maximum time to wait for the file
207+
* @return The created flare file
208+
* @throws AssertionError if no flare file is created within the timeout
209+
*/
210+
private static File waitForFlareFile(File flareDir, int timeoutSeconds = FLARE_TRIGGER_SECONDS + 5) {
211+
Path dirPath = flareDir.toPath()
212+
WatchService watchService = FileSystems.getDefault().newWatchService()
213+
214+
try {
215+
def existingFile = findFlareFileIfExists(flareDir)
216+
if (existingFile) {
217+
return existingFile
218+
}
219+
220+
dirPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE)
221+
long deadlineMillis = System.currentTimeMillis() + (timeoutSeconds * 1000)
222+
223+
while (System.currentTimeMillis() < deadlineMillis) {
224+
long remainingMillis = deadlineMillis - System.currentTimeMillis()
225+
if (remainingMillis <= 0) {
226+
break
227+
}
228+
229+
WatchKey key = watchService.poll(remainingMillis, TimeUnit.MILLISECONDS)
230+
if (key == null) {
231+
existingFile = findFlareFileIfExists(flareDir)
232+
if (existingFile) {
233+
return existingFile
234+
}
235+
break
236+
}
237+
238+
for (WatchEvent<?> event : key.pollEvents()) {
239+
WatchEvent<Path> pathEvent = (WatchEvent<Path>) event
240+
Path fileName = pathEvent.context()
241+
242+
if (isFlareFile(fileName.toString())) {
243+
return new File(flareDir, fileName.toString())
244+
}
245+
}
246+
247+
boolean valid = key.reset()
248+
if (!valid) {
249+
throw new AssertionError("Watch directory ${flareDir} is no longer accessible")
250+
}
251+
}
252+
253+
existingFile = findFlareFileIfExists(flareDir)
254+
if (existingFile) {
255+
return existingFile
256+
}
257+
258+
throw new AssertionError("No flare file created in ${flareDir} within ${timeoutSeconds} seconds")
259+
260+
} finally {
261+
watchService.close()
262+
}
263+
}
264+
265+
/**
266+
* Attempts to find an existing flare file in the directory.
267+
* Returns null if no flare file exists.
268+
*/
269+
private static File findFlareFileIfExists(File flareDir) {
270+
def flareFiles = flareDir.listFiles({ File dir, String name ->
271+
isFlareFile(name)
272+
} as FilenameFilter)
273+
274+
return flareFiles?.size() > 0 ? flareFiles.first() : null
275+
}
276+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ include(
215215
":dd-smoke-tests:springboot-tomcat",
216216
":dd-smoke-tests:springboot-tomcat-jsp",
217217
":dd-smoke-tests:springboot-velocity",
218+
":dd-smoke-tests:tracer-flare",
218219
":dd-smoke-tests:vertx-3.4",
219220
":dd-smoke-tests:vertx-3.9",
220221
":dd-smoke-tests:vertx-3.9-resteasy",

0 commit comments

Comments
 (0)