Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b91074c
Reference to scheduled action should be inside task to avoid race con…
AlexeyKuznetsov-DD Sep 22, 2025
7b1d89d
Bump ddprof to 1.32.0 (#9584)
zhengyu123 Sep 22, 2025
a519ee2
Enable process tags collection by default (#9510)
raphaelgavache Sep 22, 2025
0094a60
Defer metrics aggregator classloading to save startup time (#9590)
amarziali Sep 23, 2025
aadd471
Add support for JUnit 6.0.0-RC3 (#9592)
daniel-mohedano Sep 23, 2025
6ee44dd
Allow pre-Java 6 classes to be transformed in the debugger (#9591)
jpbempel Sep 23, 2025
e992b65
Exclude config-utils from shadowed sub-projects (avoids duplicate jar…
mcculls Sep 23, 2025
27641db
Fix symbol extraction for interface static method (#9597)
jpbempel Sep 23, 2025
5c66a15
ConfigProvider iterates over all sources and reports all non-null val…
mtoffl01 Sep 23, 2025
70cfa2d
Adding Gradle Plugins for Config Inversion (#9565)
mhlidd Sep 23, 2025
11e677e
Raised `relativeAccuracy` to `0.2` since `0.1` causes `~16%` random f…
AlexeyKuznetsov-DD Sep 23, 2025
274d144
feat(tooling): Add community contribution PR mirroring (#9602)
PerfectSlayer Sep 24, 2025
d76b984
Remove extra jersey smoke test module (#9599)
sarahchen6 Sep 24, 2025
46f4b13
[DJM-964] Add Spark physical plan with spark.sql spans (#9600)
charlesmyu Sep 24, 2025
1450a11
Fix Gradle Dependencies with Javadoc task (#9604)
mhlidd Sep 24, 2025
9f07fa1
Convert configurations syntax to use lazy API (#9606)
sarahchen6 Sep 24, 2025
9ac9a56
Migrate tests and `ConfigInversionMetricCollector` to `config-utils` …
mhlidd Sep 24, 2025
b09dcc5
Remove extra `.configure` method (#9609)
sarahchen6 Sep 24, 2025
140e25d
Refactored EnvironmentVariables to be testable.
AlexeyKuznetsov-DD Sep 20, 2025
2b8305e
WIP.
AlexeyKuznetsov-DD Sep 22, 2025
0969a7d
adding supported-configurations.json file
mhlidd Sep 18, 2025
0eda863
removing extra supported-configurations.json
mhlidd Sep 23, 2025
d4cf4fb
migrating config-utils tests and ConfigInversionMetric telemetry
mhlidd Sep 18, 2025
76281ab
config inversion init
mhlidd Sep 15, 2025
83f0f98
migrating config-utils tests
mhlidd Sep 15, 2025
e9f9689
undo move of test files that rely on inject*config
mhlidd Sep 16, 2025
9625dd4
adding deprecation handling
mhlidd Sep 16, 2025
a2926f9
updating tests
mhlidd Sep 16, 2025
bcb6db6
spotless
mhlidd Sep 16, 2025
31fc7b4
excluding json from shadowjar
mhlidd Sep 19, 2025
71e309f
updating gradle files
mhlidd Sep 19, 2025
c65460a
responding to PR comments
mhlidd Sep 19, 2025
a4de955
updating class coverage exclude
mhlidd Sep 19, 2025
cad575c
updating ConfigHelper to be a singleton
mhlidd Sep 23, 2025
5fe0a88
refactoring ConfigHelper and ConfigurationSources to simplify code re…
mhlidd Sep 23, 2025
4c924eb
responding to PR comments and refactoring ConfigHelperTest to utilize…
mhlidd Sep 25, 2025
3bbf486
updating PR comments
mhlidd Sep 25, 2025
8ba684e
cleanup and code coverage
mhlidd Sep 25, 2025
cad4c24
bugfix
mhlidd Oct 1, 2025
11fc52b
responding to PR comments
mhlidd Oct 6, 2025
63e5d5a
fixing issue with EmptyMap
mhlidd Oct 6, 2025
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
Expand Up @@ -98,7 +98,7 @@ abstract class CallSiteInstrumentationPlugin : Plugin<Project>{
extendsFrom(project.configurations.named(mainSourceSet.compileClasspathConfigurationName).get())
}

project.tasks.named(csiSourceSet.getCompileTaskName("java"), AbstractCompile::class.java).configure {
project.tasks.named(csiSourceSet.getCompileTaskName("java"), AbstractCompile::class.java) {
sourceCompatibility = JavaVersion.VERSION_1_8.toString()
targetCompatibility = JavaVersion.VERSION_1_8.toString()
}
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>
}

// 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;")
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"))
}
Loading
Loading