Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ target/
node
node_modules
package-lock.json
build/
.gradle
lib/
53 changes: 53 additions & 0 deletions log4j-transform-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
id 'java-gradle-plugin'
id 'groovy'
id 'com.diffplug.spotless' version '7.2.1'
}

group = 'org.apache.logging.log4j'
version = '0.0.1-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.apache.logging.log4j:log4j-core:2.24.0'
implementation 'org.apache.logging.log4j:log4j-weaver:0.2.0'
implementation 'org.codehaus.plexus:plexus-utils:4.0.2'
implementation 'org.apache.commons:commons-lang3:3.18.0'
}

gradlePlugin {
plugins {
loggingTransform {
id = 'org.apache.logging.transform'
implementationClass = 'org.apache.logging.log4j.transform.gradle.LoggingTransformPlugin'
description = 'A plugin that provides logging transformation to add location information to your logs'
}
}
}

spotless {
groovy {
removeSemicolons()
greclipse()
}
}
18 changes: 18 additions & 0 deletions log4j-transform-gradle-plugin/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

rootProject.name = 'log4j-transform-gradle-plugin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.logging.log4j.transform.gradle

import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.apache.logging.log4j.weaver.LocationClassConverter
import org.apache.logging.log4j.weaver.LocationCacheGenerator
import org.codehaus.plexus.util.DirectoryScanner
import java.nio.file.Files
import java.nio.file.Path

/**
* A Gradle task for weaving Log4j transformations into compiled class files using the log4j-weaver library.
* This task mimics the functionality of the log4j-transform-maven-plugin's process-classes goal.
*/
abstract class Log4jWeaverTask extends DefaultTask {
/**
* The directory containing the compiled source class files to process.
*/
@InputDirectory
abstract Property<String> getSourceDirectoryPath()

/**
* The directory where transformed class files will be written (typically the same as sourceDirectory for in-place transformation).
*/
@OutputDirectory
abstract Property<File> getOutputDirectory()

/**
* Tolerance in milliseconds for determining if a class file needs processing based on timestamps.
*/
@Input
abstract Property<Long> getToleranceMillis()

/**
* Set of include patterns for class files
*/
@Input
abstract SetProperty<String> includes = project.objects.setProperty(String)

/**
* Set of exclude patterns for class files.
*/
@Input
abstract SetProperty<String> excludes = project.objects.setProperty(String)

@Internal
File sourceDirectory

/**
* The main action of the task: weaves Log4j transformations into class files.
*/
@TaskAction
void weave() {
sourceDirectory = project.file(sourceDirectoryPath.get())
logger.info("Starting Log4jWeaverTask: sourceDir=$sourceDirectory, outputDir=$outputDirectory, includes=$includes, excludes=$excludes")
if (!sourceDirectory.exists()) {
logger.warn("Skipping task: source directory ${sourceDirectory} does not exist")
return
}

URLClassLoader classLoader = createClassLoader()
LocationCacheGenerator locationCache = new LocationCacheGenerator()
LocationClassConverter converter = new LocationClassConverter(classLoader)

try {
Set<Path> filesToProcess = getFilesToProcess(sourceDirectory.toPath(), outputDirectory.get().toPath())
if (filesToProcess.empty) {
logger.warn("No class files selected for transformation")
return
}

filesToProcess.groupBy { Path path -> LocationCacheGenerator.getCacheClassFile(path) }
.values()
.each { List<Path> classFiles ->
convertClassFiles(classFiles, converter, locationCache)
}

Map<String, byte[]> cacheClasses = locationCache.generateClasses()
cacheClasses.each { String className, byte[] data ->
saveCacheFile(className, data)
}
} catch (Exception e) {
logger.error("Failed to process class files", e)
throw new RuntimeException("Failed to process class files", e)
}
}

/**
* Creates a ClassLoader including the source directory and runtime classpath dependencies.
*
* @return The created URLClassLoader.
*/
private URLClassLoader createClassLoader() {
try {
List<URL> urls = []
urls << sourceDirectory.toURI().toURL()
project.configurations.runtimeClasspath.files.each { File file ->
urls << file.toURI().toURL()
}
URLClassLoader classLoader = new URLClassLoader(urls as URL[], Thread.currentThread().contextClassLoader)
return classLoader
} catch (Exception e) {
logger.error("Failed to create ClassLoader", e)
throw new RuntimeException("Failed to create ClassLoader", e)
}
}

/**
* Scans the source directory for class files that need processing based on include/exclude patterns and timestamp checks.
*
* @param sourceDir The source directory path.
* @param outputDir The output directory path.
* @return Set of Path objects for class files that need processing.
*/
private Set<Path> getFilesToProcess(Path sourceDir, Path outputDir) {
DirectoryScanner scanner = new DirectoryScanner()
scanner.setBasedir(sourceDir.toFile())
scanner.setIncludes(includes.get() as String[])
scanner.setExcludes(excludes.get() as String[])
scanner.scan()

String[] includedFiles = scanner.getIncludedFiles()

Set<Path> filesToProcess = includedFiles.findAll { String relativePath ->
Path outputPath = outputDir.resolve(relativePath)
return !Files.exists(outputPath) ||
Files.getLastModifiedTime(sourceDir.resolve(relativePath)).toMillis() + toleranceMillis.get() >
Files.getLastModifiedTime(outputPath).toMillis()
}.collect { String relativePath ->
sourceDir.resolve(relativePath)
}.toSet()

return filesToProcess
}

/**
* Converts a group of class files using the LocationClassConverter.
*
* @param classFiles List of class file paths to convert.
* @param converter The LocationClassConverter to use for transformation.
* @param locationCache The LocationCacheGenerator for cache management.
*/
protected void convertClassFiles(List<Path> classFiles, LocationClassConverter converter, LocationCacheGenerator locationCache) {
Path sourceDir = sourceDirectory.toPath()
ByteArrayOutputStream buf = new ByteArrayOutputStream()
classFiles.sort()
classFiles.each { Path classFile ->
try {
buf.reset()
Files.newInputStream(sourceDir.resolve(classFile)).withCloseable { InputStream src ->
converter.convert(src, buf, locationCache)
}
byte[] data = buf.toByteArray()
saveClassFile(classFile, data)
} catch (IOException e) {
logger.error("Failed to process class file: ${sourceDir.relativize(classFile)}", e)
throw new RuntimeException("Failed to process class file: ${sourceDir.relativize(classFile)}", e)
}
}
}

/**
* Saves a transformed class file to the output directory.
*
* @param dest The relative path of the class file to save.
* @param data The byte array of the transformed class file.
*/
protected void saveClassFile(Path dest, byte[] data) {
Path outputPath = outputDirectory.get().toPath().resolve(dest)
saveFile(outputPath, data)
logger.info("Saved transformed class file: ${outputDirectory.get().toPath().relativize(outputPath)}")
}

/**
* Saves a generated cache class file to the output directory.
*
* @param internalClassName The internal name of the class (e.g., 'org/apache/logging/log4j/some/CacheClass').
* @param data The byte array of the cache class file.
*/
protected void saveCacheFile(String internalClassName, byte[] data) {
Path outputPath = outputDirectory.get().toPath().resolve("${internalClassName}.class")
saveFile(outputPath, data)
logger.info("Saved cache class file: ${outputDirectory.get().toPath().relativize(outputPath)}")
}

protected static void saveFile(Path outputPath, byte[] data) {
Files.createDirectories(outputPath.parent)
Files.write(outputPath, data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.logging.log4j.transform.gradle

import org.apache.logging.log4j.weaver.Constants

class LoggingTransformExtension {
String sourceDirectory = 'classes/java/main/'
String outputDirectory = 'classes/java/main/'
long toleranceMillis = 1000
Set<String> includes = ['**/*.class']
Set<String> excludes = [
'**/*' + Constants.LOCATION_CACHE_SUFFIX + '.class'
]
Set<String> dependencies = []
boolean testFixturesEnabled = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.logging.log4j.transform.gradle

import org.apache.logging.log4j.weaver.Constants
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.internal.artifacts.dependencies.DefaultProjectDependency

class LoggingTransformPlugin implements Plugin<Project> {
public static final String LOGGING_TRANSFORM = 'loggingTransform'
public static final String COMPILE_JAVA = 'compileJava'
public static final String CLASSES = 'classes'
public static final String COMPILE_TEST_FIXTURES_JAVA = 'compileTestFixturesJava'

@Override
void apply(Project project) {
LoggingTransformExtension extension = project.extensions.create(LOGGING_TRANSFORM, LoggingTransformExtension)
registerTask(project, extension)
}

static void registerTask(Project project, LoggingTransformExtension extension) {
project.tasks.register(LOGGING_TRANSFORM, Log4jWeaverTask) { Log4jWeaverTask task ->
group = 'build'
description = 'A task that provides logging transformation to add location information to your logs'
task.dependsOn(COMPILE_JAVA)
task.sourceDirectoryPath.set("${project.buildDir}/${extension.sourceDirectory}")
task.outputDirectory.set(project.file("${project.buildDir}/${extension.outputDirectory}"))
task.toleranceMillis.set(extension.toleranceMillis)
task.includes.addAll(extension.includes)
task.excludes.addAll(extension.excludes)
task.excludes.add("**/*${Constants.LOCATION_CACHE_SUFFIX}.class")
extension.dependencies.each { String dependency -> task.dependsOn(dependency)}
if (extension.testFixturesEnabled) {
project.tasks.getByName(COMPILE_TEST_FIXTURES_JAVA).dependsOn(task)
}
task.dependsOn(project.provider {
project.configurations
.collect { Configuration configuration -> configuration.dependencies }
.flatten()
?.findAll { Object dependency ->
dependency in DefaultProjectDependency && dependency.name != project.name
}
?.collect { Object dependency -> dependency as DefaultProjectDependency }
?.collect { DefaultProjectDependency dependency -> dependency.getDependencyProject() }
?.collect { Project projectDep -> projectDep.tasks.getByName(CLASSES)}
})
}

project.tasks.getByName(CLASSES).dependsOn(LOGGING_TRANSFORM)
}
}