Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
13 changes: 13 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Project> {
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<SourceSetContainer>()
.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<String> = 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<String>
}
Comment on lines +50 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: I believe this might be more efficient to parse the json file rather than wait for generatedFile (actually a generatedClass) to be compiled.


// 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.")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Any?> = FileInputStream(input).use { inStream ->
mapper.readValue(inStream, object : TypeReference<Map<String, Any?>>() {})
}

@Suppress("UNCHECKED_CAST")
val supported = fileData["supportedConfigurations"] as Map<String, List<String>>
@Suppress("UNCHECKED_CAST")
val aliases = fileData["aliases"] as Map<String, List<String>>
@Suppress("UNCHECKED_CAST")
val deprecated = (fileData["deprecations"] as? Map<String, String>) ?: emptyMap()

val aliasMapping = mutableMapOf<String, String>()
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<String>,
aliases: Map<String, List<String>>,
aliasMapping: Map<String, String>,
deprecated: Map<String, String>
) {
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;")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 suggestion: ‏I would recommend using Kotlin multiline strings for readability.

Copy link
Contributor

@bric3 bric3 Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

polish: There's also the buildString { } but that's basically some sugar around a StringBuilder. The multiline, looks like a nice improvement. But could be done in a follow-up.

out.println()
out.println("import java.util.*;")
out.println()
out.println("public final class $className {")
out.println()
out.println(" public static final Set<String> SUPPORTED;")
out.println()
out.println(" public static final Map<String, List<String>> ALIASES;")
out.println()
out.println(" public static final Map<String, String> ALIAS_MAPPING;")
out.println()
out.println(" public static final Map<String, String> DEPRECATED;")
out.println()
out.println(" static {")
out.println()

// SUPPORTED
out.print(" Set<String> 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<String, List<String>> 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<String, String> 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<String, String> 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>): String =
list.joinToString(", ") { "\"${esc(it)}\"" }

private fun esc(s: String): String =
s.replace("\\", "\\\\").replace("\"", "\\\"")
}
Original file line number Diff line number Diff line change
@@ -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<Project> {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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>(String::class.java).convention(":utils:config-utils")
val className = objects.property<String>(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"))
}
4 changes: 4 additions & 0 deletions dd-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ def includeShadowJar(TaskProvider<ShadowJar> shadowJarTask, String jarname) {

from(zipTree(shadowJarTask.get().archiveFile)) {
into jarname
// Excludes the repeated generated files :
// ${jarname}/datadog/trace/config/inversion/GeneratedSupportedConfigurations.classdata
exclude 'datadog/trace/config/inversion/**'
rename '(^.*)\\.class$', '$1.classdata'
// Rename LICENSE file since it clashes with license dir on non-case sensitive FSs (i.e. Mac)
rename '^LICENSE$', 'LICENSE.renamed'
Expand Down Expand Up @@ -302,6 +305,7 @@ dependencies {
sharedShadowInclude project(':remote-config:remote-config-core'), {
transitive = false
}

sharedShadowInclude project(':utils:container-utils'), {
transitive = false
}
Expand Down
1 change: 1 addition & 0 deletions utils/config-utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
`java-library`
id("supported-config-generator")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: Just some tip about kotlin-dsl, it also possible to refer to that plugin with the generated accessor supported-config-generator which is exactly the same as id("supported-config-generator").

Note this code is fine and you can close the comment.

}

apply(from = "$rootDir/gradle/java.gradle")
Expand Down
Loading