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
68 changes: 41 additions & 27 deletions dd-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -475,24 +475,30 @@ tasks.withType(Test).configureEach {
dependsOn "shadowJar"
}

def agentJarChecksProps = rootProject.file('metadata/agent-jar-checks.properties')

tasks.register('verifyAgentJarContents') {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Verify the agent jar contains required entries and meets structural invariants'

def jarProvider = tasks.named('shadowJar', ShadowJar).flatMap { it.archiveFile }
inputs.file(jarProvider)
inputs.file(agentJarChecksProps)
inputs.property('productPrefixes', includedProductPrefixes)
outputs.file(project.layout.buildDirectory.file("tmp/${it.name}/.verified"))

doLast {
def props = new Properties()
agentJarChecksProps.withInputStream { props.load(it) }

File jarFile = jarProvider.get().asFile
List<String> failures = []
Map<String, Long> entries = [:]

// Jar size ceiling — raise only when the growth is intentional
def sizeCeiling = 33L * 1024 * 1024
if (jarFile.length() > sizeCeiling) {
failures << "Jar size ${jarFile.length()} B exceeds ceiling ${sizeCeiling} B (33 MiB)"
// Jar size budget — raise only when the growth is intentional
def sizeBudget = Long.parseLong(props['jar.size.budget'])
if (jarFile.length() > sizeBudget) {
failures.add("Jar size ${jarFile.length()} B exceeds budget ${sizeBudget} B")
}

// Inspect jar content
Expand Down Expand Up @@ -524,15 +530,15 @@ tasks.register('verifyAgentJarContents') {
'META-INF/maven/com.datadoghq/dd-java-agent/pom.properties',
].each { required ->
if (!entries.containsKey(required)) {
failures << "Missing required entry: ${required}"
failures.add("Missing required entry: ${required}")
}
}

// Sanity check on the minimum number of classes; update as needed. Set to about 98% of that number.
// Sanity check on the minimum number of classes; see metadata/agent-jar-checks.properties.
def classCount = entries.keySet().count { it.endsWith('.class') || it.endsWith('.classdata') }
def classFloor = 17_000 // a bit more than 98% of 17,279 at time of writing
def classFloor = Integer.parseInt(props['classes.minimum.guard'])
if (classCount < classFloor) {
failures << "Class count ${classCount} is below floor ${classFloor}"
failures.add("Class count ${classCount} is below floor ${classFloor}")
}

// Each registered product must contribute at least one .classdata entry.
Expand All @@ -543,13 +549,13 @@ tasks.register('verifyAgentJarContents') {
.toSet()
includedProductPrefixes.get().each { dir ->
if (!classdataPrefixes.contains(dir)) {
failures << "Product '${dir}' has no .classdata entries in the assembled jar"
failures.add("Product '${dir}' has no .classdata entries in the assembled jar")
}
}

// All *.index files in the jar must be non-empty
entries.findAll { name, size -> name.endsWith('.index') && size == 0 }.each { name, _ ->
failures << "Empty index file: ${name}"
failures.add("Empty index file: ${name}")
}

// Packages that must not appear anywhere in the jar after relocation.
Expand All @@ -561,7 +567,7 @@ tasks.register('verifyAgentJarContents') {
}
if (!leaked.empty) {
def sample = leaked.take(3).toString()
failures << "Unrelocated package '${pkg}': ${sample}${leaked.size() > 3 ? ' ...' : ''}"
failures.add("Unrelocated package '${pkg}': ${sample}${leaked.size() > 3 ? ' ...' : ''}")
}
}

Expand All @@ -577,15 +583,13 @@ tasks.register('verifyAgentJarContents') {
}
}

def integrationsGoldenFile = project.file('expected-integrations.txt')

tasks.register('verifyAgentJarIntegrations', JavaExec) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Verify the agent jar lists exactly the integrations in expected-integrations.txt'
description = 'Verify the agent jar lists exactly the integrations in metadata/agent-jar-checks.properties'

def jarProvider = tasks.named('shadowJar', ShadowJar).flatMap { it.archiveFile }
inputs.file(jarProvider)
inputs.file(integrationsGoldenFile)
inputs.file(agentJarChecksProps)
outputs.file(project.layout.buildDirectory.file("tmp/${it.name}/.verified"))

// Run the assembled agent jar directly — this exercises dd-java-agent.index,
Expand All @@ -609,19 +613,15 @@ tasks.register('verifyAgentJarIntegrations', JavaExec) {
"(likely a module load failure; see InstrumenterIndex.buildModule):\n${stderr}")
}

if (!integrationsGoldenFile.exists()) {
throw new GradleException(
"${integrationsGoldenFile.name} not found. " +
"Run './gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile' to create it.")
}

def props = new Properties()
agentJarChecksProps.withInputStream { props.load(it) }
def actual = capturedOutput.toString().readLines().findAll { !it.isBlank() }.toSorted()
def expected = integrationsGoldenFile.readLines().findAll { !it.isBlank() }.toSorted()
def expected = props.getProperty('expected.integrations').split(',').collect { it.trim() }.toSorted()
def added = actual - expected
def removed = expected - actual

if (added || removed) {
def msg = new StringBuilder("Integration list differs from ${integrationsGoldenFile.name}.")
def msg = new StringBuilder('Integration list differs from metadata/agent-jar-checks.properties.')
msg.append(" Run './gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile' to update it.\n")
added.each { msg.append(" + ${it}\n") }
removed.each { msg.append(" - ${it}\n") }
Expand All @@ -634,10 +634,11 @@ tasks.register('verifyAgentJarIntegrations', JavaExec) {
}
}

// Manual run after adding/removing integrations to update expected-integrations.txt, then add with the new integration.
// Manual run after adding/removing integrations; rewrites the expected.integrations block in
// metadata/agent-jar-checks.properties while preserving all other properties and comments.
tasks.register('updateAgentJarIntegrationsGoldenFile', JavaExec) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = 'Regenerate expected-integrations.txt from the current agent jar'
description = 'Regenerate expected.integrations in metadata/agent-jar-checks.properties from the current agent jar'

def jarProvider = tasks.named('shadowJar', ShadowJar).flatMap { it.archiveFile }
inputs.file(jarProvider)
Expand All @@ -651,8 +652,21 @@ tasks.register('updateAgentJarIntegrationsGoldenFile', JavaExec) {

doLast {
def integrations = capturedOutput.toString().readLines().findAll { !it.isBlank() }.toSorted()
integrationsGoldenFile.text = integrations.join('\n') + '\n'
logger.lifecycle("Updated ${integrationsGoldenFile.name} with ${integrations.size()} integrations")

def entryLines = integrations.withIndex().collect { name, i ->
def prefix = (i == 0) ? 'expected.integrations = ' : ' '
def suffix = (i < integrations.size() - 1) ? ',\\' : ''
"${prefix}${name}${suffix}"
}

// Replace everything between the BEGIN/END markers (inclusive) with the fresh value.
def BEGIN = '# BEGIN expected.integrations'
def END = '# END expected.integrations'
def allLines = agentJarChecksProps.readLines()
def before = allLines.subList(0, allLines.findIndexOf { it.startsWith(BEGIN) })
def after = allLines.subList(allLines.findIndexOf { it.startsWith(END) } + 1, allLines.size())
agentJarChecksProps.text = (before + [BEGIN] + entryLines + [END] + after).join('\n') + '\n'
logger.lifecycle("Updated metadata/agent-jar-checks.properties with ${integrations.size()} integrations")
}
}

Expand Down
208 changes: 0 additions & 208 deletions dd-java-agent/expected-integrations.txt

This file was deleted.

12 changes: 6 additions & 6 deletions docs/add_new_instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,18 +451,18 @@ There are four verification strategies, three of which are mandatory.

### Agent jar integrations golden file

The agent jar check task `checkAgentJarIntegrations` verifies that the set of integrations
listed in `dd-java-agent/expected-integrations.txt` exactly matches the integrations
shipped in the built agent jar. This catches accidental additions or removals.
The agent jar check task `verifyAgentJarIntegrations` verifies that the set of integrations
listed under the `expected.integrations` key in `metadata/agent-jar-checks.properties`
exactly matches the integrations shipped in the built agent jar. This catches accidental additions or removals.

When you add or remove an integration, update the golden file:
When you add or remove an integration, update the properties file:

```shell
./gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile
```

Then commit `dd-java-agent/expected-integrations.txt` alongside your instrumentation changes.
The `check` task runs `checkAgentJarIntegrations` automatically, so CI will fail if the file
Then commit `metadata/agent-jar-checks.properties` alongside your instrumentation changes.
The `check` task runs `verifyAgentJarIntegrations` automatically, so CI will fail if the file
is out of date.

All integrations must include sufficient test coverage. This HTTP client integration will include
Expand Down
Loading