diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java index 4bfd5bfff7..68f7c6e37c 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java @@ -1,140 +1,2 @@ -package ch.qos.logback.classic.encoder; - -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.encoder.JsonEncoderBase; -import ch.qos.logback.core.util.DirectJson; - -import java.util.ArrayList; -import java.util.List; - -/** - * This is a concrete JsonEncoder for {@link ILoggingEvent} that emits fields according to object's configuration. - * It is partially imported from penna, but adapted to logback's structure. - * - * @author Henry John Kupty - */ -public class JsonEncoder extends JsonEncoderBase { - - - // Excerpt below imported from - // ch.qos.logback.contrib.json.classic.JsonLayout - public static final String TIMESTAMP_ATTR_NAME = "timestamp"; - public static final String LEVEL_ATTR_NAME = "level"; - public static final String MARKERS_ATTR_NAME = "tags"; - public static final String THREAD_ATTR_NAME = "thread"; - public static final String MDC_ATTR_NAME = "mdc"; - public static final String LOGGER_ATTR_NAME = "logger"; - public static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; - public static final String MESSAGE_ATTR_NAME = "raw-message"; - public static final String EXCEPTION_ATTR_NAME = "exception"; - public static final String CONTEXT_ATTR_NAME = "context"; - - protected boolean includeLevel; - protected boolean includeThreadName; - protected boolean includeMDC; - protected boolean includeLoggerName; - protected boolean includeFormattedMessage; - protected boolean includeMessage; - protected boolean includeException; - protected boolean includeContextName; - - private final List> emitters; - - - public JsonEncoder() { - super(); - - emitters = new ArrayList<>(); - this.includeLevel = true; - this.includeThreadName = true; - this.includeMDC = true; - this.includeLoggerName = true; - this.includeFormattedMessage = true; - this.includeException = true; - this.includeContextName = true; - } - - //protected = new DirectJson(); - - - public void writeMessage(DirectJson jsonWriter, ILoggingEvent event) { - jsonWriter.writeStringValue(MESSAGE_ATTR_NAME, event.getMessage()); - } - - public void writeFormattedMessage(DirectJson jsonWriter, ILoggingEvent event) { - jsonWriter.writeStringValue(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); - } - - public void writeLogger(DirectJson jsonWriter, ILoggingEvent event) { - jsonWriter.writeStringValue(LOGGER_ATTR_NAME, event.getLoggerName()); - } - - public void writeThreadName(DirectJson jsonWriter, ILoggingEvent event) { - jsonWriter.writeStringValue(THREAD_ATTR_NAME, event.getThreadName()); - } - - public void writeLevel(DirectJson jsonWriter, ILoggingEvent event) { - jsonWriter.writeStringValue(LEVEL_ATTR_NAME, event.getLevel().levelStr); - } - - - public void writeMarkers(DirectJson jsonWriter, ILoggingEvent event) { - var markers = event.getMarkerList(); - if (!markers.isEmpty()) { - jsonWriter.openArray(MARKERS_ATTR_NAME); - for (var marker : markers) { - jsonWriter.writeString(marker.getName()); - jsonWriter.writeSep(); - } - // Close array will overwrite the last "," in the buffer, so we are OK - jsonWriter.closeArray(); - jsonWriter.writeSep(); - } - } - - public void writeMdc(DirectJson jsonWriter, ILoggingEvent event) { - var mdc = event.getMDCPropertyMap(); - if (!mdc.isEmpty()) { - jsonWriter.openObject(MDC_ATTR_NAME); - for (var entry : mdc.entrySet()) { - jsonWriter.writeStringValue(entry.getKey(), entry.getValue()); - } - jsonWriter.closeObject(); - jsonWriter.writeSep(); - } - } - - private void buildEmitterList() { - // This method should be re-entrant and allow for reconfiguring the emitters if something change; - emitters.clear(); - - // TODO figure out order - if (includeLevel) emitters.add(this::writeLevel); - if (includeMDC) emitters.add(this::writeMdc); - if (includeMessage) emitters.add(this::writeMessage); - if (includeFormattedMessage) emitters.add(this::writeFormattedMessage); - if (includeThreadName) emitters.add(this::writeThreadName); - if (includeLoggerName) emitters.add(this::writeLogger); - // TODO add fields missing: - // context - // exception - // custom data - // marker - } - - @Override - public byte[] encode(ILoggingEvent event) { - if (emitters.isEmpty()) { - buildEmitterList(); - } - DirectJson jsonWriter = new DirectJson(); - jsonWriter.openObject(); - - for (var emitter: emitters) { - emitter.write(jsonWriter, event); - } - - jsonWriter.closeObject(); - return jsonWriter.flush(); - } +package ch.qos.logback.classic.encoder;public class JsonEncoder { } diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder2.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder2.java new file mode 100644 index 0000000000..4bfd5bfff7 --- /dev/null +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder2.java @@ -0,0 +1,140 @@ +package ch.qos.logback.classic.encoder; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.encoder.JsonEncoderBase; +import ch.qos.logback.core.util.DirectJson; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a concrete JsonEncoder for {@link ILoggingEvent} that emits fields according to object's configuration. + * It is partially imported from penna, but adapted to logback's structure. + * + * @author Henry John Kupty + */ +public class JsonEncoder extends JsonEncoderBase { + + + // Excerpt below imported from + // ch.qos.logback.contrib.json.classic.JsonLayout + public static final String TIMESTAMP_ATTR_NAME = "timestamp"; + public static final String LEVEL_ATTR_NAME = "level"; + public static final String MARKERS_ATTR_NAME = "tags"; + public static final String THREAD_ATTR_NAME = "thread"; + public static final String MDC_ATTR_NAME = "mdc"; + public static final String LOGGER_ATTR_NAME = "logger"; + public static final String FORMATTED_MESSAGE_ATTR_NAME = "message"; + public static final String MESSAGE_ATTR_NAME = "raw-message"; + public static final String EXCEPTION_ATTR_NAME = "exception"; + public static final String CONTEXT_ATTR_NAME = "context"; + + protected boolean includeLevel; + protected boolean includeThreadName; + protected boolean includeMDC; + protected boolean includeLoggerName; + protected boolean includeFormattedMessage; + protected boolean includeMessage; + protected boolean includeException; + protected boolean includeContextName; + + private final List> emitters; + + + public JsonEncoder() { + super(); + + emitters = new ArrayList<>(); + this.includeLevel = true; + this.includeThreadName = true; + this.includeMDC = true; + this.includeLoggerName = true; + this.includeFormattedMessage = true; + this.includeException = true; + this.includeContextName = true; + } + + //protected = new DirectJson(); + + + public void writeMessage(DirectJson jsonWriter, ILoggingEvent event) { + jsonWriter.writeStringValue(MESSAGE_ATTR_NAME, event.getMessage()); + } + + public void writeFormattedMessage(DirectJson jsonWriter, ILoggingEvent event) { + jsonWriter.writeStringValue(FORMATTED_MESSAGE_ATTR_NAME, event.getFormattedMessage()); + } + + public void writeLogger(DirectJson jsonWriter, ILoggingEvent event) { + jsonWriter.writeStringValue(LOGGER_ATTR_NAME, event.getLoggerName()); + } + + public void writeThreadName(DirectJson jsonWriter, ILoggingEvent event) { + jsonWriter.writeStringValue(THREAD_ATTR_NAME, event.getThreadName()); + } + + public void writeLevel(DirectJson jsonWriter, ILoggingEvent event) { + jsonWriter.writeStringValue(LEVEL_ATTR_NAME, event.getLevel().levelStr); + } + + + public void writeMarkers(DirectJson jsonWriter, ILoggingEvent event) { + var markers = event.getMarkerList(); + if (!markers.isEmpty()) { + jsonWriter.openArray(MARKERS_ATTR_NAME); + for (var marker : markers) { + jsonWriter.writeString(marker.getName()); + jsonWriter.writeSep(); + } + // Close array will overwrite the last "," in the buffer, so we are OK + jsonWriter.closeArray(); + jsonWriter.writeSep(); + } + } + + public void writeMdc(DirectJson jsonWriter, ILoggingEvent event) { + var mdc = event.getMDCPropertyMap(); + if (!mdc.isEmpty()) { + jsonWriter.openObject(MDC_ATTR_NAME); + for (var entry : mdc.entrySet()) { + jsonWriter.writeStringValue(entry.getKey(), entry.getValue()); + } + jsonWriter.closeObject(); + jsonWriter.writeSep(); + } + } + + private void buildEmitterList() { + // This method should be re-entrant and allow for reconfiguring the emitters if something change; + emitters.clear(); + + // TODO figure out order + if (includeLevel) emitters.add(this::writeLevel); + if (includeMDC) emitters.add(this::writeMdc); + if (includeMessage) emitters.add(this::writeMessage); + if (includeFormattedMessage) emitters.add(this::writeFormattedMessage); + if (includeThreadName) emitters.add(this::writeThreadName); + if (includeLoggerName) emitters.add(this::writeLogger); + // TODO add fields missing: + // context + // exception + // custom data + // marker + } + + @Override + public byte[] encode(ILoggingEvent event) { + if (emitters.isEmpty()) { + buildEmitterList(); + } + DirectJson jsonWriter = new DirectJson(); + jsonWriter.openObject(); + + for (var emitter: emitters) { + emitter.write(jsonWriter, event); + } + + jsonWriter.closeObject(); + return jsonWriter.flush(); + } +} diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoder2Test.java similarity index 66% rename from logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java rename to logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoder2Test.java index 7122ce806b..c4a8b814a5 100644 --- a/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoder2Test.java @@ -29,13 +29,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; - - -@Disabled public class JsonEncoderTest { - - LoggerContext context = new LoggerContext(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); Logger logger = context.getLogger(PatternLayoutEncoderTest.class); @@ -54,7 +49,32 @@ public void smoke() throws IOException { ILoggingEvent event = makeLoggingEvent(msg); byte[] eventBytes = je.encode(event); baos.write(eventBytes); - assertEquals(msg, baos.toString()); + String witnessPattern = makeWitness(event); + assertEquals(witnessPattern, baos.toString()); + } + + @Test + public void twoEvents() throws IOException { + + ILoggingEvent event0 = makeLoggingEvent("hello"); + ILoggingEvent event1 = makeLoggingEvent("world"); + + byte[] eventBytes0 = je.encode(event0); + byte[] eventBytes1 = je.encode(event1); + + baos.write(eventBytes0); + baos.write(eventBytes1); + + String witnessPattern0 = makeWitness(event0); + String witnessPattern1 = makeWitness(event1); + + assertEquals(witnessPattern0+witnessPattern1, baos.toString()); + } + + + private static String makeWitness(ILoggingEvent event) { + return "{\"level\":\"" + event.getLevel() + "\",\"message\":\"" + event.getMessage() + "\",\"thread\":\"" + + event.getThreadName() + "\",\"logger\":\"" + event.getLoggerName() + "\"}"; } ILoggingEvent makeLoggingEvent(String message) { diff --git a/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java b/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java index 3afee877c1..af00bbd640 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java +++ b/logback-core/src/main/java/ch/qos/logback/core/CoreConstants.java @@ -13,6 +13,9 @@ */ package ch.qos.logback.core; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + public class CoreConstants { final public static String DISABLE_SERVLET_CONTAINER_INITIALIZER_KEY = "logbackDisableServletContainerInitializer"; @@ -107,6 +110,8 @@ public class CoreConstants { */ public static final String[] EMPTY_STRING_ARRAY = new String[] {}; + public static final Charset UTF_8_CHARSET = StandardCharsets.UTF_8; + /** * An empty Class array. */ @@ -129,6 +134,7 @@ public class CoreConstants { public static final char DASH_CHAR = '-'; public static final String DEFAULT_VALUE_SEPARATOR = ":-"; + public static final String NULL_STR = "null"; /** * Number of rows before in an HTML table before, we close the table and create * a new one diff --git a/logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEncoderBase.java b/logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEncoderBase2.java similarity index 100% rename from logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEncoderBase.java rename to logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEncoderBase2.java diff --git a/logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEscapeUtil.java b/logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEscapeUtil.java new file mode 100644 index 0000000000..99d9b44148 --- /dev/null +++ b/logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEscapeUtil.java @@ -0,0 +1,107 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2023, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.encoder; + +public class JsonEscapeUtil { + + protected final static char[] HEXADECIMALS_TABLE = "0123456789ABCDEF".toCharArray(); + + + static final int ESCAPE_CODES_COUNT = 32; + + static final String[] ESCAPE_CODES = new String[ESCAPE_CODES_COUNT]; + + + // From RFC-8259 page 5 + + // %x22 / ; " quotation mark U+0022 + // %x5C / ; \ reverse solidus U+005C + // %x2F / ; / solidus U+002F + + // %x62 / ; b backspace U+0008 + // %x74 / ; t tab U+0009 + // %x6E / ; n line feed U+000A + // %x66 / ; f form feed U+000C + // %x72 / ; r carriage return U+000D + + static { + for(char c = 0; c < ESCAPE_CODES_COUNT; c++) { + + switch(c) { + case 0x08: ESCAPE_CODES[c] = "\\b"; + break; + case 0x09: ESCAPE_CODES[c] = "\\t"; + break; + case 0x0A: ESCAPE_CODES[c] = "\\n"; + break; + case 0x0C: ESCAPE_CODES[c] = "\\f"; + break; + case 0x0D: ESCAPE_CODES[c] = "\\r"; + break; + default: + ESCAPE_CODES[c] = getEscapeCodeBelowASCII32(c); + } + } + } + static String getEscapeCodeBelowASCII32(char c) { + if(c > 32) { + throw new IllegalArgumentException("input must be less than 32"); + } + + StringBuilder sb = new StringBuilder(6); + sb.append("\\u00"); + + int highPart = c >> 4; + sb.append(HEXADECIMALS_TABLE[highPart]); + + int lowPart = c & 0x0F; + sb.append(HEXADECIMALS_TABLE[lowPart]); + + + return sb.toString(); + } + + // %x22 / ; " quotation mark U+0022 + // %x5C / ; \ reverse solidus U+005C + + static String getObligatoryEscapeCode(char c) { + if(c < 32) + return getEscapeCodeBelowASCII32(c); + if(c == 0x22) + return "\\\""; + if(c == 0x5C) + return "\\/"; + + return null; + } + + static String jsonEscapeString(String input) { + int length = input.length(); + int lenthWithLeeway = (int) (length*1.1); + + StringBuilder sb = new StringBuilder(lenthWithLeeway); + for(int i = 0; i < length; i++) { + final char c = input.charAt(i); + String escaped = getObligatoryEscapeCode(c); + if(escaped == null) + sb.append(c); + else + sb.append(escaped); + } + + return sb.toString(); + } + +} diff --git a/logback-core/src/main/java/ch/qos/logback/core/model/ModelConstants.java b/logback-core/src/main/java/ch/qos/logback/core/model/ModelConstants.java index 5b000a75a8..53c772c5e2 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/model/ModelConstants.java +++ b/logback-core/src/main/java/ch/qos/logback/core/model/ModelConstants.java @@ -13,10 +13,12 @@ */ package ch.qos.logback.core.model; +import ch.qos.logback.core.CoreConstants; + public class ModelConstants { public static final String DEBUG_SYSTEM_PROPERTY_KEY = "logback.debug"; - public static final String NULL_STR = "null"; + public static final String NULL_STR = CoreConstants.NULL_STR; } diff --git a/logback-core/src/test/java/ch/qos/logback/core/encoder/JsonEscapeUtilTest.java b/logback-core/src/test/java/ch/qos/logback/core/encoder/JsonEscapeUtilTest.java new file mode 100644 index 0000000000..88805d889d --- /dev/null +++ b/logback-core/src/test/java/ch/qos/logback/core/encoder/JsonEscapeUtilTest.java @@ -0,0 +1,44 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2023, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v1.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ + +package ch.qos.logback.core.encoder; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonEscapeUtilTest { + + @Test + public void testEscapeCodes() { + assertEquals("\\u0001", JsonEscapeUtil.ESCAPE_CODES[1]); + assertEquals("\\u0005", JsonEscapeUtil.ESCAPE_CODES[5]); + assertEquals("\\b", JsonEscapeUtil.ESCAPE_CODES[8]); + assertEquals("\\t", JsonEscapeUtil.ESCAPE_CODES[9]); + assertEquals("\\n", JsonEscapeUtil.ESCAPE_CODES[0x0A]); + assertEquals("\\u000B", JsonEscapeUtil.ESCAPE_CODES[0x0B]); + assertEquals("\\f", JsonEscapeUtil.ESCAPE_CODES[0x0C]); + assertEquals("\\r", JsonEscapeUtil.ESCAPE_CODES[0x0D]); + assertEquals("\\u000E", JsonEscapeUtil.ESCAPE_CODES[0x0E]); + + assertEquals("\\u001A", JsonEscapeUtil.ESCAPE_CODES[0x1A]); + } + + @Test + public void testEscapeString() { + assertEquals("abc", JsonEscapeUtil.jsonEscapeString("abc")); + assertEquals("{world: \\\"world\\\"}", JsonEscapeUtil.jsonEscapeString("{world: \"world\"}")); + assertEquals("{world: "+'\\'+'"'+"world\\\"}", JsonEscapeUtil.jsonEscapeString("{world: \"world\"}")); + } +} \ No newline at end of file