Repository: logging-log4j2 Updated Branches: refs/heads/master ecdc012e1 -> 6cee32d8b
[LOG4J2-1088] Add Comma Separated Value (CSV) layouts for parameter and event logging. Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/6cee32d8 Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/6cee32d8 Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/6cee32d8 Branch: refs/heads/master Commit: 6cee32d8b160e7b9f475fc0ff10d01a71bb2b78b Parents: ecdc012 Author: ggregory <[email protected]> Authored: Mon Sep 7 02:37:32 2015 -0700 Committer: ggregory <[email protected]> Committed: Mon Sep 7 02:37:32 2015 -0700 ---------------------------------------------------------------------- .../log4j/message/ObjectArrayMessage.java | 134 +++++++++++++++++ .../log4j/message/ObjectArrayMessageTest.java | 40 ++++++ log4j-core/pom.xml | 6 + .../logging/log4j/core/layout/CsvLayout.java | 97 +++++++++++++ .../log4j/core/layout/CsvLogEventLayout.java | 81 +++++++++++ .../log4j/core/layout/CsvParameterLayout.java | 88 ++++++++++++ .../core/layout/CsvLogEventLayoutTest.java | 136 ++++++++++++++++++ .../core/layout/CsvParameterLayoutTest.java | 143 +++++++++++++++++++ pom.xml | 6 + src/changes/changes.xml | 3 + src/site/xdoc/manual/layouts.xml.vm | 123 ++++++++++++++++ src/site/xdoc/runtime-dependencies.xml | 4 + 12 files changed, 861 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-api/src/main/java/org/apache/logging/log4j/message/ObjectArrayMessage.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ObjectArrayMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ObjectArrayMessage.java new file mode 100644 index 0000000..729d9c5 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ObjectArrayMessage.java @@ -0,0 +1,134 @@ +/* + * 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.message; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Arrays; + +/** + * Handles messages that contain an Object[]. + * <p> + * Created for use with the CSV layout. For example: + * </p> + * <p> + * {@code logger.debug(new ObjectArrayMessage(1, 2, "Bob"));} + * </p> + * + * @since 2.4 + */ +public final class ObjectArrayMessage implements Message { + + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + private static final long serialVersionUID = -5903272448334166185L; + + private transient Object[] array; + private transient String arrayString; + + /** + * Creates the ObjectMessage. + * + * @param obj + * The Object to format. + */ + public ObjectArrayMessage(final Object... obj) { + this.array = obj == null ? EMPTY_OBJECT_ARRAY : obj; + } + + private boolean equalObjectsOrStrings(final Object[] left, final Object[] right) { + return left.equals(right) || String.valueOf(left).equals(String.valueOf(right)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ObjectArrayMessage that = (ObjectArrayMessage) o; + return array == null ? that.array == null : equalObjectsOrStrings(array, that.array); + } + + /** + * Returns the object formatted using its toString method. + * + * @return the String representation of the object. + */ + @Override + public String getFormat() { + return getFormattedMessage(); + } + + /** + * Returns the formatted object message. + * + * @return the formatted object message. + */ + @Override + public String getFormattedMessage() { + // LOG4J2-763: cache formatted string in case obj changes later + if (arrayString == null) { + arrayString = Arrays.toString(array); + } + return arrayString; + } + + /** + * Returns the object as if it were a parameter. + * + * @return The object. + */ + @Override + public Object[] getParameters() { + return array; + } + + /** + * Returns null. + * + * @return null. + */ + @Override + public Throwable getThrowable() { + return null; + } + + @Override + public int hashCode() { + return array.hashCode(); + } + + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + array = (Object[]) in.readObject(); + } + + @Override + public String toString() { + return "ObjectArrayMessage[obj=" + getFormattedMessage() + ']'; + } + + private void writeObject(final ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeObject(array); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-api/src/test/java/org/apache/logging/log4j/message/ObjectArrayMessageTest.java ---------------------------------------------------------------------- diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/message/ObjectArrayMessageTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/message/ObjectArrayMessageTest.java new file mode 100644 index 0000000..5f11cf8 --- /dev/null +++ b/log4j-api/src/test/java/org/apache/logging/log4j/message/ObjectArrayMessageTest.java @@ -0,0 +1,40 @@ +/* + * 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.message; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @since 2.4 + */ +public class ObjectArrayMessageTest { + + private static final Object[] ARRAY = { "A", "B", "C" }; + private static final ObjectArrayMessage OBJECT_ARRAY_MESSAGE = new ObjectArrayMessage(ARRAY); + + @Test + public void testGetParameters() { + Assert.assertArrayEquals(ARRAY, OBJECT_ARRAY_MESSAGE.getParameters()); + } + + @Test + public void testGetThrowable() { + Assert.assertEquals(null, OBJECT_ARRAY_MESSAGE.getThrowable()); + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/pom.xml ---------------------------------------------------------------------- diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml index 92a1023..1c00fbc 100644 --- a/log4j-core/pom.xml +++ b/log4j-core/pom.xml @@ -120,6 +120,12 @@ <artifactId>commons-compress</artifactId> <optional>true</optional> </dependency> + <!-- Used for the CSV layout --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-csv</artifactId> + <optional>true</optional> + </dependency> <!-- TEST DEPENDENCIES --> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLayout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLayout.java new file mode 100644 index 0000000..67da734 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLayout.java @@ -0,0 +1,97 @@ +package org.apache.logging.log4j.core.layout; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.QuoteMode; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.status.StatusLogger; + +/** + * A Comma-Separated Value (CSV) layout. + * + * <p> + * Best used with: + * </p> + * <p> + * {@code logger.debug(new ObjectArrayMessage(1, 2, "Bob"));} + * </p> + * + * Depends on Apache Commons CSV 1.2. + * + * @since 2.4 + */ +@Plugin(name = "CsvLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public class CsvLayout extends AbstractStringLayout { + + private static final String CONTENT_TYPE = "text/csv"; + protected static final String DEFAULT_CHARSET = "UTF-8"; + protected static final String DEFAULT_FORMAT = "Default"; + private static final long serialVersionUID = 1L; + + protected static CSVFormat createFormat(final String format, final Character delimiter, final Character escape, final Character quote, final QuoteMode quoteMode, final String nullString, + final String recordSeparator) { + CSVFormat csvFormat = CSVFormat.valueOf(format); + if (delimiter != null) { + csvFormat = csvFormat.withDelimiter(delimiter); + } + if (escape != null) { + csvFormat = csvFormat.withEscape(escape); + } + if (quote != null) { + csvFormat = csvFormat.withQuote(quote); + } + if (quoteMode != null) { + csvFormat = csvFormat.withQuoteMode(quoteMode); + } + if (nullString != null) { + csvFormat = csvFormat.withNullString(nullString); + } + if (recordSeparator != null) { + csvFormat = csvFormat.withRecordSeparator(recordSeparator); + } + return csvFormat; + } + + private final CSVFormat format; + + protected CsvLayout(final Charset charset, final CSVFormat csvFormat, final String header, + final String footer) { + super(charset, toBytes(header, charset), toBytes(footer, charset)); + this.format = csvFormat; + } + + @Override + public String getContentType() { + return CONTENT_TYPE + "; charset=" + this.getCharset(); + } + + public CSVFormat getFormat() { + return format; + } + + @Override + public String toSerializable(final LogEvent event) { + final Message message = event.getMessage(); + final Object[] parameters = message.getParameters(); + final StringBuilder buffer = new StringBuilder(1024); + try { + // Revisit when 1.3 is out so that we do not need to create a new + // printer for each event. + // No need to close the printer. + final CSVPrinter printer = new CSVPrinter(buffer, getFormat()); + printer.printRecord(parameters); + return buffer.toString(); + } catch (final IOException e) { + StatusLogger.getLogger().error(message, e); + return getFormat().getCommentMarker() + " " + e; + } + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLogEventLayout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLogEventLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLogEventLayout.java new file mode 100644 index 0000000..7be65d6 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvLogEventLayout.java @@ -0,0 +1,81 @@ +package org.apache.logging.log4j.core.layout; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.QuoteMode; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.status.StatusLogger; + +public class CsvLogEventLayout extends CsvLayout { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public static CsvLogEventLayout createDefaultLayout() { + return new CsvLogEventLayout(Charset.forName(DEFAULT_CHARSET), CSVFormat.valueOf(DEFAULT_FORMAT), null, null); + } + + public static CsvLogEventLayout createLayout(final CSVFormat format) { + return new CsvLogEventLayout(Charset.forName(DEFAULT_CHARSET), format, null, null); + } + + @PluginFactory + public static CsvLogEventLayout createLayout( + // @formatter:off + @PluginAttribute(value = "format", defaultString = DEFAULT_FORMAT) final String format, + @PluginAttribute("delimiter") final Character delimiter, + @PluginAttribute("escape") final Character escape, + @PluginAttribute("quote") final Character quote, + @PluginAttribute("quoteMode") final QuoteMode quoteMode, + @PluginAttribute("nullString") final String nullString, + @PluginAttribute("recordSeparator") final String recordSeparator, + @PluginAttribute(value = "charset", defaultString = DEFAULT_CHARSET) final Charset charset, + @PluginAttribute("header") final String header, + @PluginAttribute("footer") final String footer) + // @formatter:on + { + + final CSVFormat csvFormat = createFormat(format, delimiter, escape, quote, quoteMode, nullString, recordSeparator); + return new CsvLogEventLayout(charset, csvFormat, header, footer); + } + + protected CsvLogEventLayout(final Charset charset, final CSVFormat csvFormat, final String header, final String footer) { + super(charset, csvFormat, header, footer); + } + + @Override + public String toSerializable(final LogEvent event) { + final StringBuilder buffer = new StringBuilder(1024); + try { + // Revisit when 1.3 is out so that we do not need to create a new + // printer for each event. + // No need to close the printer. + final CSVPrinter printer = new CSVPrinter(buffer, getFormat()); + printer.print(event.getNanoTime()); + printer.print(event.getTimeMillis()); + printer.print(event.getLevel()); + printer.print(event.getThreadName()); + printer.print(event.getMessage().getFormattedMessage()); + printer.print(event.getLoggerFqcn()); + printer.print(event.getLoggerName()); + printer.print(event.getMarker()); + printer.print(event.getThrownProxy()); + printer.print(event.getSource()); + printer.print(event.getContextMap()); + printer.print(event.getContextStack()); + printer.println(); + return buffer.toString(); + } catch (final IOException e) { + StatusLogger.getLogger().error(event.toString(), e); + return getFormat().getCommentMarker() + " " + e; + } + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvParameterLayout.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvParameterLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvParameterLayout.java new file mode 100644 index 0000000..09d1ef1 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/CsvParameterLayout.java @@ -0,0 +1,88 @@ +package org.apache.logging.log4j.core.layout; + +import java.io.IOException; +import java.nio.charset.Charset; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.QuoteMode; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.status.StatusLogger; + +/** + * A Comma-Separated Value (CSV) layout to log event parameters. + * The event message is currently ignored. + * + * <p> + * Best used with: + * </p> + * <p> + * {@code logger.debug(new ObjectArrayMessage(1, 2, "Bob"));} + * </p> + * + * Depends on Apache Commons CSV 1.2. + * + * @since 2.4 + */ +@Plugin(name = "CsvLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public class CsvParameterLayout extends CsvLayout { + + private static final long serialVersionUID = 1L; + + public static CsvLayout createDefaultLayout() { + return new CsvParameterLayout(Charset.forName(DEFAULT_CHARSET), CSVFormat.valueOf(DEFAULT_FORMAT), null, null); + } + + public static CsvLayout createLayout(final CSVFormat format) { + return new CsvParameterLayout(Charset.forName(DEFAULT_CHARSET), format, null, null); + } + + @PluginFactory + public static CsvLayout createLayout( + // @formatter:off + @PluginAttribute(value = "format", defaultString = DEFAULT_FORMAT) final String format, + @PluginAttribute("delimiter") final Character delimiter, + @PluginAttribute("escape") final Character escape, + @PluginAttribute("quote") final Character quote, + @PluginAttribute("quoteMode") final QuoteMode quoteMode, + @PluginAttribute("nullString") final String nullString, + @PluginAttribute("recordSeparator") final String recordSeparator, + @PluginAttribute(value = "charset", defaultString = DEFAULT_CHARSET) final Charset charset, + @PluginAttribute("header") final String header, + @PluginAttribute("footer") final String footer) + // @formatter:on + { + + final CSVFormat csvFormat = createFormat(format, delimiter, escape, quote, quoteMode, nullString, recordSeparator); + return new CsvParameterLayout(charset, csvFormat, header, footer); + } + + public CsvParameterLayout(final Charset charset, final CSVFormat csvFormat, final String header, final String footer) { + super(charset, csvFormat, header, footer); + } + + @Override + public String toSerializable(final LogEvent event) { + final Message message = event.getMessage(); + final Object[] parameters = message.getParameters(); + final StringBuilder buffer = new StringBuilder(1024); + try { + // Revisit when 1.3 is out so that we do not need to create a new + // printer for each event. + // No need to close the printer. + final CSVPrinter printer = new CSVPrinter(buffer, getFormat()); + printer.printRecord(parameters); + return buffer.toString(); + } catch (final IOException e) { + StatusLogger.getLogger().error(message, e); + return getFormat().getCommentMarker() + " " + e; + } + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvLogEventLayoutTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvLogEventLayoutTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvLogEventLayoutTest.java new file mode 100644 index 0000000..4df936e --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvLogEventLayoutTest.java @@ -0,0 +1,136 @@ +/* + * 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.core.layout; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.apache.commons.csv.CSVFormat; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.BasicConfigurationFactory; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.test.appender.ListAppender; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests {@link CsvLayout}. + * + * @since 2.4 + */ +public class CsvLogEventLayoutTest { + static ConfigurationFactory cf = new BasicConfigurationFactory(); + + @AfterClass + public static void cleanupClass() { + ConfigurationFactory.removeConfigurationFactory(cf); + ThreadContext.clearAll(); + } + + @BeforeClass + public static void setupClass() { + ThreadContext.clearAll(); + ConfigurationFactory.setConfigurationFactory(cf); + final LoggerContext ctx = LoggerContext.getContext(); + ctx.reconfigure(); + } + + private final LoggerContext ctx = LoggerContext.getContext(); + + private final Logger root = ctx.getLogger(""); + + @Test + public void testCustomCharset() { + final CsvLayout layout = CsvLogEventLayout.createLayout("Excel", null, null, null, null, null, null, + StandardCharsets.UTF_16, null, null); + assertEquals("text/csv; charset=UTF-16", layout.getContentType()); + } + + @Test + public void testDefaultCharset() { + final CsvLayout layout = CsvLogEventLayout.createDefaultLayout(); + assertEquals(StandardCharsets.UTF_8, layout.getCharset()); + } + + @Test + public void testDefaultContentType() { + final CsvLayout layout = CsvLogEventLayout.createDefaultLayout(); + assertEquals("text/csv; charset=UTF-8", layout.getContentType()); + } + + private void testLayout(final CSVFormat format) { + final CsvLayout layout = CsvLogEventLayout.createLayout(format); + final Map<String, Appender> appenders = root.getAppenders(); + for (final Appender appender : appenders.values()) { + root.removeAppender(appender); + } + // set up appender + final ListAppender appender = new ListAppender("List", null, layout, true, false); + appender.start(); + + // set appender on root and set level to debug + root.addAppender(appender); + root.setLevel(Level.DEBUG); + + root.debug("one={}, two={}, three={}", 1, 2, 3); + root.info("Hello"); + appender.stop(); + + final List<String> list = appender.getMessages(); + final String event0 = list.get(0); + final char del = format.getDelimiter(); + Assert.assertTrue(event0, event0.contains(del + "DEBUG" + del)); + final String quote = del == ',' ? "\"" : ""; + Assert.assertTrue(event0, event0.contains(del + quote + "one=1, two=2, three=3" + quote + del)); + final String event1 = list.get(1); + Assert.assertTrue(event1, event1.contains(del + "INFO" + del)); + } + + @Test + public void testLayoutDefault() throws Exception { + testLayout(CSVFormat.DEFAULT); + } + + @Test + public void testLayoutExcel() throws Exception { + testLayout(CSVFormat.EXCEL); + } + + @Test + public void testLayoutMySQL() throws Exception { + testLayout(CSVFormat.MYSQL); + } + + @Test + public void testLayoutRFC4180() throws Exception { + testLayout(CSVFormat.RFC4180); + } + + @Test + public void testLayoutTab() throws Exception { + testLayout(CSVFormat.TDF); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvParameterLayoutTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvParameterLayoutTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvParameterLayoutTest.java new file mode 100644 index 0000000..d9a04c5 --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/CsvParameterLayoutTest.java @@ -0,0 +1,143 @@ +/* + * 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.core.layout; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.apache.commons.csv.CSVFormat; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.BasicConfigurationFactory; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationFactory; +import org.apache.logging.log4j.message.ObjectArrayMessage; +import org.apache.logging.log4j.test.appender.ListAppender; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests {@link CsvLayout}. + * + * @since 2.4 + */ +public class CsvParameterLayoutTest { + static ConfigurationFactory cf = new BasicConfigurationFactory(); + + @AfterClass + public static void cleanupClass() { + ConfigurationFactory.removeConfigurationFactory(cf); + ThreadContext.clearAll(); + } + + @BeforeClass + public static void setupClass() { + ThreadContext.clearAll(); + ConfigurationFactory.setConfigurationFactory(cf); + final LoggerContext ctx = LoggerContext.getContext(); + ctx.reconfigure(); + } + + private final LoggerContext ctx = LoggerContext.getContext(); + + private final Logger root = ctx.getLogger(""); + + @Test + public void testCustomCharset() { + final CsvLayout layout = CsvParameterLayout.createLayout("Excel", null, null, null, null, null, null, + StandardCharsets.UTF_16, null, null); + assertEquals("text/csv; charset=UTF-16", layout.getContentType()); + } + + @Test + public void testDefaultCharset() { + final CsvLayout layout = CsvParameterLayout.createDefaultLayout(); + assertEquals(StandardCharsets.UTF_8, layout.getCharset()); + } + + @Test + public void testDefaultContentType() { + final CsvLayout layout = CsvParameterLayout.createDefaultLayout(); + assertEquals("text/csv; charset=UTF-8", layout.getContentType()); + } + + private void testLayoutNormalApi(final CsvLayout layout, boolean messageApi) throws Exception { + final Map<String, Appender> appenders = root.getAppenders(); + for (final Appender appender : appenders.values()) { + root.removeAppender(appender); + } + // set up appender + final ListAppender appender = new ListAppender("List", null, layout, true, false); + appender.start(); + + // set appender on root and set level to debug + root.addAppender(appender); + root.setLevel(Level.DEBUG); + + // output messages + if (messageApi) { + logDebugObjectArrayMessage(); + } else { + logDebugNormalApi(); + } + + appender.stop(); + + final List<String> list = appender.getMessages(); + final char d = layout.getFormat().getDelimiter(); + Assert.assertEquals("1" + d + "2" + d + "3", list.get(0)); + Assert.assertEquals("2" + d + "3", list.get(1)); + Assert.assertEquals("5" + d + "6", list.get(2)); + Assert.assertEquals("7" + d + "8" + d + "9" + d + "10", list.get(3)); + } + + private void logDebugNormalApi() { + root.debug(null, 1, 2, 3); + root.debug(null, 2, 3); + root.debug(null, 5, 6); + root.debug(null, 7, 8, 9, 10); + } + + private void logDebugObjectArrayMessage() { + root.debug(new ObjectArrayMessage(1, 2, 3)); + root.debug(new ObjectArrayMessage(2, 3)); + root.debug(new ObjectArrayMessage(5, 6)); + root.debug(new ObjectArrayMessage(7, 8, 9, 10)); + } + + @Test + public void testLayoutDefaultNormal() throws Exception { + testLayoutNormalApi(CsvParameterLayout.createDefaultLayout(), false); + } + + @Test + public void testLayoutDefaultObjectArrayMessage() throws Exception { + testLayoutNormalApi(CsvParameterLayout.createDefaultLayout(), true); + } + + @Test + public void testLayoutTab() throws Exception { + testLayoutNormalApi(CsvParameterLayout.createLayout(CSVFormat.TDF), true); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 9a74e24..019b089 100644 --- a/pom.xml +++ b/pom.xml @@ -692,6 +692,12 @@ <artifactId>commons-compress</artifactId> <version>1.10</version> </dependency> + <!-- Used for the CSV layout --> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-csv</artifactId> + <version>1.2</version> + </dependency> </dependencies> </dependencyManagement> <build> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 1f37bc8..a11ea10 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -34,6 +34,9 @@ <action issue="LOG4J2-1107" dev="ggregory" type="add" due-to="Mikael Ståldal"> New Appender for Apache Kafka. </action> + <action issue="LOG4J2-1088" dev="ggregory" type="add" due-to="Gary Gregory"> + Add Comma Separated Value (CSV) layouts for parameter and event logging. + </action> <action issue="LOG4J2-812" dev="rgoers" type="update"> PatternLayout timestamp formatting performance improvement: replaced synchronized SimpleDateFormat with Apache Commons FastDateFormat. This and better caching resulted in a ~3-30X faster timestamp formatting. http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/src/site/xdoc/manual/layouts.xml.vm ---------------------------------------------------------------------- diff --git a/src/site/xdoc/manual/layouts.xml.vm b/src/site/xdoc/manual/layouts.xml.vm index a04921d..0127bb2 100644 --- a/src/site/xdoc/manual/layouts.xml.vm +++ b/src/site/xdoc/manual/layouts.xml.vm @@ -49,6 +49,129 @@ where the default there is UTF-8. Each layout that extends <code>org.apache.logging.log4j.core.layout.AbstractStringLayout</code> can provide its own default. See each layout below. </p> + <a name="CSVLayouts"/> + <subsection name="CSV Layouts"> + <p> + The CSV layout can be used in two ways: First, using <code>CsvParameterLayout</code> to log event parameters + to create a custom database, usually to a logger and file appender uniquely configured for this purpose. + Second, using <code>CsvLogEventLayout</code> to log events to create a database, as an alternative to using a + full DBMS or using a JDBC driver that supports the CSV format. + </p> + <p> + The <code>CsvParameterLayout</code> converts an event's parameters into a CSV record, ignoring the message. + To log CSV records, you can use the usual Logger methods <code>info()</code>, <code>debug()</code>, and so on: + </p> + <pre class="prettyprint linenums"> +logger.info("Ignored", value1, value2, value3); +</pre> + <p> + Which will create the CSV record: + </p> + <pre class="prettyprint linenums"> +value1, value2, value3 +</pre> + <p> + Alternatively, you can use a <code>ObjectArrayMessage</code>, which only carries parameters: + </p> + <pre class="prettyprint linenums"> +logger.info(new ObjectArrayMessage(value1, value2, value3)); +</pre> + <p> + The layouts CsvParameterLayout and CsvLogEventLayout are configured with the following parameters: + </p> + <table> + <caption>CsvParameterLayout and CsvLogEventLayout</caption> + <tr> + <th>Parameter Name</th> + <th>Type</th> + <th>Description</th> + </tr> + <tr> + <td>format</td> + <td>String</td> + <td> + One of the predefined formats: <code>Default</code>, <code>Excel</code>, <code>MySQL</code>, + <code>RFC4180</code>, <code>TDF</code>. + See + <a href="https://commons.apache.org/proper/commons-csv/archives/1.2/apidocs/org/apache/commons/csv/CSVFormat.Predefined.html">CSVFormat.Predefined</a>. + </td> + </tr> + <tr> + <td>delimiter</td> + <td>Character</td> + <td>Sets the delimiter of the format to the specified character.</td> + </tr> + <tr> + <td>escape</td> + <td>Character</td> + <td>Sets the escape character of the format to the specified character.</td> + </tr> + <tr> + <td>quote</td> + <td>Character</td> + <td>Sets the quoteChar of the format to the specified character.</td> + </tr> + <tr> + <td>quoteMode</td> + <td>String</td> + <td> + Sets the output quote policy of the format to the specified value. One of: <code>ALL</code>, + <code>MINIMAL</code>, <code>NON_NUMERIC</code>, <code>NONE</code>. + </td> + </tr> + <tr> + <td>nullString</td> + <td>String</td> + <td>Writes null as the given nullString when writing records.</td> + </tr> + <tr> + <td>recordSeparator</td> + <td>String</td> + <td>Sets the record separator of the format to the specified String.</td> + </tr> + <tr> + <td>charset</td> + <td>Charset</td> + <td>The output Charset.</td> + </tr> + <tr> + <td>header</td> + <td>Sets the header to include when the stream is opened.</td> + <td>Desc.</td> + </tr> + <tr> + <td>footer</td> + <td>Sets the footer to include when the stream is closed.</td> + <td>Desc.</td> + </tr> + </table> + <p> + Logging as a CSV events looks like this: + </p> + <pre class="prettyprint linenums"> +logger.debug("one={}, two={}, three={}", 1, 2, 3); +</pre> + <p> + Produces a CSV record with the following fields: + <ol> + <li>Time Nanos</li> + <li>Time Millis</li> + <li>Level</li> + <li>Thread Name</li> + <li>Formatted Message</li> + <li>Logger FQCN</li> + <li>Logger Name</li> + <li>Marker</li> + <li>Thrown Proxy</li> + <li>Source</li> + <li>Context Map</li> + <li>Context Stack</li> + </ol> + </p> + <pre class="prettyprint linenums"> +0,1441617184044,DEBUG,main,"one=1, two=2, three=3",org.apache.logging.log4j.spi.AbstractLogger,,,,org.apache.logging.log4j.core.layout.CsvLogEventLayoutTest.testLayout(CsvLogEventLayoutTest.java:98),{},[] +</pre> + </subsection> <a name="JSONLayout"/> <subsection name="JSONLayout"> <!-- From Javadoc of org.apache.logging.log4j.core.layout.JSONLayout --> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/6cee32d8/src/site/xdoc/runtime-dependencies.xml ---------------------------------------------------------------------- diff --git a/src/site/xdoc/runtime-dependencies.xml b/src/site/xdoc/runtime-dependencies.xml index af8447f..bbee62a 100644 --- a/src/site/xdoc/runtime-dependencies.xml +++ b/src/site/xdoc/runtime-dependencies.xml @@ -53,6 +53,10 @@ <th>Requirements</th> </tr> <tr> + <td>CSV Layout</td> + <td><a href="https://commons.apache.org/proper/commons-csv/">Apache Commons CSV</a></td> + </tr> + <tr> <td>XML configuration</td> <td>-</td> </tr>
