From 53a4aa4bc505795bd7e170699aa0014967b40732 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Sun, 5 Jan 2014 15:39:54 -0600 Subject: [PATCH] Add LoggerStream and tests. To: log4j-dev@logging.apache.org - Fixes LOG4J2-481. --- .../main/java/org/apache/logging/log4j/Logger.java | 18 ++ .../apache/logging/log4j/spi/AbstractLogger.java | 29 +- .../org/apache/logging/log4j/spi/LoggerStream.java | 298 +++++++++++++++++++++ .../org/apache/logging/log4j/EventLoggerTest.java | 8 +- .../java/org/apache/logging/log4j/LoggerTest.java | 72 +++-- .../java/org/apache/logging/log4j/TestLogger.java | 3 + .../apache/logging/log4j/spi/LoggerStreamTest.java | 188 +++++++++++++ 7 files changed, 587 insertions(+), 29 deletions(-) create mode 100644 log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerStream.java create mode 100644 log4j-api/src/test/java/org/apache/logging/log4j/spi/LoggerStreamTest.java diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java b/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java index 4c3fd1f..d1eda02 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/Logger.java @@ -18,6 +18,7 @@ package org.apache.logging.log4j; import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.MessageFactory; +import org.apache.logging.log4j.spi.LoggerStream; /** * This is the central interface in the log4j package. Most logging @@ -459,6 +460,23 @@ public interface Logger { String getName(); /** + * Gets a print stream that logs lines to this logger. + * + * @param level the logging level + * @return print stream that logs printed lines to this logger. + */ + LoggerStream getStream(Level level); + + /** + * Gets a marked print stream that logs lines to this logger. + * + * @param marker the marker data specific to this log statement + * @param level the logging level + * @return print stream that logs printed lines to this logger. + */ + LoggerStream getStream(Marker marker, Level level); + + /** * Logs a message with the specific Marker at the {@link Level#INFO INFO} level. * * @param marker the marker data specific to this log statement diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java index b1e3315..d73a844 100644 --- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java @@ -16,8 +16,6 @@ */ package org.apache.logging.log4j.spi; -import java.io.Serializable; - import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; @@ -28,12 +26,14 @@ import org.apache.logging.log4j.message.ParameterizedMessageFactory; import org.apache.logging.log4j.message.StringFormattedMessage; import org.apache.logging.log4j.status.StatusLogger; +import java.io.Serializable; + /** * Base implementation of a Logger. It is highly recommended that any Logger implementation extend this class. */ public abstract class AbstractLogger implements Logger, Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; /** * Marker for flow tracing. @@ -854,6 +854,29 @@ public abstract class AbstractLogger implements Logger, Serializable { } /** + * Gets a print stream that logs lines to this logger. + * + * @param level the logging level + * @return print stream that logs printed lines to this logger. + */ + @Override + public LoggerStream getStream(final Level level) { + return new LoggerStream(this, level); + } + + /** + * Gets a marked print stream that logs lines to this logger. + * + * @param marker the marker data specific to this log statement + * @param level the logging level + * @return print stream that logs printed lines to this logger. + */ + @Override + public LoggerStream getStream(Marker marker, Level level) { + return new LoggerStream(this, marker, level); + } + + /** * Logs a message with the specific Marker at the INFO level. * * @param marker the marker data specific to this log statement diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerStream.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerStream.java new file mode 100644 index 0000000..6ac753e --- /dev/null +++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerStream.java @@ -0,0 +1,298 @@ +/* + * 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.spi; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.message.Message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Locale; + +/** + * Output stream that logs each line written to a pre-defined level. Can also + * be configured with a Marker. This class provides an interface that follows + * the {@link java.io.PrintStream} methods in spirit, but doesn't output to + * any external stream. This class should not be used as a stream for an + * underlying logger unless it's being used as a bridge. Otherwise, infinite + * loops may occur! + */ +public class LoggerStream extends PrintStream { + + final PrintStream stream; + + public LoggerStream(final AbstractLogger logger, final Level level) { + super(System.out); + stream = new PrintStream(new HelperStream(logger, null, level), true); + } + + public LoggerStream(final AbstractLogger logger, final Marker marker, final Level level) { + super(System.out); + stream = new PrintStream(new HelperStream(logger, marker, level), true); + } + + @Override + public void write(int b) { + stream.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + stream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) { + stream.write(b, off, len); + } + + @Override + public void flush() { + stream.flush(); + } + + @Override + public void close() { + stream.close(); + } + + @Override + public void print(boolean b) { + stream.print(b); + } + + @Override + public void print(char c) { + stream.print(c); + } + + @Override + public void print(int i) { + stream.print(i); + } + + @Override + public void print(long l) { + stream.print(l); + } + + @Override + public void print(float f) { + stream.print(f); + } + + @Override + public void print(double d) { + stream.print(d); + } + + @Override + public void print(char[] s) { + stream.print(s); + } + + @Override + public void print(String s) { + stream.print(s); + } + + @Override + public void print(Object obj) { + stream.print(obj); + } + + @Override + public void println() { + stream.println(); + } + + @Override + public void println(boolean x) { + stream.println(x); + } + + @Override + public void println(char x) { + stream.println(x); + } + + @Override + public void println(int x) { + stream.println(x); + } + + @Override + public void println(long x) { + stream.println(x); + } + + @Override + public void println(float x) { + stream.println(x); + } + + @Override + public void println(double x) { + stream.println(x); + } + + @Override + public void println(char[] x) { + stream.println(x); + } + + @Override + public void println(String x) { + stream.println(x); + } + + @Override + public void println(Object x) { + stream.println(x); + } + + @Override + public LoggerStream printf(String format, Object... args) { + stream.printf(format, args); + return this; + } + + @Override + public LoggerStream printf(Locale l, String format, Object... args) { + stream.printf(l, format, args); + return this; + } + + @Override + public LoggerStream append(char c) { + stream.append(c); + return this; + } + + @Override + public LoggerStream append(CharSequence csq) { + stream.append(csq); + return this; + } + + @Override + public LoggerStream append(CharSequence csq, int start, int end) { + stream.append(csq, start, end); + return this; + } + + @Override + public LoggerStream format(String format, Object... args) { + stream.format(format, args); + return this; + } + + @Override + public LoggerStream format(Locale l, String format, Object... args) { + stream.format(l, format, args); + return this; + } + + @Override + public boolean checkError() { + return stream.checkError(); + } + + @Override + public String toString() { + return "LoggerStream{" + + "stream=" + stream + + '}'; + } + + @Override + public boolean equals(Object other) { + return this == other + || !(other == null || getClass() != other.getClass()) + && stream.equals(((LoggerStream) other).stream); + } + + @Override + public int hashCode() { + return stream.hashCode(); + } + + private static class HelperStream extends ByteArrayOutputStream { + private static final String FQCN = LoggerStream.class.getName(); + private final AbstractLogger logger; + private final Level level; + private final Marker marker; + + private HelperStream(AbstractLogger logger, Marker marker, Level level) { + this.logger = logger; + this.marker = marker; + this.level = level; + } + + private void log(int upTo) { + if (upTo < 0 || upTo >= count) { + throw new IndexOutOfBoundsException(); + } + final Message message = logger.getMessageFactory().newMessage(extractLine(upTo)); + logger.log(marker, FQCN, level, message, null); + } + + private String extractLine(int upTo) { + final String line = new String(buf, 0, upTo); + leftShiftBuffer(upTo + 1); + return line; + } + + private void leftShiftBuffer(int numBytes) { + int remaining = count - numBytes; + if (remaining > 0) { + System.arraycopy(buf, numBytes, buf, 0, remaining); + count = remaining + 1; + } else { + reset(); + } + } + + private int lastIndexOf(int b) { + for (int i = count - 1; i >= 0; --i) { + if (buf[i] == b) { + return i; + } + } + return -1; + } + + @Override + public synchronized void write(int b) { + super.write(b); + if (b == '\n') { + log(count - 1); + } + } + + @Override + public synchronized void write(byte[] b, int off, int len) { + super.write(b, off, len); + int newLine = lastIndexOf('\n'); + if (newLine != -1) { + log(newLine); + } + } + } +} diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/EventLoggerTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/EventLoggerTest.java index 8522dd7..388cf58 100644 --- a/log4j-api/src/test/java/org/apache/logging/log4j/EventLoggerTest.java +++ b/log4j-api/src/test/java/org/apache/logging/log4j/EventLoggerTest.java @@ -23,8 +23,8 @@ import org.junit.Test; import java.util.List; import java.util.Locale; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.*; /** * @@ -51,8 +51,8 @@ public class EventLoggerTest { EventLogger.logEvent(msg); ThreadContext.clear(); assertEquals(1, results.size()); - final String expected = " OFF Audit [Transfer@18060 Amount=\"200.00\" FromAccount=\"123457\" ToAccount=\"123456\"] Transfer Complete"; - assertTrue("Incorrect structured data: " + results.get(0) + ", expected: " ,results.get(0).startsWith(expected)); + final String expected = "EVENT OFF Audit [Transfer@18060 Amount=\"200.00\" FromAccount=\"123457\" ToAccount=\"123456\"] Transfer Complete"; + assertThat("Incorrect structured data", results.get(0), startsWith(expected)); } } diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/LoggerTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/LoggerTest.java index 632d635..77435a6 100644 --- a/log4j-api/src/test/java/org/apache/logging/log4j/LoggerTest.java +++ b/log4j-api/src/test/java/org/apache/logging/log4j/LoggerTest.java @@ -16,21 +16,20 @@ */ package org.apache.logging.log4j; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -import java.util.Date; -import java.util.List; -import java.util.Locale; - import org.apache.logging.log4j.message.ParameterizedMessageFactory; import org.apache.logging.log4j.message.StringFormatterMessageFactory; import org.apache.logging.log4j.message.StructuredDataMessage; -import org.junit.Assert; +import org.apache.logging.log4j.spi.LoggerStream; import org.junit.Before; import org.junit.Test; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.junit.Assert.*; + /** * */ @@ -52,8 +51,8 @@ public class LoggerTest { logger.entry(); logger.exit(); assertEquals(2, results.size()); - assertTrue("Incorrect Entry", results.get(0).startsWith(" TRACE entry")); - assertTrue("incorrect Exit", results.get(1).startsWith(" TRACE exit")); + assertThat("Incorrect Entry", results.get(0), startsWith("ENTRY[ FLOW ] TRACE entry")); + assertThat("incorrect Exit", results.get(1), startsWith("EXIT[ FLOW ] TRACE exit")); } @@ -64,8 +63,8 @@ public class LoggerTest { } catch (final Exception e) { logger.catching(e); assertEquals(1, results.size()); - assertTrue("Incorrect Catching", - results.get(0).startsWith(" ERROR catching java.lang.NullPointerException")); + assertThat("Incorrect Catching", + results.get(0), startsWith("CATCHING[ EXCEPTION ] ERROR catching java.lang.NullPointerException")); } } @@ -239,28 +238,57 @@ public class LoggerTest { } @Test + public void getStream() { + final LoggerStream stream = logger.getStream(Level.DEBUG); + stream.println("Debug message 1"); + stream.print("Debug message 2"); + stream.println(); + stream.println(); + stream.print("Debug message 3\n"); + assertEquals(4, results.size()); + assertThat("Incorrect message", results.get(0), startsWith(" DEBUG Debug message 1")); + assertThat("Incorrect message", results.get(1), startsWith(" DEBUG Debug message 2")); + assertEquals("Message should be blank-ish", results.get(2), " DEBUG "); + assertThat("Incorrect message", results.get(3), startsWith(" DEBUG Debug message 3")); + } + + @Test + public void getStream_Marker() { + final LoggerStream stream = logger.getStream(MarkerManager.getMarker("HI"), Level.INFO); + stream.println("Hello, world!"); + stream.print("How about this?\n"); + stream.println("Is this thing on?"); + assertEquals(3, results.size()); + assertThat("Incorrect message.", results.get(0), startsWith("HI INFO Hello")); + assertThat("Incorrect message.", results.get(1), startsWith("HI INFO How about")); + assertThat("Incorrect message.", results.get(2), startsWith("HI INFO Is this")); + } + + @Test public void printf() { logger.printf(Level.DEBUG, "Debug message %d", 1); logger.printf(Level.DEBUG, MarkerManager.getMarker("Test"), "Debug message %d", 2); assertEquals(2, results.size()); - assertTrue("Incorrect message", results.get(0).startsWith(" DEBUG Debug message 1")); - assertTrue("Incorrect message", results.get(1).startsWith(" DEBUG Debug message 2")); + assertThat("Incorrect message", results.get(0), startsWith(" DEBUG Debug message 1")); + assertThat("Incorrect message", results.get(1), startsWith("Test DEBUG Debug message 2")); } + @Test public void getLoggerByNullClass() { // Returns a SimpleLogger - Assert.assertNotNull(LogManager.getLogger((Class) null)); + assertNotNull(LogManager.getLogger((Class) null)); } + @Test public void getLoggerByNullObject() { // Returns a SimpleLogger - Assert.assertNotNull(LogManager.getLogger((Object) null)); + assertNotNull(LogManager.getLogger((Object) null)); } @Test public void getLoggerByNullString() { // Returns a SimpleLogger - Assert.assertNotNull(LogManager.getLogger((String) null)); + assertNotNull(LogManager.getLogger((String) null)); } @Test @@ -356,15 +384,15 @@ public class LoggerTest { logger.info(MarkerManager.getMarker("EVENT"), msg); ThreadContext.clear(); assertEquals(1, results.size()); - assertTrue("Incorrect structured data: " + results.get(0),results.get(0).startsWith( - " INFO Transfer [Audit@18060 Amount=\"200.00\" FromAccount=\"123457\" ToAccount=\"123456\"] Transfer Complete")); + assertThat("Incorrect structured data: ", results.get(0), startsWith( + "EVENT INFO Transfer [Audit@18060 Amount=\"200.00\" FromAccount=\"123457\" ToAccount=\"123456\"] Transfer Complete")); } @Test public void throwing() { logger.throwing(new IllegalArgumentException("Test Exception")); assertEquals(1, results.size()); - assertTrue("Incorrect Throwing", - results.get(0).startsWith(" ERROR throwing java.lang.IllegalArgumentException: Test Exception")); + assertThat("Incorrect Throwing", + results.get(0), startsWith("THROWING[ EXCEPTION ] ERROR throwing java.lang.IllegalArgumentException: Test Exception")); } } diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/TestLogger.java b/log4j-api/src/test/java/org/apache/logging/log4j/TestLogger.java index 2238fc6..22f2008 100644 --- a/log4j-api/src/test/java/org/apache/logging/log4j/TestLogger.java +++ b/log4j-api/src/test/java/org/apache/logging/log4j/TestLogger.java @@ -54,6 +54,9 @@ public class TestLogger extends AbstractLogger { @Override public void log(final Marker marker, final String fqcn, final Level level, final Message msg, final Throwable throwable) { final StringBuilder sb = new StringBuilder(); + if (marker != null) { + sb.append(marker); + } sb.append(" "); sb.append(level.toString()); sb.append(" "); diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/spi/LoggerStreamTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/spi/LoggerStreamTest.java new file mode 100644 index 0000000..60fe421 --- /dev/null +++ b/log4j-api/src/test/java/org/apache/logging/log4j/spi/LoggerStreamTest.java @@ -0,0 +1,188 @@ +/* + * 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.spi; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.TestLogger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.Assert.*; + +/** + * @author msicker + * @version 1.0.0 + */ +@RunWith(Parameterized.class) +public class LoggerStreamTest { + private List results; + private LoggerStream stream; + private Level level; + private String logMessage; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList( + new Object[][]{ + { Level.DEBUG, "debug log string test" }, + { Level.INFO, "info log string test" }, + { Level.WARN, "DANGER ZONE" }, + { Level.ERROR, "MAYDAY! MAYDAY!" }, + { Level.FATAL, "ABANDON SHIP!" } + } + ); + } + + public LoggerStreamTest(final Level level, final String logMessage) { + this.level = level; + this.logMessage = logMessage; + } + + @Before + public void setUp() throws Exception { + TestLogger logger = (TestLogger) LogManager.getLogger(); + results = logger.getEntries(); + assertEmpty(); + stream = new LoggerStream(logger, level); + assertEmpty(); + } + + @After + public void tearDown() throws Exception { + results.clear(); + } + + private void assertEmpty() { + assertTrue("There should be no results yet.", results.isEmpty()); + } + + private void assertNumResults(int numResults) { + assertEquals("Unexpected number of results.", numResults, results.size()); + } + + private void assertMessageStartsWith(final String message) { + assertNumResults(1); + final String start = ' ' + level.name() + ' ' + message; + assertThat(results.get(0), startsWith(start)); + } + + @Test + public void testWrite_Int() throws Exception { + for (byte b : logMessage.getBytes()) { + stream.write(b); + assertEmpty(); + } + stream.write('\n'); + assertMessageStartsWith(logMessage); + } + + @Test + public void testWrite_ByteArray() throws Exception { + final byte[] bytes = logMessage.getBytes(); + stream.write(bytes); + assertEmpty(); + stream.write('\n'); + assertMessageStartsWith(logMessage); + } + + @Test + public void testWrite_ByteArray_Offset_Length() throws Exception { + final byte[] bytes = logMessage.getBytes(); + int middle = bytes.length/2; + int length = bytes.length - middle; + final String right = new String(bytes, middle, length); + stream.write(bytes, middle, length); + assertEmpty(); + stream.write('\n'); + assertMessageStartsWith(right); + } + + @Test + public void testPrint_Boolean() throws Exception { + stream.print(true); + assertEmpty(); + stream.println(); + assertMessageStartsWith("true"); + } + + @Test + public void testPrint_Character() throws Exception { + for (char c : logMessage.toCharArray()) { + stream.print(c); + assertEmpty(); + } + stream.println(); + assertMessageStartsWith(logMessage); + } + + @Test + public void testPrint_Integer() throws Exception { + int n = logMessage.codePointAt(0); + stream.print(n); + assertEmpty(); + stream.println(); + assertMessageStartsWith(String.valueOf(n)); + } + + @Test + public void testPrint_CharacterArray() throws Exception { + stream.print(logMessage.toCharArray()); + assertEmpty(); + stream.println(); + assertMessageStartsWith(logMessage); + } + + @Test + public void testPrint_String() throws Exception { + stream.print(logMessage); + assertEmpty(); + stream.println(); + assertMessageStartsWith(logMessage); + } + + @Test + public void testPrint_Object() throws Exception { + final Object o = logMessage; + stream.print(o); + assertEmpty(); + stream.println(); + assertMessageStartsWith(logMessage); + } + + @Test + public void testPrintf() throws Exception { + stream.printf("<<<%s>>>", logMessage); + assertEmpty(); + stream.println(); + assertMessageStartsWith("<<<" + logMessage); + } + + @Test + public void testFormat() throws Exception { + stream.format("[%s]", logMessage).println(); + assertMessageStartsWith("[" + logMessage); + } +} -- 1.8.5.2