Skip to content
Open
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ config-inversion-linter:
needs: []
script:
- ./gradlew --version
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings
- ./gradlew logEnvVarUsages checkEnvironmentVariablesUsage checkConfigStrings checkInstrumenterModuleConfigurations checkDecoratorAnalyticsConfigurations

test_published_artifacts:
extends: .gradle_build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ import com.github.javaparser.ParserConfiguration
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
import com.github.javaparser.ast.body.FieldDeclaration
import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.body.VariableDeclarator
import com.github.javaparser.ast.expr.StringLiteralExpr
import com.github.javaparser.ast.nodeTypes.NodeWithModifiers
import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt
import com.github.javaparser.ast.stmt.ReturnStmt
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.getByType
import java.net.URLClassLoader
import java.nio.file.Path
Expand All @@ -23,13 +33,16 @@ class ConfigInversionLinter : Plugin<Project> {
registerLogEnvVarUsages(target, extension)
registerCheckEnvironmentVariablesUsage(target)
registerCheckConfigStringsTask(target, extension)
registerCheckInstrumenterModuleConfigurations(target, extension)
registerCheckDecoratorAnalyticsConfigurations(target, extension)
Comment on lines +36 to +37
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

todo: the task changes looks good, now I think the tsk should not "leak" into the gitlab file.

So I propose to wire the task as dependencies. Rereading this I think you could create a dumb lifecycle task, e.g. checkConfigurations, that depends on the other tasks.

tasks.register("checkConfigurations") {
  depends(
    checkInstrumenterModuleConfigurationsTaskProvider,
    //...
  )
}

}
}

// Data class for fields from generated class
private data class LoadedConfigFields(
data class LoadedConfigFields(
val supported: Set<String>,
val aliasMapping: Map<String, String> = emptyMap()
val aliasMapping: Map<String, String> = emptyMap(),
val aliases: Map<String, List<String>> = emptyMap()
)

// Cache for fields from generated class
Expand All @@ -55,7 +68,9 @@ private fun loadConfigFields(

@Suppress("UNCHECKED_CAST")
val aliasMappingMap = clazz.getField("ALIAS_MAPPING").get(null) as Map<String, String>
LoadedConfigFields(supportedSet, aliasMappingMap)
@Suppress("UNCHECKED_CAST")
val aliasesMap = clazz.getField("ALIASES").get(null) as Map<String, List<String>>
LoadedConfigFields(supportedSet, aliasMappingMap, aliasesMap)
}.also { cachedConfigFields = it }
}
}
Expand Down Expand Up @@ -248,3 +263,206 @@ private fun registerCheckConfigStringsTask(project: Project, extension: Supporte
}
}
}

/** Collects violations for [key] against [supported] and [aliases], checking that all [expectedAliases] are values of that alias entry. */
private fun collectMissingKeysAndAliases(
key: String,
expectedAliases: List<String>,
supported: Set<String>,
aliases: Map<String, List<String>>,
location: String,
context: String
): List<String> = buildList {
if (key !in supported) {
add("$location -> $context: '$key' is missing from SUPPORTED")
}
if (key !in aliases) {
add("$location -> $context: '$key' is missing from ALIASES")
} else {
val aliasValues = aliases[key] ?: emptyList()
for (expected in expectedAliases) {
if (expected !in aliasValues) {
add("$location -> $context: '$expected' is missing from ALIASES['$key']")
}
}
}
}

/** Abstract base for tasks that scan instrumentation source files against the generated config class. */
abstract class InstrumentationConfigCheckTask : DefaultTask() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick: By the way I would move these classes in their own files. (Maybe collectMissingKeysAndAliases can be in the parent class)

@get:InputFiles
abstract val mainSourceSetOutput: ConfigurableFileCollection

@get:InputFiles
abstract val instrumentationFiles: ConfigurableFileCollection

@get:Input
abstract val generatedClassName: Property<String>

@get:Input
abstract val errorHeader: Property<String>

@get:Input
abstract val errorMessage: Property<String>

@get:Input
abstract val successMessage: Property<String>

@TaskAction
fun execute() {
val configFields = loadConfigFields(mainSourceSetOutput, generatedClassName.get())

val parserConfig = ParserConfiguration()
parserConfig.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8)
StaticJavaParser.setConfiguration(parserConfig)

val repoRoot = project.rootProject.projectDir.toPath()
val violations = instrumentationFiles.files.flatMap { file ->
val rel = repoRoot.relativize(file.toPath()).toString()
val cu: CompilationUnit = try {
StaticJavaParser.parse(file)
} catch (_: Exception) {
return@flatMap emptyList()
}
collectPropertyViolations(configFields, rel, cu)
}

if (violations.isNotEmpty()) {
logger.error(errorHeader.get())
violations.forEach { logger.lifecycle(it) }
throw GradleException(errorMessage.get())
} else {
logger.info(successMessage.get())
}
}

protected abstract fun collectPropertyViolations(
configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit
): List<String>
}

/** Checks that InstrumenterModule integration names have proper entries in SUPPORTED and ALIASES. */
abstract class CheckInstrumenterModuleConfigTask : InstrumentationConfigCheckTask() {
override fun collectPropertyViolations(
configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit
): List<String> {
val violations = mutableListOf<String>()

cu.findAll(ClassOrInterfaceDeclaration::class.java).forEach classLoop@{ classDecl ->
val extendsModule = classDecl.extendedTypes.any { it.toString().startsWith("InstrumenterModule") }
if (!extendsModule) return@classLoop

classDecl.findAll(ExplicitConstructorInvocationStmt::class.java)
.filter { !it.isThis }
.forEach { superCall ->
val names = superCall.arguments
.filterIsInstance<StringLiteralExpr>()
.map { it.value }
val line = superCall.range.map { it.begin.line }.orElse(1)

for (name in names) {
val normalized = name.uppercase().replace("-", "_").replace(".", "_")
val enabledKey = "DD_TRACE_${normalized}_ENABLED"
val context = "Integration '$name' (super arg)"
val location = "$relativePath:$line"

violations.addAll(collectMissingKeysAndAliases(
enabledKey,
listOf("DD_TRACE_INTEGRATION_${normalized}_ENABLED", "DD_INTEGRATION_${normalized}_ENABLED"),
configFields.supported, configFields.aliases, location, context
))
}
}
}

return violations
}
}

/** Checks that Decorator instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */
abstract class CheckDecoratorAnalyticsConfigTask : InstrumentationConfigCheckTask() {
override fun collectPropertyViolations(
configFields: LoadedConfigFields, relativePath: String, cu: CompilationUnit
): List<String> {
val violations = mutableListOf<String>()

cu.findAll(MethodDeclaration::class.java)
.filter { it.nameAsString == "instrumentationNames" && it.parameters.isEmpty() }
.forEach { method ->
val names = method.findAll(ReturnStmt::class.java).flatMap { ret ->
ret.expression.map { it.findAll(StringLiteralExpr::class.java).map { s -> s.value } }
.orElse(emptyList())
}
val line = method.range.map { it.begin.line }.orElse(1)

for (name in names) {
val normalized = name.uppercase().replace("-", "_").replace(".", "_")
val context = "Decorator instrumentationName '$name'"
val location = "$relativePath:$line"

violations.addAll(collectMissingKeysAndAliases(
"DD_TRACE_${normalized}_ANALYTICS_ENABLED",
listOf("DD_${normalized}_ANALYTICS_ENABLED"),
configFields.supported, configFields.aliases, location, context
))
violations.addAll(collectMissingKeysAndAliases(
"DD_TRACE_${normalized}_ANALYTICS_SAMPLE_RATE",
listOf("DD_${normalized}_ANALYTICS_SAMPLE_RATE"),
configFields.supported, configFields.aliases, location, context
))
}
}

return violations
}
}

/** Registers `checkInstrumenterModuleConfigurations` to verify each InstrumenterModule's integration name has proper entries in SUPPORTED and ALIASES. */
private fun registerCheckInstrumenterModuleConfigurations(project: Project, extension: SupportedTracerConfigurations) {
val ownerPath = extension.configOwnerPath
val generatedFile = extension.className

project.tasks.register("checkInstrumenterModuleConfigurations", CheckInstrumenterModuleConfigTask::class.java) {
group = "verification"
description = "Validates that InstrumenterModule integration names have corresponding entries in SUPPORTED and ALIASES"

mainSourceSetOutput.from(ownerPath.map {
project.project(it)
.extensions.getByType<SourceSetContainer>()
.named(SourceSet.MAIN_SOURCE_SET_NAME)
.map { main -> main.output }
})
instrumentationFiles.from(project.fileTree(project.rootProject.projectDir) {
include("dd-java-agent/instrumentation/**/src/main/java/**/*.java")
})
generatedClassName.set(generatedFile)
errorHeader.set("\nFound InstrumenterModule integration names with missing SUPPORTED/ALIASES entries:")
errorMessage.set("InstrumenterModule integration names are missing from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.")
successMessage.set("All InstrumenterModule integration names have proper SUPPORTED and ALIASES entries.")
}
}

/** Registers `checkDecoratorAnalyticsConfigurations` to verify each BaseDecorator subclass's instrumentationNames have proper analytics entries in SUPPORTED and ALIASES. */
private fun registerCheckDecoratorAnalyticsConfigurations(project: Project, extension: SupportedTracerConfigurations) {
val ownerPath = extension.configOwnerPath
val generatedFile = extension.className

project.tasks.register("checkDecoratorAnalyticsConfigurations", CheckDecoratorAnalyticsConfigTask::class.java) {
group = "verification"
description = "Validates that Decorator instrumentationNames have corresponding analytics entries in SUPPORTED and ALIASES"

mainSourceSetOutput.from(ownerPath.map {
project.project(it)
.extensions.getByType<SourceSetContainer>()
.named(SourceSet.MAIN_SOURCE_SET_NAME)
.map { main -> main.output }
})
instrumentationFiles.from(project.fileTree(project.rootProject.projectDir) {
include("dd-java-agent/instrumentation/**/src/main/java/**/*.java")
})
generatedClassName.set(generatedFile)
errorHeader.set("\nFound Decorator instrumentationNames with missing analytics SUPPORTED/ALIASES entries:")
errorMessage.set("Decorator instrumentationNames are missing analytics entries from SUPPORTED or ALIASES in '${extension.jsonFile.get()}'.")
successMessage.set("All Decorator instrumentationNames have proper analytics SUPPORTED and ALIASES entries.")
}
}
Loading