From bf7f0b0cfdfd8d2f7fa9d3f5d5cdf4e77ed05c09 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 7 Nov 2024 09:03:49 +0100 Subject: [PATCH] Add a tool to manipulate plugin descriptors (#139) The tool is based on picocli and supports the following commands: * `toJson`: converts a `Log4j2Plugins.dat` to a JSON representation. * `fromJson`: converts the JSON representation of a plugin descriptor to its `Log4j2Plugins.dat` form. * `filterReflectConfig`: filters a GraalVM `reflect-config.json` file by removing the classes that are not contained in a `Log4j2Plugins.json` file. --- .gitattributes | 11 +- log4j-codegen/.picocli-application-activator | 17 + log4j-codegen/pom.xml | 68 +-- .../.picocli-application-activator | 17 + log4j-converter-plugin-descriptor/pom.xml | 125 +++++ .../plugins/PluginCacheConverter.java | 275 +++++++++++ .../plugins/internal/JacksonUtils.java | 53 ++ .../plugins/internal/PluginDescriptors.java | 464 ++++++++++++++++++ .../plugins/internal/ReflectConfigFilter.java | 60 +++ .../log4j/converter/plugins/package-info.java | 20 + .../main/resources/Log4j2Plugins.schema.json | 61 +++ .../log4j2.component.properties | 20 + .../log4j2.simplelog.properties | 20 + log4j-transform-parent/pom.xml | 84 +++- pom.xml | 7 + src/site/antora/modules/ROOT/pages/cli.adoc | 187 ++++++- ...ansform-maven-shade-plugin-extensions.adoc | 2 +- 17 files changed, 1410 insertions(+), 81 deletions(-) create mode 100644 log4j-codegen/.picocli-application-activator create mode 100644 log4j-converter-plugin-descriptor/.picocli-application-activator create mode 100644 log4j-converter-plugin-descriptor/pom.xml create mode 100644 log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java create mode 100644 log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java create mode 100644 log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java create mode 100644 log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java create mode 100644 log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java create mode 100644 log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json create mode 100644 log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties create mode 100644 log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties diff --git a/.gitattributes b/.gitattributes index 10914c1..597281d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,8 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Checked by Spotless (LF line endings) -*.java text eol=lf -*.xml text eol=lf -*.yaml text eol=lf -*.yml text eol=lf +# All text files with LF line endings +* text=auto eol=lf +# Maven Wrapper cmd script +/mvnw.cmd eol=crlf +# Maven Wrapper need LF line endings +/.mvn/wrapper/maven-wrapper.properties eol=lf diff --git a/log4j-codegen/.picocli-application-activator b/log4j-codegen/.picocli-application-activator new file mode 100644 index 0000000..a395748 --- /dev/null +++ b/log4j-codegen/.picocli-application-activator @@ -0,0 +1,17 @@ +# +# 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. +# +This file activates the `picocli` profile diff --git a/log4j-codegen/pom.xml b/log4j-codegen/pom.xml index 2d87ba4..c53f6a7 100644 --- a/log4j-codegen/pom.xml +++ b/log4j-codegen/pom.xml @@ -61,11 +61,10 @@ provided - + info.picocli picocli - ${picocli.version} @@ -89,69 +88,4 @@ - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - info.picocli - picocli-codegen - ${picocli.version} - - - - - - - org.apache.maven.plugins - maven-shade-plugin - ${maven-shade-plugin.version} - - - org.apache.logging.log4j - log4j-transform-maven-shade-plugin-extensions - ${project.version} - - - - - shade-jar-with-dependencies - - shade - - - - - *:* - - module-info.class - META-INF/versions/*/module-info.class - META-INF/MANIFEST.MF - - - - true - shaded - - - - - - - true - - - - - - - - - - - diff --git a/log4j-converter-plugin-descriptor/.picocli-application-activator b/log4j-converter-plugin-descriptor/.picocli-application-activator new file mode 100644 index 0000000..a395748 --- /dev/null +++ b/log4j-converter-plugin-descriptor/.picocli-application-activator @@ -0,0 +1,17 @@ +# +# 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. +# +This file activates the `picocli` profile diff --git a/log4j-converter-plugin-descriptor/pom.xml b/log4j-converter-plugin-descriptor/pom.xml new file mode 100644 index 0000000..74d8e5d --- /dev/null +++ b/log4j-converter-plugin-descriptor/pom.xml @@ -0,0 +1,125 @@ + + + + 4.0.0 + + org.apache.logging.log4j + log4j-transform-parent + ${revision} + ../log4j-transform-parent + + + log4j-converter-plugin-descriptor + Apache Log4j plugin descriptor tools + Tools to manipulate `Log4j2Plugins.dat` plugin descriptors and synchronize them with GraalVM reachability metadata. + + + + false + + org.apache.logging.log4j.converter.plugins.PluginCacheConverter + + + 2.18.0 + + + + + + org.jspecify + jspecify + provided + + + + + org.apache.logging.log4j + log4j-api + + + + info.picocli + picocli + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + create-shaded-resources + + single + + prepare-package + + + + shaded-resources + + jar + + / + + + src/main/shaded-resources + / + + + + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-jar-with-dependencies + + + ${project.build.directory}/${project.build.finalName}-shaded-resources.jar + + + + + + + + + diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java new file mode 100644 index 0000000..63b1ee3 --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/PluginCacheConverter.java @@ -0,0 +1,275 @@ +/* + * 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.converter.plugins; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.converter.plugins.internal.PluginDescriptors.Namespace; +import org.apache.logging.log4j.converter.plugins.internal.PluginDescriptors.PluginDescriptor; +import org.apache.logging.log4j.converter.plugins.internal.ReflectConfigFilter; +import org.jspecify.annotations.Nullable; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import picocli.CommandLine.ScopeType; + +@Command(name = "convertPlugin", mixinStandardHelpOptions = true) +public class PluginCacheConverter { + + private static final String PLUGIN_DESCRIPTOR_FILE = + "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat"; + private static final String PLUGIN_DESCRIPTOR_JSON_FILE = "Log4j2Plugins.json"; + + private static final Logger logger = LogManager.getLogger(PluginCacheConverter.class); + private static final JsonFactory jsonFactory = new ObjectMapper().getFactory(); + + @Option( + names = {"-o", "--outputDirectory"}, + description = "Directory for the command output", + defaultValue = ".", + scope = ScopeType.INHERIT) + private File outputDirectory; + + public static void main(final String[] args) { + System.exit(new CommandLine(new PluginCacheConverter()).execute(args)); + } + + @Command + public void toJson( + @Parameters( + arity = "1..*", + description = "Classpath containing Log4j Core plugins", + paramLabel = "") + final List classPath) + throws IOException { + new PluginDescriptorToJsonConverter(classPath, outputDirectory).call(); + } + + @Command + public void fromJson(@Parameters(description = "Plugin descriptor in JSON format") final File jsonPluginDescriptor) + throws IOException { + new JsonToPluginDescriptorConverter(jsonPluginDescriptor, outputDirectory).call(); + } + + @Command + public void filterReflectConfig( + @Parameters(description = "Plugin descriptor in JSON format") final File jsonPluginDescriptor, + @Parameters( + arity = "1..*", + description = "Classpath containing GraalVM descriptors", + paramLabel = "") + final List classPath) + throws IOException { + new ReflectConfigTransformer(jsonPluginDescriptor, classPath, outputDirectory).call(); + } + + private static Collection validateClassPath(final Collection classPath) { + return classPath.stream() + .flatMap(classPathElement -> Arrays.stream(classPathElement.split(File.pathSeparator, -1))) + .map(classPathElement -> { + final Path inputPath = Paths.get(classPathElement); + if (!Files.isRegularFile(inputPath)) { + throw new IllegalArgumentException("Input file " + inputPath + " is not a file."); + } + if (!inputPath.getFileName().toString().endsWith(".jar")) { + throw new IllegalArgumentException( + "Invalid input file, only JAR files are supported: " + inputPath); + } + return inputPath; + }) + .collect(Collectors.toList()); + } + + private static void createParentDirectories(final Path path) throws IOException { + final Path parent = path.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + } + + private static final class PluginDescriptorToJsonConverter implements Callable<@Nullable Void> { + + private final ClassLoader classLoader; + + private final Path outputDirectory; + + PluginDescriptorToJsonConverter(final Collection classPath, final File outputDirectory) { + this.classLoader = AccessController.doPrivileged( + (PrivilegedAction) () -> new URLClassLoader(validateClassPath(classPath).stream() + .map(p -> { + try { + return p.toUri().toURL(); + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(e); + } + }) + .toArray(URL[]::new))); + this.outputDirectory = outputDirectory.toPath(); + } + + @Override + public @Nullable Void call() throws IOException { + final Path pluginDescriptorPath = outputDirectory.resolve(PLUGIN_DESCRIPTOR_JSON_FILE); + logger.info("Creating Log4j plugin descriptor file (JSON format): {}", pluginDescriptorPath); + createParentDirectories(pluginDescriptorPath); + try (final OutputStream output = Files.newOutputStream(pluginDescriptorPath); + final JsonGenerator generator = jsonFactory.createGenerator(output)) { + final PluginDescriptor pluginDescriptor = new PluginDescriptor(); + // Read all `Log4j2Plugins.dat` file on the classpath + for (final URL url : Collections.list(classLoader.getResources(PLUGIN_DESCRIPTOR_FILE))) { + try (final BufferedInputStream input = new BufferedInputStream(url.openStream())) { + pluginDescriptor.readPluginDescriptor(input); + } + } + // Write JSON file + pluginDescriptor.withBuilderHierarchy(classLoader).toJson(generator); + } + return null; + } + } + + private static class JsonToPluginDescriptorConverter implements Callable<@Nullable Void> { + + private final Path input; + private final Path outputDirectory; + + JsonToPluginDescriptorConverter(final File input, final File outputDirectory) { + this.input = input.toPath(); + this.outputDirectory = outputDirectory.toPath(); + } + + @Override + public @Nullable Void call() throws IOException { + final Path pluginDescriptorPath = outputDirectory.resolve(PLUGIN_DESCRIPTOR_FILE); + logger.info("Creating Log4j plugin descriptor file (binary format): {}", pluginDescriptorPath); + createParentDirectories(pluginDescriptorPath); + try (final OutputStream output = Files.newOutputStream(pluginDescriptorPath); + final InputStream inputStream = Files.newInputStream(input); + final JsonParser parser = jsonFactory.createParser(inputStream)) { + final PluginDescriptor pluginDescriptor = new PluginDescriptor(); + // Input JSON + pluginDescriptor.readJson(parser); + // Output `Log4j2Plugins.dat` file + pluginDescriptor.toPluginDescriptor(output); + } + return null; + } + } + + private static class ReflectConfigTransformer implements Callable<@Nullable Void> { + + private final Path pluginDescriptorPath; + private final Collection classPath; + private final Path outputDirectory; + + ReflectConfigTransformer( + final File pluginDescriptorPath, final Collection classPath, final File outputDirectory) { + this.pluginDescriptorPath = pluginDescriptorPath.toPath(); + this.classPath = validateClassPath(classPath); + this.outputDirectory = outputDirectory.toPath(); + } + + void filterReflectConfigInJar(final Path jar, final ReflectConfigFilter filter) throws IOException { + final URI jarFileSystemRoot; + try { + jarFileSystemRoot = new URI("jar", jar.toUri().toASCIIString(), null); + } catch (final URISyntaxException e) { + throw new IllegalArgumentException(e); + } + try (final FileSystem fileSystem = FileSystems.newFileSystem(jarFileSystemRoot, Collections.emptyMap())) { + final Path rootPath = fileSystem.getPath("/"); + final Path nativeImagePath = rootPath.resolve("META-INF/native-image"); + if (Files.isDirectory(nativeImagePath)) { + try (final Stream paths = Files.walk(nativeImagePath, 3)) { + paths.filter(p -> "reflect-config.json" + .equals(p.getFileName().toString())) + .forEach(p -> filterReflectConfig(rootPath, p, filter)); + } + } + } + } + + void filterReflectConfig(final Path rootPath, final Path reflectConfig, final ReflectConfigFilter filter) { + try { + final String relativePath = rootPath.relativize(reflectConfig).toString(); + final Path outputPath = outputDirectory.resolve(relativePath); + logger.info("Creating GraalVM configuration file: {}", outputPath); + createParentDirectories(outputPath); + try (final OutputStream output = Files.newOutputStream(outputPath); + final InputStream inputStream = Files.newInputStream(reflectConfig); + final JsonParser parser = jsonFactory.createParser(inputStream); + final JsonGenerator generator = jsonFactory.createGenerator(output)) { + filter.filter(parser, generator); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public @Nullable Void call() throws IOException { + final PluginDescriptor pluginDescriptor = new PluginDescriptor(); + // Read plugin descriptor + try (final InputStream inputStream = Files.newInputStream(pluginDescriptorPath); + final JsonParser parser = jsonFactory.createParser(inputStream)) { + pluginDescriptor.readJson(parser); + } + // Find all referenced classes + final Set classNames = new HashSet<>(); + pluginDescriptor.getNamespaces().flatMap(Namespace::getPlugins).forEach(p -> { + classNames.add(p.getClassName()); + classNames.addAll(p.getBuilderHierarchy()); + }); + final ReflectConfigFilter filter = new ReflectConfigFilter(classNames); + for (final Path jar : classPath) { + filterReflectConfigInJar(jar, filter); + } + return null; + } + } +} diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java new file mode 100644 index 0000000..412dd90 --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/JacksonUtils.java @@ -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. + */ +package org.apache.logging.log4j.converter.plugins.internal; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.IOException; + +final class JacksonUtils { + + static void assertArrayStart(final JsonParser parser) throws IOException { + parser.nextToken(); + assertCurrentToken(parser, JsonToken.START_ARRAY, "start of JSON array"); + } + + static void assertArrayEnd(final JsonParser parser) throws IOException { + assertCurrentToken(parser, JsonToken.END_ARRAY, "end of JSON array"); + } + + static void assertObjectStart(final JsonParser parser) throws IOException { + parser.nextToken(); + assertCurrentToken(parser, JsonToken.START_OBJECT, "start of JSON object"); + } + + static void assertObjectEnd(final JsonParser parser) throws IOException { + assertCurrentToken(parser, JsonToken.END_OBJECT, "end of JSON object"); + } + + static void assertCurrentToken(final JsonParser parser, final JsonToken token, final String expectingMessage) + throws IOException { + if (parser.currentToken() != token) { + throw new IOException(String.format( + "Parser error at %s: expecting %s, but found '%s'.", + parser.currentLocation(), expectingMessage, parser.currentName())); + } + } + + private JacksonUtils() {} +} diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java new file mode 100644 index 0000000..6600a1a --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/PluginDescriptors.java @@ -0,0 +1,464 @@ +/* + * 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.converter.plugins.internal; + +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayEnd; +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayStart; +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertObjectEnd; +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertObjectStart; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class PluginDescriptors { + + private static final String PLUGIN_BUILDER_FACTORY = + "org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory"; + private static final String PLUGIN_FACTORY = "org.apache.logging.log4j.core.config.plugins.PluginFactory"; + + private static final Logger logger = LogManager.getLogger(PluginDescriptors.class); + + public static final class PluginDescriptor { + private final Map namespacesByName; + + public PluginDescriptor() { + this(new TreeMap<>()); + } + + private PluginDescriptor(final Map namespacesByName) { + this.namespacesByName = namespacesByName; + } + + /** + * Reads an additional plugin descriptor. + * + * @param input An input stream for a `Log4j2Plugins.dat` file. + */ + public void readPluginDescriptor(final InputStream input) throws IOException { + try (final DataInputStream dataInput = new DataInputStream(input)) { + final int namespaceCount = dataInput.readInt(); + for (int i = 0; i < namespaceCount; i++) { + final Namespace namespace = Namespace.fromDataInput(dataInput); + namespacesByName.merge(namespace.getName(), namespace, Namespace::merge); + } + } + } + + public void toPluginDescriptor(final OutputStream output) throws IOException { + try (final DataOutputStream dataOutput = new DataOutputStream(output)) { + dataOutput.writeInt(namespacesByName.size()); + for (final Namespace namespace : namespacesByName.values()) { + namespace.writeDataOutput(dataOutput); + } + } + } + + public void readJson(final JsonParser parser) throws IOException { + assertObjectStart(parser); + while (parser.nextToken() == JsonToken.FIELD_NAME) { + final String namespace = parser.currentName(); + namespacesByName.merge(namespace, Namespace.fromJson(parser, namespace), Namespace::merge); + } + assertObjectEnd(parser); + } + + public PluginDescriptor withBuilderHierarchy(final ClassLoader classLoader) { + final Map namespacesByName = new TreeMap<>(this.namespacesByName); + namespacesByName.replaceAll((k, v) -> v.withBuilderHierarchy(classLoader)); + return new PluginDescriptor(namespacesByName); + } + + public void toJson(final JsonGenerator generator) throws IOException { + generator.writeStartObject(); + for (final Namespace namespace : namespacesByName.values()) { + generator.writeFieldName(namespace.getName()); + namespace.toJson(generator); + } + generator.writeEndObject(); + } + + public Stream getNamespaces() { + return namespacesByName.values().stream(); + } + } + + /** + * Represents a namespace of plugins + */ + public static final class Namespace { + + private final String name; + + private final Map pluginsByClassName; + + private Namespace(final String name, final Map pluginsByClassName) { + this.name = name; + this.pluginsByClassName = pluginsByClassName; + } + + public String getName() { + return name; + } + + public Stream getPlugins() { + return pluginsByClassName.values().stream(); + } + + static Namespace fromDataInput(final DataInput dataInput) throws IOException { + final Map pluginsByClassName = new TreeMap<>(); + final String namespace = dataInput.readUTF(); + final int pluginCount = dataInput.readInt(); + for (int i = 0; i < pluginCount; i++) { + final Plugin plugin = Plugin.fromDataInput(dataInput); + pluginsByClassName.merge(plugin.getClassName(), plugin, Plugin::merge); + } + return new Namespace(namespace, Collections.unmodifiableMap(pluginsByClassName)); + } + + void writeDataOutput(final DataOutput dataOutput) throws IOException { + dataOutput.writeUTF(name); + final int pluginCount = countPluginNames(pluginsByClassName.values()); + dataOutput.writeInt(pluginCount); + for (final Plugin plugin : pluginsByClassName.values()) { + plugin.toDataOutput(dataOutput); + } + } + + static Namespace fromJson(final JsonParser parser, final String namespace) throws IOException { + final Map pluginsByClassName = new TreeMap<>(); + assertObjectStart(parser); + while (parser.nextToken() == JsonToken.FIELD_NAME) { + final String className = parser.currentName(); + final Plugin plugin = Plugin.fromJson(parser, className); + pluginsByClassName.put(className, plugin); + } + assertObjectEnd(parser); + return new Namespace(namespace, Collections.unmodifiableMap(pluginsByClassName)); + } + + void toJson(final JsonGenerator generator) throws IOException { + generator.writeStartObject(); + for (final Plugin plugin : pluginsByClassName.values()) { + generator.writeFieldName(plugin.getClassName()); + plugin.toJson(generator); + } + generator.writeEndObject(); + } + + Namespace merge(final Namespace other) { + final Map pluginsByClassName = new TreeMap<>(this.pluginsByClassName); + pluginsByClassName.putAll(other.pluginsByClassName); + return new Namespace(name, Collections.unmodifiableMap(pluginsByClassName)); + } + + Namespace withBuilderHierarchy(final ClassLoader classLoader) { + final Map newPluginsByClassName = new TreeMap<>(); + pluginsByClassName.forEach((k, v) -> { + try { + newPluginsByClassName.put(k, v.withBuilderHierarchy(classLoader)); + } catch (final PluginNotFoundException e) { + // No need to bother the user with the full cause + // Since we use Simple Logger, we only print the NoClassDefFound message. + logger.warn( + "Skipping plugin {} because it can not be loaded: {}", + e.getMessage(), + e.getCause().toString()); + } + }); + return new Namespace(name, Collections.unmodifiableMap(newPluginsByClassName)); + } + + private int countPluginNames(final Collection plugins) { + return plugins.stream() + .mapToInt(plugin -> plugin.getPluginNames().size()) + .sum(); + } + } + + /** + * Represents a single plugin. + */ + public static final class Plugin { + + private static final String BUILDER_HIERARCHY = "builderHierarchy"; + private static final String PLUGIN_NAMES = "pluginNames"; + private static final String ELEMENT_NAME = "elementName"; + private static final String PRINTABLE = "printable"; + private static final String DEFER = "defer"; + + /** + * The name and aliases of the plugin. + */ + private final Set pluginNames; + + /** + * The name and all the plugin aliases. + */ + private final String elementName; + + /** + * The name of the plugin class. + *

+ * Only present in the serialized representation. + * In JSON the className is encoded as key. + *

+ */ + private final String className; + + private final boolean printable; + + private final boolean defer; + + /** + * The fully qualified class name of the builder and its ancestor. + *

+ * Only present in the JSON representation. + *

+ */ + private final List builderHierarchy; + + private Plugin( + final Set pluginNames, + final String elementName, + final String className, + final boolean printable, + final boolean defer, + final List builderHierarchy) { + this.pluginNames = pluginNames; + this.elementName = elementName; + this.className = className; + this.printable = printable; + this.defer = defer; + this.builderHierarchy = builderHierarchy; + } + + public Set getPluginNames() { + return Collections.unmodifiableSet(pluginNames); + } + + public String getClassName() { + return className; + } + + public List getBuilderHierarchy() { + return Collections.unmodifiableList(builderHierarchy); + } + + static Plugin fromDataInput(final DataInput dataInput) throws IOException { + final String pluginName = dataInput.readUTF(); + final String className = dataInput.readUTF(); + final String elementName = dataInput.readUTF(); + final boolean printable = dataInput.readBoolean(); + final boolean defer = dataInput.readBoolean(); + return new Plugin( + Collections.singleton(pluginName), + elementName, + className, + printable, + defer, + Collections.emptyList()); + } + + void toDataOutput(final DataOutput dataOutput) throws IOException { + for (final String pluginName : pluginNames) { + dataOutput.writeUTF(pluginName); + dataOutput.writeUTF(className); + dataOutput.writeUTF(elementName); + dataOutput.writeBoolean(printable); + dataOutput.writeBoolean(defer); + } + } + + static Plugin fromJson(final JsonParser parser, final String className) throws IOException { + final Set pluginNames = new TreeSet<>(); + final List builderHierarchy = new ArrayList<>(); + String elementName = null; + boolean printable = false, defer = false; + assertObjectStart(parser); + while (parser.nextToken() == JsonToken.FIELD_NAME) { + switch (parser.currentName()) { + case PLUGIN_NAMES: + readArray(parser, pluginNames); + break; + case BUILDER_HIERARCHY: + // read and ignore + readArray(parser, builderHierarchy); + break; + case ELEMENT_NAME: + elementName = parser.nextTextValue(); + break; + case DEFER: + defer = parser.nextBooleanValue(); + break; + case PRINTABLE: + printable = parser.nextBooleanValue(); + break; + default: + throw new IOException( + "Unknown property " + parser.currentName() + " for Plugin element " + className); + } + } + assertObjectEnd(parser); + return new Plugin(pluginNames, elementName, className, printable, defer, builderHierarchy); + } + + void toJson(final JsonGenerator generator) throws IOException { + generator.writeStartObject(); + // Aliases + generator.writeArrayFieldStart(PLUGIN_NAMES); + for (final String pluginName : pluginNames) { + generator.writeString(pluginName); + } + generator.writeEndArray(); + // Simple fields + generator.writeStringField(ELEMENT_NAME, elementName); + generator.writeBooleanField(PRINTABLE, printable); + generator.writeBooleanField(DEFER, defer); + // Compute the class name of the builder + generator.writeArrayFieldStart(BUILDER_HIERARCHY); + for (final String fqcn : builderHierarchy) { + generator.writeString(fqcn); + } + generator.writeEndArray(); + generator.writeEndObject(); + } + + private Plugin merge(final Plugin other) { + // Intentionally ignore `elementName` + if (!this.className.equals(other.className) + || this.printable != other.printable + || this.defer != other.defer) { + throw new IllegalArgumentException( + "Attempting to merge incompatible plugins: " + this + " and " + other); + } + final Set pluginNames = new TreeSet<>(this.pluginNames); + pluginNames.addAll(other.pluginNames); + return new Plugin( + Collections.unmodifiableSet(pluginNames), + this.elementName, + this.className, + this.printable, + this.defer, + this.builderHierarchy); + } + + /** + * Computes the builder hierarchy. + * + * @param classLoader Class loader to use to load classes. + * @return A new plugin with the computed builder hierarchy. + */ + public Plugin withBuilderHierarchy(final ClassLoader classLoader) { + final List builderHierarchy = Collections.unmodifiableList(findBuilderClassHierarchy(classLoader)); + return new Plugin(pluginNames, elementName, className, printable, defer, builderHierarchy); + } + + @Override + public String toString() { + return "Plugin{" + "pluginNames=" + + pluginNames + ", elementName='" + + elementName + '\'' + ", className='" + + className + '\'' + ", printable=" + + printable + ", defer=" + + defer + ", builderHierarchy=" + + builderHierarchy + '}'; + } + + private List findBuilderClassHierarchy(final ClassLoader classLoader) { + try { + final Class pluginClass = classLoader.loadClass(className); + for (final Method method : pluginClass.getMethods()) { + for (final Annotation annotation : method.getAnnotations()) { + switch (annotation.annotationType().getName()) { + case PLUGIN_FACTORY: + // Continue the search until apache/logging-log4j2#3126 is fixed. + break; + case PLUGIN_BUILDER_FACTORY: + return computeClassHierarchy(findBuilderClass(method.getGenericReturnType())); + } + } + } + return Collections.emptyList(); + } catch (final ClassNotFoundException | LinkageError e) { + throw new PluginNotFoundException(pluginNames, e); + } + } + + private static Class findBuilderClass(final Type type) { + if (type instanceof Class) { + return ((Class) type); + } + if (type instanceof ParameterizedType) { + return findBuilderClass(((ParameterizedType) type).getRawType()); + } + if (type instanceof TypeVariable) { + return findBuilderClass(((TypeVariable) type).getBounds()[0]); + } + throw new IllegalArgumentException("Unable to handle reflective type: " + type); + } + + private static List computeClassHierarchy(final Class clazz) { + final List classes = new ArrayList<>(); + Class current = clazz; + while (current != null && !current.equals(Object.class)) { + classes.add(current.getName()); + current = current.getSuperclass(); + } + return classes; + } + + private static void readArray(final JsonParser parser, final Collection output) + throws IOException { + assertArrayStart(parser); + while (parser.nextToken() == JsonToken.VALUE_STRING) { + output.add(parser.getText()); + } + assertArrayEnd(parser); + } + } + + private static final class PluginNotFoundException extends RuntimeException { + + private PluginNotFoundException(final Set pluginNames, final Throwable cause) { + super(pluginNames.toString(), cause); + } + } +} diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java new file mode 100644 index 0000000..3999438 --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/internal/ReflectConfigFilter.java @@ -0,0 +1,60 @@ +/* + * 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.converter.plugins.internal; + +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayEnd; +import static org.apache.logging.log4j.converter.plugins.internal.JacksonUtils.assertArrayStart; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +public class ReflectConfigFilter { + + private static final String TYPE_NAME = "name"; + + private final Set includeClassNames; + + public ReflectConfigFilter(final Set includeClassNames) { + this.includeClassNames = new HashSet<>(includeClassNames); + } + + public void filter(final JsonParser input, final JsonGenerator output) throws IOException { + assertArrayStart(input); + output.writeStartArray(); + // Read and filter entries + while (input.nextToken() == JsonToken.START_OBJECT) { + final JsonNode node = input.readValueAsTree(); + final JsonNode nameNode = node.get(TYPE_NAME); + if (nameNode != null) { + final String name = nameNode.asText(); + // Include all the plugin visitors + if (name.startsWith("org.apache.logging.log4j.core.config.plugins.visitors") + || name.startsWith("org.apache.logging.log4j.core.config.plugins.validation.validators") + || includeClassNames.contains(name)) { + output.writeTree(node); + } + } + } + assertArrayEnd(input); + output.writeEndArray(); + } +} diff --git a/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java new file mode 100644 index 0000000..8a16a06 --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/java/org/apache/logging/log4j/converter/plugins/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +@NullMarked +package org.apache.logging.log4j.converter.plugins; + +import org.jspecify.annotations.NullMarked; diff --git a/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json b/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json new file mode 100644 index 0000000..d17d078 --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/resources/Log4j2Plugins.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://logging.apache.org/json/schema/Log4j2Plugins.schema.json", + "title": "Log4j 2.x Plugin Descriptor", + "description": "A JSON version of the `Log4j2Plugins.dat` file.", + "type": "object", + "additionalProperties": { + "type": { + "$ref": "#/definitions/namespace" + } + }, + "definitions": { + "plugin": { + "description": "Represents a Log4j Core Plugin.", + "type": "object", + "properties": { + "pluginNames": { + "description": "The name and aliases of the plugin in lowercase.", + "type": "array", + "items": { + "type": "string" + } + }, + "elementName": { + "description": "The generic element name/category of the plugin.", + "type": "string" + }, + "printable": { + "description": "Indicates if the plugin has a useful toString() method.", + "type": "boolean" + }, + "defer": { + "description": "Indicates if the instantiation of configuration parameters should be deferred.", + "type": "boolean" + }, + "builderHierarchy": { + "description": "The fully qualified name of the builder class and its ancestors.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "pluginNames", + "elementName", + "printable", + "defer" + ] + }, + "namespace": { + "description": "Represents a namespace of plugins. The key is the fully qualified class name of the plugin", + "type": "object", + "additionalProperties": { + "type": { + "$ref": "#/definitions/plugin" + } + } + } + } +} \ No newline at end of file diff --git a/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties new file mode 100644 index 0000000..a86ef7a --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.component.properties @@ -0,0 +1,20 @@ +# +# 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. +# +## +# Enable Simple Logger +# +log4j.provider = org.apache.logging.log4j.simple.internal.SimpleProvider diff --git a/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties new file mode 100644 index 0000000..c43843c --- /dev/null +++ b/log4j-converter-plugin-descriptor/src/main/shaded-resources/log4j2.simplelog.properties @@ -0,0 +1,20 @@ +# +# 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. +# +## +# Configures Simple Logger for the shaded package +# +log4j2.simplelogLevel = INFO diff --git a/log4j-transform-parent/pom.xml b/log4j-transform-parent/pom.xml index 31e48a0..be09e8e 100644 --- a/log4j-transform-parent/pom.xml +++ b/log4j-transform-parent/pom.xml @@ -40,13 +40,13 @@ 5.11.3 2.24.1 3.9.9 + 4.7.5 4.0.2 2.0.16 0.8.12 3.6.1 - 3.5.0 3.0.0-M7 @@ -120,6 +120,12 @@ ${maven.version} + + info.picocli + picocli + ${picocli.version} + + org.codehaus.plexus plexus-utils @@ -151,17 +157,12 @@ ${maven-invoker-plugin.version}
- - org.apache.maven - maven-shade-plugin - ${maven-shade-plugin.version} - - + java8-tests @@ -181,6 +182,75 @@ + + + + picocli + + + .picocli-application-activator + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + info.picocli + picocli-codegen + ${picocli.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-jar-with-dependencies + + shade + + + + + *:* + + module-info.class + META-INF/versions/*/module-info.class + META-INF/DEPENDENCIES + META-INF/MANIFEST.MF + + + + true + shaded + + + + + + + true + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index c9887ea..e220926 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ log4j-codegen + log4j-converter-plugin-descriptor log4j-transform-maven-plugin log4j-transform-maven-shade-plugin-extensions log4j-weaver @@ -132,6 +133,12 @@ ${project.version} + + org.apache.logging.log4j + log4j-converter-plugin-descriptor + ${project.version} + + org.apache.logging.log4j log4j-transform-maven-plugin diff --git a/src/site/antora/modules/ROOT/pages/cli.adoc b/src/site/antora/modules/ROOT/pages/cli.adoc index 43bb43b..5ae4da8 100644 --- a/src/site/antora/modules/ROOT/pages/cli.adoc +++ b/src/site/antora/modules/ROOT/pages/cli.adoc @@ -73,4 +73,189 @@ For a list of standard priorities see [source,subs="+attributes"] ---- java -jar log4j-codegen-{project-version}.jar extendedLogger DIAG=350 NOTICE=450 VERBOSE=550 ----- \ No newline at end of file +---- + +[#log4j-converter-plugin-descriptor] +== `log4j-converter-plugin-descriptor` + +The `log4j-converter-plugin-descriptor` tool helps you to create custom +https://logging.apache.org/log4j/2.x/manual/plugins.html#plugin-registry[plugin descriptors] +and align their content with the +https://www.graalvm.org/latest/reference-manual/native-image/metadata/[GraalVM reachability metadata]. +This can be used to create smaller GraalVM native images by removing the parts of Log4j Core that are not used by the application. + +[NOTE] +==== +Custom plugin descriptors are not required for applications running in the JVM. +If you are +https://logging.apache.org/log4j/2.x/faq.html#shading[shading/shadowing your application], +and you need to merge multiple plugin descriptors, use the +xref:log4j-transform-maven-shade-plugin-extensions.adoc#log4j-plugin-cache-transformer[Log4j Plugin Descriptor Transformer] +instead. +==== + +To create a custom plugin descriptor and reachability metadata descriptor, you need to: + +. Extract the information contained in the `Log4j2Plugins.dat` descriptors in your runtime classpath. +See <> on how to do it. +. Select the plugins that you want in your GraalVM application. +See <> for some tips on how to do it. +. Convert your list of plugins back into the `Log4j2Plugins.dat` format. +See <> for more information. +. Create a custom `reflect-config.json` using the reduced list of Log4j plugins. +See <> for more details. + +[#log4j-converter-plugin-descriptor-toJson] +=== Converting from `Log4j2Plugins.dat` to `Log4j2Plugins.json` + +To convert all the `Log4j2Plugins.dat` files on your application's classpath run: + +[source,subs="+attributes"] +---- +java -jar log4j-converter-plugin-descriptor-{project-version}.jar \ + toJson [-o=] ... +---- + +where: + +``:: +The directory, where the command's output will be saved. +Defaults to the current working directory. + +``:: +A list of file paths to the runtime dependencies of your application, separated by either spaces or your system path separator (`:` for UNIX and `;` for Windows). + +The command will generate a `Log4j2Plugins.json` file in the output directory. + +[#log4j-converter-plugin-descriptor-select] +=== Selecting plugins + +The `Log4j2Plugins.json` file contains all the +https://logging.apache.org/log4j/2.x/manual/plugins.html#declare-plugin[Log4j Plugins] +contained on your classpath and grouped by category/namespace. +A functional Log4j Core installation needs these categories: + +`configurationfactory`:: ++ +Unless you have a +https://logging.apache.org/log4j/2.x/manual/customconfig.html#ConfigurationFactory[custom `ConfigurationFactory`] +you need to include at least the configuration factory for your configuration format. + +`core`:: +This category contains all the plugins that can be used in a configuration file. +You can browse the +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-Configuration[plugin reference] +to choose those that you need. +A minimal Log4j Core installation will certainly need: +-- +* The +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-AppendersPlugin[`Appenders`] +and +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggersPlugin[`Loggers`] +plugins. +* Either the +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggerConfig-RootLogger[`Root`] +and +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-LoggerConfig[`Logger`] +plugins or the +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-async-AsyncLoggerConfig-RootLogger[`AsyncRoot`] +and +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-async-AsyncLoggerConfig[`AsyncLogger`] plugins. +* At least one +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-Appender[appender plugin]. +See +https://logging.apache.org/log4j/2.x/manual/appenders.html[Appenders] +for more information on appenders. +* At least one +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-Layout[layout plugin]. +See +https://logging.apache.org/log4j/2.x/manual/layouts.html[Layouts] +for more information on layouts. +-- +If you plan to define properties for +https://logging.apache.org/log4j/2.x/manual/configuration.html#property-substitution[property substitution] +in your configuration file, consider adding the +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-PropertiesPlugin[`Properties`] +and +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-config-Property[`Property`] +plugins. + +`converter`:: +If you plan to use +https://logging.apache.org/log4j/2.x/manual/pattern-layout.html[Pattern Layout] +you need to add some +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-pattern-PatternConverter[pattern converter plugins]. + +`jsontemplateresolverfactory`:: +To use +https://logging.apache.org/log4j/2.x/manual/json-template-layout.html[JSON Template Layout] +you need to add some +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-layout-template-json_org-apache-logging-log4j-layout-template-json-resolver-TemplateResolverFactory[template resolver factories]. + +`lookup`:: +The `lookup` category contains +https://logging.apache.org/log4j/2.x/manual/lookups.html[lookups] +that can be used to retrieve configuration values from external sources. +See also +https://logging.apache.org/log4j/2.x/plugin-reference.html#org-apache-logging-log4j_log4j-core_org-apache-logging-log4j-core-lookup-StrLookup[lookup plugins] +for a list of options. + +`typeconverter`:: +This category provides basic conversion capabilities. +Unless you know what you are doing, keep all the plugins. + +[#log4j-converter-plugin-descriptor-fromJson] +=== Creating a custom `Log4j2Plugins.dat` file + +Once you have chosen the plugins for your Log4j Core custom image, you need to convert the modified `Log4j2Plugins.json` file back to its original format. +To do that run: + +[source,subs="+attributes"] +---- +java -jar log4j-converter-plugin-descriptor-{project-version}.jar \ + fromJson [-o=] +---- + +where: + +``:: +The directory, where the command's output will be saved. +This parameter should point at the root of your application's classpath (e.g., the `src/main/resources`) folder. +Defaults to the current working directory. + +``:: +The path to the `Log4j2Plugins.json` file. + +The command will generate a `Log4j2Plugins.dat` file in the `org/apache/logging/log4j/core/config/plugins` subfolder of the output directory. + +[#log4j-converter-plugin-descriptor-filterReflectConfig] +=== Creating a custom `reflect-config.json` file + +The same `Log4j2Plugins.json` file can be used to trim the +https://www.graalvm.org/latest/reference-manual/native-image/metadata/[GraalVM reachability metadata] +embedded in Log4j `2.25.0` and later, so that they contain only the classes required by the selected plugins. +To extract all the `reflect-config.json` files from your runtime classpath and remove the unnecessary classes run: + +[source,subs="+attributes"] +---- +java -jar log4j-converter-plugin-descriptor-{project-version}.jar \ + filterReflectConfig [-o=] ... +---- + +where: + +``:: +The directory, where the command's output will be saved. +This parameter should point at the root of your application's classpath (e.g., the `src/main/resources`) folder. +Defaults to the current working directory. + +``:: +The path to the `Log4j2Plugins.json` file. + +``:: +A list of file paths to the runtime dependencies of your application, separated by either spaces or your system path separator (`:` for UNIX and `;` for Windows). + +The command will filter and output each `reflect-config.json` in its original path under the `META-INF/native-image` subfolder of the output directory. + +[#log4j-converter-plugin-descriptor-example] +=== Examples \ No newline at end of file diff --git a/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc b/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc index 71ce292..5ed11ca 100644 --- a/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc +++ b/src/site/antora/modules/ROOT/pages/log4j-transform-maven-shade-plugin-extensions.adoc @@ -20,7 +20,7 @@ This project contains a collection of https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html[resource transformer]s for the Apache Maven Shade Plugin that allows you to use additional Log4j 2.x Core component modules. [#log4j-plugin-cache-transformer] -== Log4j Plugin Cache Transformer +== Log4j Plugin Descriptor Transformer A https://maven.apache.org/plugins/maven-shade-plugin/examples/resource-transformers.html[resource transformer]