diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageCollectionMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageCollectionMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..19d083e0891088e6cf79e6e735075a7995f80ee2 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageCollectionMessage.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * A Message that is a collection of Messages. + * @param <T> The Message type. + */ +public interface MessageCollectionMessage<T> extends Message, Iterable<T> { + + +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/StructuredDataCollectionMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/StructuredDataCollectionMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..3199193dfae89cf16a3c656c420006a61797c553 --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/StructuredDataCollectionMessage.java @@ -0,0 +1,93 @@ +/* + * 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.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A collection of StructuredDataMessages. + */ +public class StructuredDataCollectionMessage implements MessageCollectionMessage<StructuredDataMessage> { + + private List<StructuredDataMessage> structuredDataMessageList; + + public StructuredDataCollectionMessage(List<StructuredDataMessage> messages) { + this.structuredDataMessageList = messages; + } + + @Override + public Iterator<StructuredDataMessage> iterator() { + return structuredDataMessageList.iterator(); + } + + @Override + public String getFormattedMessage() { + StringBuilder sb = new StringBuilder(); + for (StructuredDataMessage msg : structuredDataMessageList) { + sb.append(msg.getFormattedMessage()); + } + return sb.toString(); + } + + @Override + public String getFormat() { + StringBuilder sb = new StringBuilder(); + for (StructuredDataMessage msg : structuredDataMessageList) { + if (msg.getFormat() != null) { + if (sb.length() > 0) { + sb.append(", "); + } + sb.append(msg.getFormat()); + } + } + return sb.toString(); + } + + @Override + public Object[] getParameters() { + List<Object[]> objectList = new ArrayList<>(); + int count = 0; + for (StructuredDataMessage msg : structuredDataMessageList) { + Object[] objects = msg.getParameters(); + if (objects != null) { + objectList.add(objects); + count += objects.length; + } + } + Object[] objects = new Object[count]; + int index = 0; + for (Object[] objs : objectList) { + for (Object obj : objs) { + objects[index++] = obj; + } + } + return objects; + } + + @Override + public Throwable getThrowable() { + for (StructuredDataMessage msg : structuredDataMessageList) { + Throwable t = msg.getThrowable(); + if (t != null) { + return t; + } + } + return null; + } +} diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/ProcessIdUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProcessIdUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..98522918f8fa184332684c623d2366de03f91b2d --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ProcessIdUtil.java @@ -0,0 +1,8 @@ +package org.apache.logging.log4j.util; + +public class ProcessIdUtil { + + public static String getProcessId() { + return "-"; + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java index 304e0a0f651f4765ef578ba3e48f0e59c0bc2c35..77466b1df8af0a0f4f25f13871ffde13057ffb26 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/Rfc5424Layout.java @@ -51,8 +51,11 @@ import org.apache.logging.log4j.core.pattern.ThrowablePatternConverter; import org.apache.logging.log4j.core.util.NetUtils; import org.apache.logging.log4j.core.util.Patterns; import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.MessageCollectionMessage; +import org.apache.logging.log4j.message.StructuredDataCollectionMessage; import org.apache.logging.log4j.message.StructuredDataId; import org.apache.logging.log4j.message.StructuredDataMessage; +import org.apache.logging.log4j.util.ProcessIdUtil; import org.apache.logging.log4j.util.StringBuilders; import org.apache.logging.log4j.util.Strings; @@ -190,8 +193,7 @@ public final class Rfc5424Layout extends AbstractStringLayout { final String name = config == null ? null : config.getName(); configName = Strings.isNotEmpty(name) ? name : null; this.fieldFormatters = createFieldFormatters(loggerFields, config); - // TODO Java 9: ProcessHandle.current().getPid(); - this.procId = "-"; + this.procId = ProcessIdUtil.getProcessId(); } private Map<String, FieldFormatter> createFieldFormatters(final LoggerFields[] loggerFields, @@ -331,8 +333,8 @@ public final class Rfc5424Layout extends AbstractStringLayout { private void appendMessage(final StringBuilder buffer, final LogEvent event) { final Message message = event.getMessage(); // This layout formats StructuredDataMessages instead of delegating to the Message itself. - final String text = (message instanceof StructuredDataMessage) ? message.getFormat() : message - .getFormattedMessage(); + final String text = (message instanceof StructuredDataMessage || message instanceof MessageCollectionMessage) + ? message.getFormat() : message.getFormattedMessage(); if (text != null && text.length() > 0) { buffer.append(' ').append(escapeNewlines(text, escapeNewLine)); @@ -352,7 +354,8 @@ public final class Rfc5424Layout extends AbstractStringLayout { private void appendStructuredElements(final StringBuilder buffer, final LogEvent event) { final Message message = event.getMessage(); - final boolean isStructured = message instanceof StructuredDataMessage; + final boolean isStructured = message instanceof StructuredDataMessage || + message instanceof StructuredDataCollectionMessage; if (!isStructured && (fieldFormatters != null && fieldFormatters.isEmpty()) && !includeMdc) { buffer.append('-'); @@ -387,18 +390,12 @@ public final class Rfc5424Layout extends AbstractStringLayout { } if (isStructured) { - final StructuredDataMessage data = (StructuredDataMessage) message; - final Map<String, String> map = data.getData(); - final StructuredDataId id = data.getId(); - final String sdId = getId(id); - - if (sdElements.containsKey(sdId)) { - final StructuredDataElement union = sdElements.get(id.toString()); - union.union(map); - sdElements.put(sdId, union); + if (message instanceof MessageCollectionMessage) { + for (StructuredDataMessage data : ((StructuredDataCollectionMessage)message)) { + addStructuredData(sdElements, data); + } } else { - final StructuredDataElement formattedData = new StructuredDataElement(map, eventPrefix, false); - sdElements.put(sdId, formattedData); + addStructuredData(sdElements, (StructuredDataMessage) message); } } @@ -412,6 +409,21 @@ public final class Rfc5424Layout extends AbstractStringLayout { } } + private void addStructuredData(final Map<String, StructuredDataElement> sdElements, final StructuredDataMessage data) { + final Map<String, String> map = data.getData(); + final StructuredDataId id = data.getId(); + final String sdId = getId(id); + + if (sdElements.containsKey(sdId)) { + final StructuredDataElement union = sdElements.get(id.toString()); + union.union(map); + sdElements.put(sdId, union); + } else { + final StructuredDataElement formattedData = new StructuredDataElement(map, eventPrefix, false); + sdElements.put(sdId, formattedData); + } + } + private String escapeNewlines(final String text, final String replacement) { if (null == replacement) { return text; diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java index fbfd8f544a39f95875d776dfb5f35b49bb814901..9c3792a4ee57bb20057aceb4c647d63bf6788928 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/layout/Rfc5424LayoutTest.java @@ -16,6 +16,7 @@ */ package org.apache.logging.log4j.core.layout; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -31,6 +32,7 @@ import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.net.Facility; import org.apache.logging.log4j.core.util.KeyValuePair; import org.apache.logging.log4j.junit.ThreadContextRule; +import org.apache.logging.log4j.message.StructuredDataCollectionMessage; import org.apache.logging.log4j.message.StructuredDataMessage; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.test.appender.ListAppender; @@ -58,6 +60,9 @@ public class Rfc5424LayoutTest { private static final String lineEscaped4 = "ATM - Audit [Transfer@18060 Amount=\"200.00\" FromAccount=\"123457\" ToAccount=\"123456\"]" + "[RequestContext@3692 escaped=\"Testing escaping #012 \\\" \\] \\\"\" ipAddress=\"192.168.0.120\" loginId=\"JohnDoe\"] Transfer Complete"; + private static final String collectionLine = "[Transfer@18060 Amount=\"200.00\" FromAccount=\"123457\" " + + "ToAccount=\"123456\"][Extra@18060 Item1=\"Hello\" Item2=\"World\"][RequestContext@3692 " + + "ipAddress=\"192.168.0.120\" loginId=\"JohnDoe\"] Transfer Complete"; static ConfigurationFactory cf = new BasicConfigurationFactory(); @@ -148,6 +153,72 @@ public class Rfc5424LayoutTest { list = appender.getMessages(); assertTrue("No messages expected, found " + list.size(), list.isEmpty()); } finally { + ThreadContext.clearMap(); + root.removeAppender(appender); + appender.stop(); + } + } + + /** + * Test case for MDC conversion pattern. + */ + @Test + public void testCollection() throws Exception { + for (final Appender appender : root.getAppenders().values()) { + root.removeAppender(appender); + } + // set up appender + final AbstractStringLayout layout = Rfc5424Layout.createLayout(Facility.LOCAL0, "Event", 3692, true, "RequestContext", + null, null, true, null, "ATM", null, "key1, key2, locale", null, "loginId", null, true, null, null); + 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); + + ThreadContext.put("loginId", "JohnDoe"); + ThreadContext.put("ipAddress", "192.168.0.120"); + ThreadContext.put("locale", Locale.US.getDisplayName()); + try { + final StructuredDataMessage msg = new StructuredDataMessage("Transfer@18060", "Transfer Complete", "Audit"); + msg.put("ToAccount", "123456"); + msg.put("FromAccount", "123457"); + msg.put("Amount", "200.00"); + final StructuredDataMessage msg2 = new StructuredDataMessage("Extra@18060", null, "Audit"); + msg2.put("Item1", "Hello"); + msg2.put("Item2", "World"); + List<StructuredDataMessage> messages = new ArrayList<>(); + messages.add(msg); + messages.add(msg2); + final StructuredDataCollectionMessage collectionMessage = new StructuredDataCollectionMessage(messages); + + root.info(MarkerManager.getMarker("EVENT"), collectionMessage); + + List<String> list = appender.getMessages(); + + assertTrue("Expected line 1 to end with: " + collectionLine + " Actual " + list.get(0), + list.get(0).endsWith(collectionLine)); + + for (final String frame : list) { + int length = -1; + final int frameLength = frame.length(); + final int firstSpacePosition = frame.indexOf(' '); + final String messageLength = frame.substring(0, firstSpacePosition); + try { + length = Integer.parseInt(messageLength); + // the ListAppender removes the ending newline, so we expect one less size + assertEquals(frameLength, messageLength.length() + length); + } + catch (final NumberFormatException e) { + assertTrue("Not a valid RFC 5425 frame", false); + } + } + + appender.clear(); + } finally { + ThreadContext.clearMap(); root.removeAppender(appender); appender.stop(); }