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