diff --git a/.gitignore b/.gitignore index d949d00..c79c598 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ target/ node node_modules package-lock.json +build/ +.gradle +lib/ \ No newline at end of file diff --git a/log4j-transform-gradle-plugin/build.gradle b/log4j-transform-gradle-plugin/build.gradle new file mode 100644 index 0000000..7d7ca55 --- /dev/null +++ b/log4j-transform-gradle-plugin/build.gradle @@ -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() + } +} \ No newline at end of file diff --git a/log4j-transform-gradle-plugin/settings.gradle b/log4j-transform-gradle-plugin/settings.gradle new file mode 100644 index 0000000..e4a16d0 --- /dev/null +++ b/log4j-transform-gradle-plugin/settings.gradle @@ -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' \ No newline at end of file diff --git a/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/Log4jWeaverTask.groovy b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/Log4jWeaverTask.groovy new file mode 100644 index 0000000..9ccae8c --- /dev/null +++ b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/Log4jWeaverTask.groovy @@ -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 getSourceDirectoryPath() + + /** + * The directory where transformed class files will be written (typically the same as sourceDirectory for in-place transformation). + */ + @OutputDirectory + abstract Property getOutputDirectory() + + /** + * Tolerance in milliseconds for determining if a class file needs processing based on timestamps. + */ + @Input + abstract Property getToleranceMillis() + + /** + * Set of include patterns for class files + */ + @Input + abstract SetProperty includes = project.objects.setProperty(String) + + /** + * Set of exclude patterns for class files. + */ + @Input + abstract SetProperty 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 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 classFiles -> + convertClassFiles(classFiles, converter, locationCache) + } + + Map 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 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 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 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 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) + } +} diff --git a/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformExtension.groovy b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformExtension.groovy new file mode 100644 index 0000000..fcd4b32 --- /dev/null +++ b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformExtension.groovy @@ -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 includes = ['**/*.class'] + Set excludes = [ + '**/*' + Constants.LOCATION_CACHE_SUFFIX + '.class' + ] + Set dependencies = [] + boolean testFixturesEnabled = false +} diff --git a/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformPlugin.groovy b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformPlugin.groovy new file mode 100644 index 0000000..24c39f8 --- /dev/null +++ b/log4j-transform-gradle-plugin/src/main/groovy/org/apache/logging/log4j/transform/gradle/LoggingTransformPlugin.groovy @@ -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 { + 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) + } +}