From 6d95e60d73ccfc1008bccfb521cdaea6665a2cf5 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Thu, 25 Jun 2026 11:40:42 +0200 Subject: [PATCH] chore: Move golden file and hard jar numbers to metadata --- dd-java-agent/build.gradle | 68 +++++--- dd-java-agent/expected-integrations.txt | 208 ---------------------- docs/add_new_instrumentation.md | 12 +- metadata/agent-jar-checks.properties | 220 ++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 241 deletions(-) delete mode 100644 dd-java-agent/expected-integrations.txt create mode 100644 metadata/agent-jar-checks.properties diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 13214983663..4710bd5bc98 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -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 failures = [] Map 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 @@ -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. @@ -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. @@ -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 ? ' ...' : ''}") } } @@ -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, @@ -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") } @@ -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) @@ -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") } } diff --git a/dd-java-agent/expected-integrations.txt b/dd-java-agent/expected-integrations.txt deleted file mode 100644 index dc0d8bd7e7b..00000000000 --- a/dd-java-agent/expected-integrations.txt +++ /dev/null @@ -1,208 +0,0 @@ -IastInstrumentation -aerospike -akka-http -akka-http2 -akka_actor_mailbox -akka_actor_receive -akka_actor_send -akka_concurrent -allocatedirect -amqp -apache-httpclient -armeria-grpc-client -armeria-grpc-server -armeria-jetty -avro -aws-lambda -aws-sdk -axis2 -axway-api -azure-functions -caffeine -cassandra -ci-visibility -cics -classloading -commons-fileupload -commons-http-client -confluent-schema-registry -couchbase -cucumber -cxf -datanucleus -defineclass -dropwizard -dynamodb -elasticsearch -emr-aws-sdk -eventbridge -ffm-native-tracing -finatra -freemarker -gax -glassfish -google-http-client -google-pubsub -gradle -graphql-java -grizzly -grizzly-client -grizzly-filterchain -grpc -gson -guava -hazelcast -hazelcast_legacy -hibernate -httpasyncclient -httpasyncclient5 -httpclient -httpclient5 -httpcore -httpcore-5 -httpurlconnection -hystrix -ignite -inputStream -jackson -jackson-core -jacoco -jakarta-jms -jakarta-mail -jakarta-rs -jakarta-websocket -jakarta-ws -java-http-client -java-lang -java-lang-appsec -java-lang-management -java-module -java-net -java_completablefuture -java_concurrent -java_timer -javax-mail -javax-websocket -jax-rs -jax-ws -jboss-logmanager -jdbc -jdbc-datasource -jedis -jersey -jetty -jetty-client -jetty-concurrent -jms -jni -jsp -jwt -kafka -kotlin_coroutine -lettuce -liberty -log4j -logback -maven -micronaut -mmap -mongo -mule -native-image -netty -netty-concurrent -netty-promise -not-not-trace -ognl -okhttp -openai-java -opensearch -opentelemetry-annotations -opentelemetry-beta -opentelemetry-logs -opentelemetry-metrics -opentelemetry.experimental -opentracing -org-json -pekko-http -pekko-http2 -pekko_actor_mailbox -pekko_actor_receive -pekko_actor_send -play -play-ws -protobuf -quartz -ratpack -ratpack-request-body -reactive-streams -reactor-core -reactor-netty -rediscala -redisson -renaissance -resilience4j -resilience4j-reactor -resteasy -restlet-http -rmi -rxjava -s3 -scala_concurrent -servicetalk -servlet -servlet-filter -servlet-request-body -servlet-service -sfn -shutdown -slick -snakeyaml -sns -socket -sofarpc -spark -spark-executor -spark-exit -spark-launcher -spark-openlineage -sparkjava -spray-http -spring-async -spring-beans -spring-boot -spring-boot-span-origin -spring-cloud-zuul -spring-core -spring-data -spring-jms -spring-messaging -spring-rabbit -spring-scheduling -spring-security -spring-web -spring-web-code-origin -spring-webflux -spring-ws -spymemcached -sqs -sslsocket -synapse3-client -synapse3-server -testng -throwables -thymeleaf -tibco -tinylog -tomcat -trace -twilio-sdk -undertow -urlconnection -valkey -velocity -vertx -wallclock -websphere-jmx -wildfly -zio.experimental diff --git a/docs/add_new_instrumentation.md b/docs/add_new_instrumentation.md index 35343a56baa..27a851dd198 100644 --- a/docs/add_new_instrumentation.md +++ b/docs/add_new_instrumentation.md @@ -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 diff --git a/metadata/agent-jar-checks.properties b/metadata/agent-jar-checks.properties new file mode 100644 index 00000000000..fd04d637e35 --- /dev/null +++ b/metadata/agent-jar-checks.properties @@ -0,0 +1,220 @@ +# Agent jar structural invariants — edit intentionally, commit with the change that justified it. + +# Max agent jar size in bytes. Raise only when the size growth is intentional (33 MiB = 34603008). +jar.size.budget = 34603008 + +# Minimum combined class + classdata count in the assembled agent jar. +# Set to ~98% of the actual count at the time of the last intentional change. +# At time of writing: guard 17000 / actual 17279 +classes.minimum.guard = 17000 + +# BEGIN expected.integrations -- Regenerated by: ./gradlew :dd-java-agent:updateAgentJarIntegrationsGoldenFile +expected.integrations = IastInstrumentation,\ + aerospike,\ + akka-http,\ + akka-http2,\ + akka_actor_mailbox,\ + akka_actor_receive,\ + akka_actor_send,\ + akka_concurrent,\ + allocatedirect,\ + amqp,\ + apache-httpclient,\ + armeria-grpc-client,\ + armeria-grpc-server,\ + armeria-jetty,\ + avro,\ + aws-lambda,\ + aws-sdk,\ + axis2,\ + axway-api,\ + azure-functions,\ + caffeine,\ + cassandra,\ + ci-visibility,\ + cics,\ + classloading,\ + commons-fileupload,\ + commons-http-client,\ + confluent-schema-registry,\ + couchbase,\ + cucumber,\ + cxf,\ + datanucleus,\ + defineclass,\ + dropwizard,\ + dynamodb,\ + elasticsearch,\ + emr-aws-sdk,\ + eventbridge,\ + ffm-native-tracing,\ + finatra,\ + freemarker,\ + gax,\ + glassfish,\ + google-http-client,\ + google-pubsub,\ + gradle,\ + graphql-java,\ + grizzly,\ + grizzly-client,\ + grizzly-filterchain,\ + grpc,\ + gson,\ + guava,\ + hazelcast,\ + hazelcast_legacy,\ + hibernate,\ + httpasyncclient,\ + httpasyncclient5,\ + httpclient,\ + httpclient5,\ + httpcore,\ + httpcore-5,\ + httpurlconnection,\ + hystrix,\ + ignite,\ + inputStream,\ + jackson,\ + jackson-core,\ + jacoco,\ + jakarta-jms,\ + jakarta-mail,\ + jakarta-rs,\ + jakarta-websocket,\ + jakarta-ws,\ + java-http-client,\ + java-lang,\ + java-lang-appsec,\ + java-lang-management,\ + java-module,\ + java-net,\ + java_completablefuture,\ + java_concurrent,\ + java_timer,\ + javax-mail,\ + javax-websocket,\ + jax-rs,\ + jax-ws,\ + jboss-logmanager,\ + jdbc,\ + jdbc-datasource,\ + jedis,\ + jersey,\ + jetty,\ + jetty-client,\ + jetty-concurrent,\ + jms,\ + jni,\ + jsp,\ + jwt,\ + kafka,\ + kotlin_coroutine,\ + lettuce,\ + liberty,\ + log4j,\ + logback,\ + maven,\ + micronaut,\ + mmap,\ + mongo,\ + mule,\ + native-image,\ + netty,\ + netty-concurrent,\ + netty-promise,\ + not-not-trace,\ + ognl,\ + okhttp,\ + openai-java,\ + opensearch,\ + opentelemetry-annotations,\ + opentelemetry-beta,\ + opentelemetry-logs,\ + opentelemetry-metrics,\ + opentelemetry.experimental,\ + opentracing,\ + org-json,\ + pekko-http,\ + pekko-http2,\ + pekko_actor_mailbox,\ + pekko_actor_receive,\ + pekko_actor_send,\ + play,\ + play-ws,\ + protobuf,\ + quartz,\ + ratpack,\ + ratpack-request-body,\ + reactive-streams,\ + reactor-core,\ + reactor-netty,\ + rediscala,\ + redisson,\ + renaissance,\ + resilience4j,\ + resilience4j-reactor,\ + resteasy,\ + restlet-http,\ + rmi,\ + rxjava,\ + s3,\ + scala_concurrent,\ + servicetalk,\ + servlet,\ + servlet-filter,\ + servlet-request-body,\ + servlet-service,\ + sfn,\ + shutdown,\ + slick,\ + snakeyaml,\ + sns,\ + socket,\ + sofarpc,\ + spark,\ + spark-executor,\ + spark-exit,\ + spark-launcher,\ + spark-openlineage,\ + sparkjava,\ + spray-http,\ + spring-async,\ + spring-beans,\ + spring-boot,\ + spring-boot-span-origin,\ + spring-cloud-zuul,\ + spring-core,\ + spring-data,\ + spring-jms,\ + spring-messaging,\ + spring-rabbit,\ + spring-scheduling,\ + spring-security,\ + spring-web,\ + spring-web-code-origin,\ + spring-webflux,\ + spring-ws,\ + spymemcached,\ + sqs,\ + sslsocket,\ + synapse3-client,\ + synapse3-server,\ + testng,\ + throwables,\ + thymeleaf,\ + tibco,\ + tinylog,\ + tomcat,\ + trace,\ + twilio-sdk,\ + undertow,\ + urlconnection,\ + valkey,\ + velocity,\ + vertx,\ + wallclock,\ + websphere-jmx,\ + wildfly,\ + zio.experimental +# END expected.integrations