Skip to content

Commit c9317c5

Browse files
committed
Add Gradle Plugin
1 parent c80e6e8 commit c9317c5

File tree

6 files changed

+304
-0
lines changed

6 files changed

+304
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ target/
3030
node
3131
node_modules
3232
package-lock.json
33+
build/
34+
.gradle
35+
lib/
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
plugins {
2+
id 'java-gradle-plugin'
3+
id 'groovy'
4+
id 'com.diffplug.spotless' version '7.2.1'
5+
}
6+
7+
group = 'org.apache.logging.log4j'
8+
version = '0.0.1-SNAPSHOT'
9+
10+
repositories {
11+
mavenCentral()
12+
}
13+
14+
dependencies {
15+
implementation 'org.apache.logging.log4j:log4j-core:2.24.0'
16+
implementation 'org.apache.logging.log4j:log4j-weaver:0.2.0'
17+
implementation 'org.codehaus.plexus:plexus-utils:4.0.2'
18+
implementation 'org.apache.commons:commons-lang3:3.18.0'
19+
}
20+
21+
gradlePlugin {
22+
plugins {
23+
loggingTransform {
24+
id = 'org.apache.logging.transform'
25+
implementationClass = 'org.apache.logging.log4j.transform.gradle.LoggingTransformPlugin'
26+
description = 'A plugin that provides logging transformation to add location information to your logs'
27+
}
28+
}
29+
}
30+
31+
spotless {
32+
groovy {
33+
removeSemicolons()
34+
greclipse()
35+
}
36+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'log4j-transform-gradle-plugin'
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package org.apache.logging.log4j.transform.gradle
2+
3+
import org.apache.logging.log4j.weaver.Constants
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.provider.Property
7+
import org.gradle.api.provider.SetProperty
8+
import org.gradle.api.tasks.Internal
9+
import org.gradle.api.tasks.TaskAction
10+
import org.gradle.api.tasks.Input
11+
import org.gradle.api.tasks.InputDirectory
12+
import org.gradle.api.tasks.OutputDirectory
13+
import org.apache.logging.log4j.weaver.LocationClassConverter
14+
import org.apache.logging.log4j.weaver.LocationCacheGenerator
15+
import org.codehaus.plexus.util.DirectoryScanner
16+
import java.nio.file.Files
17+
import java.nio.file.Path
18+
19+
/**
20+
* A Gradle task for weaving Log4j transformations into compiled class files using the log4j-weaver library.
21+
* This task mimics the functionality of the log4j-transform-maven-plugin's process-classes goal.
22+
*/
23+
abstract class Log4jWeaverTask extends DefaultTask {
24+
/**
25+
* The directory containing the compiled source class files to process.
26+
*/
27+
@InputDirectory
28+
abstract Property<String> getSourceDirectoryPath()
29+
30+
/**
31+
* The directory where transformed class files will be written (typically the same as sourceDirectory for in-place transformation).
32+
*/
33+
@OutputDirectory
34+
abstract Property<File> getOutputDirectory()
35+
36+
/**
37+
* Tolerance in milliseconds for determining if a class file needs processing based on timestamps.
38+
*/
39+
@Input
40+
abstract Property<Long> getToleranceMillis()
41+
42+
/**
43+
* Set of include patterns for class files
44+
*/
45+
@Input
46+
abstract SetProperty<String> includes = project.objects.setProperty(String)
47+
48+
/**
49+
* Set of exclude patterns for class files.
50+
*/
51+
@Input
52+
abstract SetProperty<String> excludes = project.objects.setProperty(String)
53+
54+
@Internal
55+
File sourceDirectory
56+
57+
/**
58+
* The main action of the task: weaves Log4j transformations into class files.
59+
*/
60+
@TaskAction
61+
void weave() {
62+
sourceDirectory = project.file(sourceDirectoryPath.get())
63+
logger.info("Starting Log4jWeaverTask: sourceDir=$sourceDirectory, outputDir=$outputDirectory, includes=$includes, excludes=$excludes")
64+
if (!sourceDirectory.exists()) {
65+
logger.warn("Skipping task: source directory ${sourceDirectory} does not exist")
66+
return
67+
}
68+
69+
URLClassLoader classLoader = createClassLoader()
70+
LocationCacheGenerator locationCache = new LocationCacheGenerator()
71+
LocationClassConverter converter = new LocationClassConverter(classLoader)
72+
73+
try {
74+
Set<Path> filesToProcess = getFilesToProcess(sourceDirectory.toPath(), outputDirectory.get().toPath())
75+
if (filesToProcess.empty) {
76+
logger.warn("No class files selected for transformation")
77+
return
78+
}
79+
80+
filesToProcess.groupBy { Path path -> LocationCacheGenerator.getCacheClassFile(path) }
81+
.values()
82+
.each { List<Path> classFiles ->
83+
convertClassFiles(classFiles, converter, locationCache)
84+
}
85+
86+
Map<String, byte[]> cacheClasses = locationCache.generateClasses()
87+
cacheClasses.each { String className, byte[] data ->
88+
saveCacheFile(className, data)
89+
}
90+
} catch (Exception e) {
91+
logger.error("Failed to process class files", e)
92+
throw new RuntimeException("Failed to process class files", e)
93+
}
94+
}
95+
96+
/**
97+
* Creates a ClassLoader including the source directory and runtime classpath dependencies.
98+
*
99+
* @return The created URLClassLoader.
100+
*/
101+
private URLClassLoader createClassLoader() {
102+
try {
103+
List<URL> urls = []
104+
urls << sourceDirectory.toURI().toURL()
105+
project.configurations.runtimeClasspath.files.each { File file ->
106+
urls << file.toURI().toURL()
107+
}
108+
URLClassLoader classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader)
109+
return classLoader
110+
} catch (Exception e) {
111+
logger.error("Failed to create ClassLoader", e)
112+
throw new RuntimeException("Failed to create ClassLoader", e)
113+
}
114+
}
115+
116+
/**
117+
* Scans the source directory for class files that need processing based on include/exclude patterns and timestamp checks.
118+
*
119+
* @param sourceDir The source directory path.
120+
* @param outputDir The output directory path.
121+
* @return Set of Path objects for class files that need processing.
122+
*/
123+
private Set<Path> getFilesToProcess(Path sourceDir, Path outputDir) {
124+
DirectoryScanner scanner = new DirectoryScanner()
125+
scanner.setBasedir(sourceDir.toFile())
126+
scanner.setIncludes(includes.get() as String[])
127+
scanner.setExcludes(excludes.get() as String[])
128+
scanner.scan()
129+
130+
String[] includedFiles = scanner.getIncludedFiles()
131+
132+
Set<Path> filesToProcess = includedFiles.findAll { String relativePath ->
133+
Path outputPath = outputDir.resolve(relativePath)
134+
return !Files.exists(outputPath) ||
135+
Files.getLastModifiedTime(sourceDir.resolve(relativePath)).toMillis() + toleranceMillis.get() >
136+
Files.getLastModifiedTime(outputPath).toMillis()
137+
}.collect { String relativePath ->
138+
sourceDir.resolve(relativePath)
139+
}.toSet()
140+
141+
return filesToProcess
142+
}
143+
144+
/**
145+
* Converts a group of class files using the LocationClassConverter.
146+
*
147+
* @param classFiles List of class file paths to convert.
148+
* @param converter The LocationClassConverter to use for transformation.
149+
* @param locationCache The LocationCacheGenerator for cache management.
150+
*/
151+
protected void convertClassFiles(List<Path> classFiles, LocationClassConverter converter, LocationCacheGenerator locationCache) {
152+
Path sourceDir = sourceDirectory.toPath()
153+
ByteArrayOutputStream buf = new ByteArrayOutputStream()
154+
classFiles.sort()
155+
classFiles.each { Path classFile ->
156+
try {
157+
buf.reset()
158+
Files.newInputStream(sourceDir.resolve(classFile)).withCloseable { InputStream src ->
159+
converter.convert(src, buf, locationCache)
160+
}
161+
byte[] data = buf.toByteArray()
162+
saveClassFile(classFile, data)
163+
} catch (IOException e) {
164+
logger.error("Failed to process class file: ${sourceDir.relativize(classFile)}", e)
165+
throw new RuntimeException("Failed to process class file: ${sourceDir.relativize(classFile)}", e)
166+
}
167+
}
168+
}
169+
170+
/**
171+
* Saves a transformed class file to the output directory.
172+
*
173+
* @param dest The relative path of the class file to save.
174+
* @param data The byte array of the transformed class file.
175+
*/
176+
protected void saveClassFile(Path dest, byte[] data) {
177+
Path outputPath = outputDirectory.get().toPath().resolve(dest)
178+
saveFile(outputPath, data)
179+
logger.info("Saved transformed class file: ${outputDirectory.get().toPath().relativize(outputPath)}")
180+
}
181+
182+
/**
183+
* Saves a generated cache class file to the output directory.
184+
*
185+
* @param internalClassName The internal name of the class (e.g., 'org/apache/logging/log4j/some/CacheClass').
186+
* @param data The byte array of the cache class file.
187+
*/
188+
protected void saveCacheFile(String internalClassName, byte[] data) {
189+
Path outputPath = outputDirectory.get().toPath().resolve("${internalClassName}.class")
190+
saveFile(outputPath, data)
191+
logger.info("Saved cache class file: ${outputDirectory.get().toPath().relativize(outputPath)}")
192+
}
193+
194+
protected static void saveFile(Path outputPath, byte[] data) {
195+
Files.createDirectories(outputPath.parent)
196+
Files.write(outputPath, data)
197+
}
198+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package org.apache.logging.log4j.transform.gradle
2+
3+
import org.apache.logging.log4j.weaver.Constants
4+
5+
class LoggingTransformExtension {
6+
String sourceDirectory = 'classes/java/main/'
7+
String outputDirectory = 'classes/java/main/'
8+
long toleranceMillis = 1000
9+
Set<String> includes = ['**/*.class']
10+
Set<String> excludes = [
11+
'**/*' + Constants.LOCATION_CACHE_SUFFIX + '.class'
12+
]
13+
Set<String> dependencies = []
14+
boolean testFixturesEnabled = false
15+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.apache.logging.log4j.transform.gradle
2+
3+
import org.apache.logging.log4j.weaver.Constants
4+
import org.gradle.api.Plugin
5+
import org.gradle.api.Project
6+
import org.gradle.api.artifacts.Configuration
7+
import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependency
8+
9+
class LoggingTransformPlugin implements Plugin<Project> {
10+
public static final String LOGGING_TRANSFORM = 'loggingTransform'
11+
public static final String COMPILE_JAVA = 'compileJava'
12+
public static final String CLASSES = 'classes'
13+
public static final String COMPILE_TEST_FIXTURES_JAVA = 'compileTestFixturesJava'
14+
15+
@Override
16+
void apply(Project project) {
17+
LoggingTransformExtension extension = project.extensions.create(LOGGING_TRANSFORM, LoggingTransformExtension)
18+
registerTask(project, extension)
19+
}
20+
21+
static void registerTask(Project project, LoggingTransformExtension extension) {
22+
project.tasks.register(LOGGING_TRANSFORM, Log4jWeaverTask) { Log4jWeaverTask task ->
23+
group = 'build'
24+
description = 'A task that provides logging transformation to add location information to your logs'
25+
task.dependsOn(COMPILE_JAVA)
26+
task.sourceDirectoryPath.set("${project.buildDir}/${extension.sourceDirectory}")
27+
task.outputDirectory.set(project.file("${project.buildDir}/${extension.outputDirectory}"))
28+
task.toleranceMillis.set(extension.toleranceMillis)
29+
task.includes.addAll(extension.includes)
30+
task.excludes.addAll(extension.excludes)
31+
task.excludes.add("**/*${Constants.LOCATION_CACHE_SUFFIX}.class")
32+
extension.dependencies.each { String dependency -> task.dependsOn(dependency)}
33+
if (extension.testFixturesEnabled) {
34+
project.tasks.getByName(COMPILE_TEST_FIXTURES_JAVA).dependsOn(task)
35+
}
36+
task.dependsOn(project.provider {
37+
project.configurations
38+
.collect { Configuration configuration -> configuration.dependencies }
39+
.flatten()
40+
?.findAll { Object dependency ->
41+
dependency in DefaultProjectDependency && dependency.name != project.name
42+
}
43+
?.collect { Object dependency -> dependency as DefaultProjectDependency }
44+
?.collect { DefaultProjectDependency dependency -> dependency.getDependencyProject() }
45+
?.collect { Project projectDep -> projectDep.tasks.getByName(CLASSES)}
46+
})
47+
}
48+
49+
project.tasks.getByName(CLASSES).dependsOn(LOGGING_TRANSFORM)
50+
}
51+
}

0 commit comments

Comments
 (0)