diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index fc37a6b8ccb..9ec83bbb1be 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -30,6 +30,14 @@ gradlePlugin { id = "tracer-version" implementationClass = "datadog.gradle.plugin.version.TracerVersionPlugin" } + create("supported-config-generation") { + id = "supported-config-generator" + implementationClass = "datadog.gradle.plugin.config.SupportedConfigPlugin" + } + create("supported-config-linter") { + id = "config-inversion-linter" + implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter" + } } } @@ -52,6 +60,11 @@ dependencies { implementation("com.google.guava", "guava", "20.0") implementation("org.ow2.asm", "asm", "9.8") implementation("org.ow2.asm", "asm-tree", "9.8") + + implementation(platform("com.fasterxml.jackson:jackson-bom:2.17.2")) + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.core:jackson-annotations") + implementation("com.fasterxml.jackson.core:jackson-core") } tasks.compileKotlin { diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt new file mode 100644 index 00000000000..3b78bc6ebae --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ConfigInversionLinter.kt @@ -0,0 +1,128 @@ +package datadog.gradle.plugin.config + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.GradleException +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.internal.impldep.kotlinx.metadata.impl.extensions.KmExtension +import org.gradle.kotlin.dsl.accessors.runtime.externalModuleDependencyFor +import org.gradle.kotlin.dsl.getByType +import java.net.URLClassLoader +import java.nio.file.Path + +class ConfigInversionLinter : Plugin { + override fun apply(target: Project) { + val extension = target.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java) + registerLogEnvVarUsages(target, extension) + registerCheckEnvironmentVariablesUsage(target) + } +} + +/** Registers `logEnvVarUsages` (scan for DD_/OTEL_ tokens and fail if unsupported). */ +private fun registerLogEnvVarUsages(target: Project, extension: SupportedTracerConfigurations) { + val ownerPath = extension.configOwnerPath + val generatedFile = extension.className + + // token check that uses the generated class instead of JSON + target.tasks.register("logEnvVarUsages") { + group = "verification" + description = "Scan Java files for DD_/OTEL_ tokens and fail if unsupported (using generated constants)" + + val mainSourceSetOutput = ownerPath.map { + target.project(it) + .extensions.getByType() + .named(SourceSet.MAIN_SOURCE_SET_NAME) + .map { main -> main.output } + } + inputs.files(mainSourceSetOutput) + + // inputs for incrementality (your own source files, not the owner’s) + val javaFiles = target.fileTree(target.projectDir) { + include("**/src/main/java/**/*.java") + exclude("**/build/**", "**/dd-smoke-tests/**") + } + inputs.files(javaFiles) + outputs.upToDateWhen { true } + doLast { + // 1) Build classloader from the owner project’s runtime classpath + val urls = mainSourceSetOutput.get().get().files.map { it.toURI().toURL() }.toTypedArray() + val supported: Set = URLClassLoader(urls, javaClass.classLoader).use { cl -> + // 2) Load the generated class + read static field + val clazz = Class.forName(generatedFile.get(), true, cl) + @Suppress("UNCHECKED_CAST") + clazz.getField("SUPPORTED").get(null) as Set + } + + // 3) Scan our sources and compare + val repoRoot = target.projectDir.toPath() + val tokenRegex = Regex("\"(?:DD_|OTEL_)[A-Za-z0-9_]+\"") + + val violations = buildList { + javaFiles.files.forEach { f -> + val rel = repoRoot.relativize(f.toPath()).toString() + var inBlock = false + f.readLines().forEachIndexed { i, raw -> + val trimmed = raw.trim() + if (trimmed.startsWith("//")) return@forEachIndexed + if (!inBlock && trimmed.contains("/*")) inBlock = true + if (inBlock) { + if (trimmed.contains("*/")) inBlock = false + return@forEachIndexed + } + tokenRegex.findAll(raw).forEach { m -> + val token = m.value.trim('"') + if (token !in supported) add("$rel:${i + 1} -> Unsupported token'$token'") + } + } + } + } + + if (violations.isNotEmpty()) { + violations.forEach { target.logger.error(it) } + throw GradleException("Unsupported DD_/OTEL_ tokens found! See errors above.") + } else { + target.logger.info("All DD_/OTEL_ tokens are supported.") + } + } + } +} + +/** Registers `checkEnvironmentVariablesUsage` (forbid EnvironmentVariables.get(...)). */ +private fun registerCheckEnvironmentVariablesUsage(project: Project) { + project.tasks.register("checkEnvironmentVariablesUsage") { + group = "verification" + description = "Scans src/main/java for direct usages of EnvironmentVariables.get(...)" + + doLast { + val repoRoot: Path = project.projectDir.toPath() + val javaFiles = project.fileTree(project.projectDir) { + include("**/src/main/java/**/*.java") + exclude("**/build/**") + exclude("internal-api/src/main/java/datadog/trace/api/ConfigHelper.java") + exclude("dd-java-agent/agent-bootstrap/**") + exclude("dd-java-agent/src/main/java/datadog/trace/bootstrap/BootstrapInitializationTelemetry.java") + } + + val pattern = Regex("""EnvironmentVariables\.get\s*\(""") + val matches = buildList { + javaFiles.forEach { f -> + val relative = repoRoot.relativize(f.toPath()) + f.readLines().forEachIndexed { idx, line -> + if (pattern.containsMatchIn(line)) { + add("$relative:${idx + 1} -> ${line.trim()}") + } + } + } + } + + if (matches.isNotEmpty()) { + project.logger.lifecycle("\nFound forbidden usages of EnvironmentVariables.get(...):") + matches.forEach { project.logger.lifecycle(it) } + throw GradleException("Forbidden usage of EnvironmentVariables.get(...) found in Java files.") + } else { + project.logger.info("No forbidden EnvironmentVariables.get(...) usages found in src/main/java.") + } + } + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseSupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseSupportedConfigurationsTask.kt new file mode 100644 index 00000000000..0864efbcbdc --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseSupportedConfigurationsTask.kt @@ -0,0 +1,157 @@ +package datadog.gradle.plugin.config + +import org.gradle.api.DefaultTask +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import java.io.File +import java.io.FileInputStream +import java.io.PrintWriter +import javax.inject.Inject + +@CacheableTask +abstract class ParseSupportedConfigurationsTask @Inject constructor( + private val objects: ObjectFactory +) : DefaultTask() { + @InputFile + @PathSensitive(PathSensitivity.NONE) + val jsonFile = objects.fileProperty() + + @get:OutputDirectory + val destinationDirectory = objects.directoryProperty() + + @Input + val className = objects.property(String::class.java) + + @TaskAction + fun generate() { + val input = jsonFile.get().asFile + val outputDir = destinationDirectory.get().asFile + val finalClassName = className.get() + outputDir.mkdirs() + + // Read JSON (directly from the file, not classpath) + val mapper = ObjectMapper() + val fileData: Map = FileInputStream(input).use { inStream -> + mapper.readValue(inStream, object : TypeReference>() {}) + } + + @Suppress("UNCHECKED_CAST") + val supported = fileData["supportedConfigurations"] as Map> + @Suppress("UNCHECKED_CAST") + val aliases = fileData["aliases"] as Map> + @Suppress("UNCHECKED_CAST") + val deprecated = (fileData["deprecations"] as? Map) ?: emptyMap() + + val aliasMapping = mutableMapOf() + for ((canonical, alist) in aliases) { + for (alias in alist) aliasMapping[alias] = canonical + } + + // Build the output .java path from the fully-qualified class name + val pkgName = finalClassName.substringBeforeLast('.', "") + val pkgPath = pkgName.replace('.', File.separatorChar) + val simpleName = finalClassName.substringAfterLast('.') + val pkgDir = if (pkgPath.isEmpty()) outputDir else File(outputDir, pkgPath).also { it.mkdirs() } + val generatedFile = File(pkgDir, "$simpleName.java").absolutePath + + // Call your existing generator (same signature as in your Java code) + generateJavaFile( + generatedFile, + simpleName, + pkgName, + supported.keys, + aliases, + aliasMapping, + deprecated + ) + } + + private fun generateJavaFile( + outputPath: String, + className: String, + packageName: String, + supportedKeys: Set, + aliases: Map>, + aliasMapping: Map, + deprecated: Map + ) { + val outFile = File(outputPath) + outFile.parentFile?.mkdirs() + + PrintWriter(outFile).use { out -> + // NOTE: adjust these if you want to match task's className + out.println("package $packageName;") + out.println() + out.println("import java.util.*;") + out.println() + out.println("public final class $className {") + out.println() + out.println(" public static final Set SUPPORTED;") + out.println() + out.println(" public static final Map> ALIASES;") + out.println() + out.println(" public static final Map ALIAS_MAPPING;") + out.println() + out.println(" public static final Map DEPRECATED;") + out.println() + out.println(" static {") + out.println() + + // SUPPORTED + out.print(" Set supportedSet = new HashSet<>(Arrays.asList(") + val supportedIter = supportedKeys.toSortedSet().iterator() + while (supportedIter.hasNext()) { + val key = supportedIter.next() + out.print("\"${esc(key)}\"") + if (supportedIter.hasNext()) out.print(", ") + } + out.println("));") + out.println(" SUPPORTED = Collections.unmodifiableSet(supportedSet);") + out.println() + + // ALIASES + out.println(" Map> aliasesMap = new HashMap<>();") + for ((canonical, list) in aliases.toSortedMap()) { + out.printf( + " aliasesMap.put(\"%s\", Collections.unmodifiableList(Arrays.asList(%s)));\n", + esc(canonical), + quoteList(list) + ) + } + out.println(" ALIASES = Collections.unmodifiableMap(aliasesMap);") + out.println() + + // ALIAS_MAPPING + out.println(" Map aliasMappingMap = new HashMap<>();") + for ((alias, target) in aliasMapping.toSortedMap()) { + out.printf(" aliasMappingMap.put(\"%s\", \"%s\");\n", esc(alias), esc(target)) + } + out.println(" ALIAS_MAPPING = Collections.unmodifiableMap(aliasMappingMap);") + out.println() + + // DEPRECATED + out.println(" Map deprecatedMap = new HashMap<>();") + for ((oldKey, note) in deprecated.toSortedMap()) { + out.printf(" deprecatedMap.put(\"%s\", \"%s\");\n", esc(oldKey), esc(note)) + } + out.println(" DEPRECATED = Collections.unmodifiableMap(deprecatedMap);") + out.println() + out.println(" }") + out.println("}") + } + } + + private fun quoteList(list: List): String = + list.joinToString(", ") { "\"${esc(it)}\"" } + + private fun esc(s: String): String = + s.replace("\\", "\\\\").replace("\"", "\\\"") +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedConfigPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedConfigPlugin.kt new file mode 100644 index 00000000000..598d566bdf0 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedConfigPlugin.kt @@ -0,0 +1,27 @@ +package datadog.gradle.plugin.config + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer + +class SupportedConfigPlugin : Plugin { + override fun apply(targetProject: Project) { + val extension = targetProject.extensions.create("supportedTracerConfigurations", SupportedTracerConfigurations::class.java) + generateSupportedConfigurations(targetProject, extension) + } + + private fun generateSupportedConfigurations(targetProject: Project, extension: SupportedTracerConfigurations) { + val generateTask = + targetProject.tasks.register("generateSupportedConfigurations", ParseSupportedConfigurationsTask::class.java) { + jsonFile.set(extension.jsonFile) + destinationDirectory.set(extension.destinationDirectory) + className.set(extension.className) + } + + val sourceset = targetProject.extensions.getByType(SourceSetContainer::class.java).named(SourceSet.MAIN_SOURCE_SET_NAME) + sourceset.configure { + java.srcDir(generateTask) + } + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedTracerConfigurations.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedTracerConfigurations.kt new file mode 100644 index 00000000000..cb93cb1b84e --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/SupportedTracerConfigurations.kt @@ -0,0 +1,15 @@ +package datadog.gradle.plugin.config + +import org.gradle.api.Project +import org.gradle.api.file.ProjectLayout +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +open class SupportedTracerConfigurations @Inject constructor(objects: ObjectFactory, layout: ProjectLayout, project: Project) { + val configOwnerPath = objects.property(String::class.java).convention(":utils:config-utils") + val className = objects.property(String::class.java).convention("datadog.trace.config.inversion.GeneratedSupportedConfigurations") + + val jsonFile = objects.fileProperty().convention(project.rootProject.layout.projectDirectory.file("metadata/supported-configurations.json")) + + val destinationDirectory = objects.directoryProperty().convention(layout.buildDirectory.dir("generated/supportedConfigurations")) +} diff --git a/dd-java-agent/build.gradle b/dd-java-agent/build.gradle index 6b86d0ba802..babf0678b41 100644 --- a/dd-java-agent/build.gradle +++ b/dd-java-agent/build.gradle @@ -302,6 +302,7 @@ dependencies { sharedShadowInclude project(':remote-config:remote-config-core'), { transitive = false } + sharedShadowInclude project(':utils:container-utils'), { transitive = false } diff --git a/utils/config-utils/build.gradle.kts b/utils/config-utils/build.gradle.kts index 9ead8f2137b..ccf2e03f6fb 100644 --- a/utils/config-utils/build.gradle.kts +++ b/utils/config-utils/build.gradle.kts @@ -1,5 +1,6 @@ plugins { `java-library` + id("supported-config-generator") } apply(from = "$rootDir/gradle/java.gradle")