-
Notifications
You must be signed in to change notification settings - Fork 338
Implement Config Inversion with Default Strictness of Warning
#9181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fd62009
a756eb2
96e2546
6a87970
b062c96
6769bda
06ed2fd
8dd50e0
9660b1a
f8d2a8c
b68f0b2
dc5d83b
fe2eeb9
229c38d
77ddecd
eb6908a
972eef4
81c12f4
98c57b5
990c0ce
5bdd8cf
52c05b4
5192a8a
f128de8
734f9c7
e9ce233
90f536d
7a1517e
5ca61c0
abc0b64
97db41f
04bde01
9a764c9
e10e0a9
340eeef
ea83e25
4b602c1
4bdb960
962490d
c7aa94d
8068724
47a8f6a
94f392c
6c2a551
fe697ff
1aefa7e
9ef8c26
b8d8eca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| 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 = mutableListOf<String>() | ||
| 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) violations += "$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 = mutableListOf<String>() | ||
|
|
||
| javaFiles.forEach { f -> | ||
| val relative = repoRoot.relativize(f.toPath()) | ||
| f.readLines().forEachIndexed { idx, line -> | ||
| if (pattern.containsMatchIn(line)) { | ||
| matches += "$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) | ||
|
mhlidd marked this conversation as resolved.
|
||
| 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,14 @@ | ||
| package datadog.gradle.plugin.config | ||
|
|
||
| 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) { | ||
| val configOwnerPath = objects.property<String>(String::class.java).convention(":utils:config-utils") | ||
| val className = objects.property<String>(String::class.java).convention("datadog.config.GeneratedSupportedConfigurations") | ||
|
|
||
| val jsonFile = objects.fileProperty().convention(layout.projectDirectory.file("src/main/resources/supported-configurations.json")) | ||
|
|
||
| val destinationDirectory = objects.directoryProperty().convention(layout.buildDirectory.dir("generated/supportedConfigurations")) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| import datadog.communication.serialization.EncodingCache; | ||
| import datadog.communication.serialization.ValueWriter; | ||
| import datadog.communication.serialization.Writable; | ||
| import datadog.trace.util.Strings; | ||
| import datadog.config.util.Strings; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: Is it needed to move Also, need to check with bootstrap there, as some packages maybe relocated.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Will be updated once Config component migration is completed |
||
| import datadog.trace.util.stacktrace.StackTraceEvent; | ||
|
|
||
| public class StackTraceEventWriter implements ValueWriter<StackTraceEvent> { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.