Skip to content

Commit d2d3204

Browse files
committed
Create a gradle plugin to check instrumenation names
1 parent b4a4111 commit d2d3204

File tree

5 files changed

+301
-0
lines changed

5 files changed

+301
-0
lines changed

buildSrc/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ gradlePlugin {
4747
id = "dd-trace-java.config-inversion-linter"
4848
implementationClass = "datadog.gradle.plugin.config.ConfigInversionLinter"
4949
}
50+
51+
create("instrumentation-naming") {
52+
id = "dd-trace-java.instrumentation-naming"
53+
implementationClass = "datadog.gradle.plugin.naming.InstrumentationNamingPlugin"
54+
}
5055
}
5156
}
5257

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package datadog.gradle.plugin.naming
2+
3+
import org.gradle.api.provider.ListProperty
4+
import org.gradle.api.provider.Property
5+
6+
/**
7+
* Extension for configuring instrumentation naming convention checks.
8+
*
9+
* Example usage:
10+
* ```
11+
* instrumentationNaming {
12+
* instrumentationsDir.set(file("dd-java-agent/instrumentation"))
13+
* exclusions.set(listOf("http-url-connection", "sslsocket"))
14+
* }
15+
* ```
16+
*/
17+
abstract class InstrumentationNamingExtension {
18+
/**
19+
* The directory containing instrumentation modules.
20+
* Defaults to "dd-java-agent/instrumentation".
21+
*/
22+
abstract val instrumentationsDir: Property<String>
23+
24+
/**
25+
* List of module names to exclude from naming convention checks.
26+
* These modules will not be validated against the naming rules.
27+
*/
28+
abstract val exclusions: ListProperty<String>
29+
30+
init {
31+
instrumentationsDir.convention("dd-java-agent/instrumentation")
32+
exclusions.convention(emptyList())
33+
}
34+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package datadog.gradle.plugin.naming
2+
3+
import org.gradle.api.GradleException
4+
import org.gradle.api.Plugin
5+
import org.gradle.api.Project
6+
import java.io.File
7+
8+
/**
9+
* Plugin that validates naming conventions for instrumentation modules.
10+
*
11+
* Rules:
12+
* 1. Module name must end with a version (e.g., "2.0", "3.1") OR end with "-common"
13+
* 2. Module name must include the parent directory name
14+
* (e.g., "couchbase-2.0" must contain "couchbase" which is the parent directory name)
15+
*
16+
* Apply this plugin:
17+
* ```
18+
* plugins {
19+
* id("dd-trace-java.instrumentation-naming")
20+
* }
21+
* ```
22+
*/
23+
class InstrumentationNamingPlugin : Plugin<Project> {
24+
override fun apply(target: Project) {
25+
val extension = target.extensions.create(
26+
"instrumentationNaming",
27+
InstrumentationNamingExtension::class.java
28+
)
29+
30+
target.tasks.register("checkInstrumentationNaming") {
31+
group = "verification"
32+
description = "Validates naming conventions for instrumentation modules"
33+
34+
doLast {
35+
val instrumentationsDir = target.rootProject.file(extension.instrumentationsDir.get())
36+
val exclusions = extension.exclusions.get().toSet()
37+
38+
if (!instrumentationsDir.exists() || !instrumentationsDir.isDirectory) {
39+
throw GradleException(
40+
"Instrumentations directory not found: ${instrumentationsDir.absolutePath}"
41+
)
42+
}
43+
44+
val violations = validateInstrumentations(instrumentationsDir, exclusions)
45+
46+
if (violations.isNotEmpty()) {
47+
val errorMessage = buildString {
48+
appendLine("\nInstrumentation naming convention violations found:")
49+
appendLine()
50+
violations.forEach { violation ->
51+
appendLine("${violation.path}")
52+
appendLine(" ${violation.message}")
53+
appendLine()
54+
}
55+
appendLine("Naming rules:")
56+
appendLine(" 1. Module name must end with a version (e.g., '2.0', '3.1') OR end with '-common'")
57+
appendLine(" 2. Module name must include the parent directory name")
58+
appendLine(" Example: 'couchbase/couchbase-2.0' ✓ (contains 'couchbase')")
59+
appendLine()
60+
appendLine("To exclude specific modules, configure the plugin:")
61+
appendLine(" instrumentationNaming {")
62+
appendLine(" exclusions.set(listOf(\"module-name\"))")
63+
appendLine(" }")
64+
}
65+
throw GradleException(errorMessage)
66+
} else {
67+
target.logger.lifecycle("✓ All instrumentation modules follow naming conventions")
68+
}
69+
}
70+
}
71+
}
72+
73+
private fun validateInstrumentations(
74+
instrumentationsDir: File,
75+
exclusions: Set<String>
76+
): List<NamingViolation> {
77+
val violations = mutableListOf<NamingViolation>()
78+
79+
// Get all subdirectories in the instrumentations directory
80+
instrumentationsDir.listFiles { file -> file.isDirectory }?.forEach parentLoop@{ parentDir ->
81+
val parentName = parentDir.name
82+
83+
// Skip build directories and other non-instrumentation directories
84+
if (parentName in setOf("build", "src", ".gradle")) {
85+
return@parentLoop
86+
}
87+
88+
// Check if this directory has a build.gradle file
89+
// If it does, it's a leaf instrumentation module
90+
val hasBuildFile = parentDir.listFiles()?.any {
91+
it.name == "build.gradle" || it.name == "build.gradle.kts"
92+
} ?: false
93+
94+
if (hasBuildFile) {
95+
// This is a leaf module, validate only the version/common suffix requirement
96+
if (parentName !in exclusions) {
97+
validateLeafModuleName(parentName, parentDir.relativeTo(instrumentationsDir).path)?.let {
98+
violations.add(it)
99+
}
100+
}
101+
} else {
102+
// This directory contains sub-modules, check each one
103+
parentDir.listFiles { file -> file.isDirectory }?.forEach moduleLoop@{ moduleDir ->
104+
val moduleName = moduleDir.name
105+
106+
// Skip build and other non-module directories
107+
if (moduleName in setOf("build", "src", ".gradle")) {
108+
return@moduleLoop
109+
}
110+
111+
// Check if this is actually a module (has build.gradle)
112+
val hasModuleBuildFile = moduleDir.listFiles()?.any {
113+
it.name == "build.gradle" || it.name == "build.gradle.kts"
114+
} ?: false
115+
116+
if (hasModuleBuildFile && moduleName !in exclusions) {
117+
validateModuleName(moduleName, parentName, moduleDir.relativeTo(instrumentationsDir).path)?.let {
118+
violations.add(it)
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
return violations
126+
}
127+
128+
private fun validateModuleName(
129+
moduleName: String,
130+
parentName: String,
131+
relativePath: String
132+
): NamingViolation? {
133+
// Rule 1: Module name must end with version pattern (X.Y, X.Y.Z, etc.) or "-common"
134+
val versionPattern = Regex("""\d+\.\d+(\.\d+)?$""")
135+
val endsWithCommon = moduleName.endsWith("-common")
136+
val endsWithVersion = versionPattern.containsMatchIn(moduleName)
137+
138+
if (!endsWithVersion && !endsWithCommon) {
139+
return NamingViolation(
140+
relativePath,
141+
"Module name '$moduleName' must end with a version (e.g., '2.0', '3.1.0') or '-common'"
142+
)
143+
}
144+
145+
// Rule 2: Module name must contain parent directory name
146+
// Extract the base name (without version or -common suffix)
147+
if (!moduleName.contains(parentName, ignoreCase = true)) {
148+
return NamingViolation(
149+
relativePath,
150+
"Module name '$moduleName' should contain parent directory name '$parentName'"
151+
)
152+
}
153+
154+
return null
155+
}
156+
157+
/**
158+
* Validates naming for leaf modules (modules at the top level with no parent grouping).
159+
* These only need to check the version/common suffix requirement.
160+
*/
161+
private fun validateLeafModuleName(
162+
moduleName: String,
163+
relativePath: String
164+
): NamingViolation? {
165+
// Rule: Module name must end with version pattern (X.Y, X.Y.Z, etc.) or "-common"
166+
val versionPattern = Regex("""\d+\.\d+(\.\d+)?$""")
167+
val endsWithCommon = moduleName.endsWith("-common")
168+
val endsWithVersion = versionPattern.containsMatchIn(moduleName)
169+
170+
if (!endsWithVersion && !endsWithCommon) {
171+
return NamingViolation(
172+
relativePath,
173+
"Module name '$moduleName' must end with a version (e.g., '2.0', '3.1.0') or '-common'"
174+
)
175+
}
176+
177+
return null
178+
}
179+
180+
private data class NamingViolation(
181+
val path: String,
182+
val message: String
183+
)
184+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Instrumentation Naming Convention Plugin
2+
3+
A Gradle plugin that validates naming conventions for instrumentation modules under `dd-java-agent/instrumentation`.
4+
5+
## Naming Rules
6+
7+
The plugin enforces the following rules:
8+
9+
1. **Version or Common suffix**: Module names must end with either:
10+
- A version number (e.g., `2.0`, `3.1`, `4.0.5`)
11+
- The suffix `-common`
12+
13+
2. **Parent directory inclusion**: Module names must include the parent directory name
14+
- Example: `couchbase/couchbase-2.0` ✓ (module name contains "couchbase")
15+
- Example: `couchbase/foo-2.0` ✗ (module name doesn't contain "couchbase")
16+
17+
## Usage
18+
19+
### Apply the plugin
20+
21+
In your root `build.gradle` or `build.gradle.kts`:
22+
23+
```kotlin
24+
plugins {
25+
id("dd-trace-java.instrumentation-naming")
26+
}
27+
```
28+
29+
### Run the validation
30+
31+
```bash
32+
./gradlew checkInstrumentationNaming
33+
```
34+
35+
### Configuration
36+
37+
You can configure the plugin with custom settings:
38+
39+
```kotlin
40+
instrumentationNaming {
41+
// Optional: specify a different instrumentations directory
42+
instrumentationsDir.set("path/to/instrumentations")
43+
44+
// Optional: exclude specific modules from validation
45+
exclusions.set(listOf(
46+
"http-url-connection",
47+
"sslsocket",
48+
"classloading"
49+
))
50+
}
51+
```
52+
53+
## Examples
54+
55+
### Valid module names:
56+
- `couchbase/couchbase-2.0`
57+
- `couchbase/couchbase-2.6`
58+
- `couchbase/couchbase-3.1`
59+
- `kafka/kafka-common`
60+
- `apache-httpclient/apache-httpclient-4.0`
61+
62+
### Invalid module names:
63+
- `couchbase/foo-2.0` ✗ (doesn't contain parent name "couchbase")
64+
- `kafka/kafka` ✗ (missing version or -common suffix)
65+
- `kafka/kafka-latest` ✗ (not a valid version number)
66+
67+
## Integration with CI
68+
69+
Add the task to your verification pipeline:
70+
71+
```kotlin
72+
tasks.named("check") {
73+
dependsOn("checkInstrumentationNaming")
74+
}
75+
```
76+
77+
This ensures naming conventions are validated on every build.

dd-java-agent/instrumentation/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
22

33
plugins {
44
id 'com.gradleup.shadow'
5+
id("dd-trace-java.instrumentation-naming")
56
}
67
apply from: "$rootDir/gradle/java.gradle"
78

0 commit comments

Comments
 (0)