Skip to content

Commit 8338f2c

Browse files
committed
ongoing work on JsonEnconder
Signed-off-by: Ceki Gulcu <[email protected]>
1 parent 0d2ccee commit 8338f2c

3 files changed

Lines changed: 262 additions & 26 deletions

File tree

Lines changed: 218 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,219 @@
1-
package ch.qos.logback.classic.encoder;public class JsonEncoder {
1+
/*
2+
* Logback: the reliable, generic, fast and flexible logging framework.
3+
* Copyright (C) 1999-2023, QOS.ch. All rights reserved.
4+
*
5+
* This program and the accompanying materials are dual-licensed under
6+
* either the terms of the Eclipse Public License v1.0 as published by
7+
* the Eclipse Foundation
8+
*
9+
* or (per the licensee's choosing)
10+
*
11+
* under the terms of the GNU Lesser General Public License version 2.1
12+
* as published by the Free Software Foundation.
13+
*/
14+
15+
package ch.qos.logback.classic.encoder;
16+
17+
import ch.qos.logback.classic.spi.ILoggingEvent;
18+
import ch.qos.logback.core.encoder.EncoderBase;
19+
import org.slf4j.Marker;
20+
import org.slf4j.event.KeyValuePair;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import static ch.qos.logback.core.CoreConstants.COLON_CHAR;
26+
import static ch.qos.logback.core.CoreConstants.COMMA_CHAR;
27+
import static ch.qos.logback.core.CoreConstants.DOUBLE_QUOTE_CHAR;
28+
import static ch.qos.logback.core.CoreConstants.UTF_8_CHARSET;
29+
import static ch.qos.logback.core.encoder.JsonEscapeUtil.jsonEscapeString;
30+
import static ch.qos.logback.core.model.ModelConstants.NULL_STR;
31+
32+
/**
33+
*
34+
*
35+
*/
36+
public class JsonEncoder extends EncoderBase<ILoggingEvent> {
37+
38+
39+
40+
static int DEFAULT_SIZE = 1024;
41+
static int DEFAULT_SIZE_WITH_THROWABLE = DEFAULT_SIZE*8;
42+
43+
static byte[] EMPTY_BYTES = new byte[0];
44+
45+
46+
public static final String CONTEXT_ATTR_NAME = "context";
47+
public static final String TIMESTAMP_ATTR_NAME = "timestamp";
48+
49+
public static final String NANOSECONDS_ATTR_NAME = "nanoseconds";
50+
51+
public static final String SEQUENCE_NUMBER_ATTR_NAME = "sequenceNumbers";
52+
53+
54+
public static final String LEVEL_ATTR_NAME = "level";
55+
public static final String MARKERS_ATTR_NAME = "markers";
56+
public static final String THREAD_ATTR_NAME = "thread";
57+
public static final String MDC_ATTR_NAME = "mdc";
58+
public static final String LOGGER_ATTR_NAME = "logger";
59+
public static final String MESSAGE_ATTR_NAME = "rawMessage";
60+
61+
public static final String ARGUMENT_ARRAY_ATTR_NAME = "arguments";
62+
public static final String KEY_VALUE_PAIRS_ATTR_NAME = "keyValuePairs";
63+
64+
public static final String THROWABLE_ATTR_NAME = "throwable";
65+
66+
private static final char OPEN_OBJ = '{';
67+
private static final char CLOSE_OBJ = '}';
68+
private static final char OPEN_ARRAY = '[';
69+
private static final char CLOSE_ARRAY = ']';
70+
71+
private static final char QUOTE = DOUBLE_QUOTE_CHAR;
72+
private static final char SP = ' ';
73+
private static final char ENTRY_SEPARATOR = COLON_CHAR;
74+
75+
private static final char COL_SP = COLON_CHAR+SP;
76+
77+
private static final char VALUE_SEPARATOR = COMMA_CHAR;
78+
79+
80+
81+
@Override
82+
public byte[] headerBytes() {
83+
return EMPTY_BYTES;
84+
}
85+
86+
@Override
87+
public byte[] encode(ILoggingEvent event) {
88+
final int initialCapacity = event.getThrowableProxy() == null ? DEFAULT_SIZE: DEFAULT_SIZE_WITH_THROWABLE;
89+
StringBuilder sb = new StringBuilder(initialCapacity);
90+
sb.append(OPEN_OBJ);
91+
92+
93+
sb.append(SEQUENCE_NUMBER_ATTR_NAME).append(COL_SP).append(event.getSequenceNumber());
94+
sb.append(VALUE_SEPARATOR);
95+
96+
97+
sb.append(TIMESTAMP_ATTR_NAME).append(COL_SP).append(event.getTimeStamp());
98+
sb.append(VALUE_SEPARATOR);
99+
100+
sb.append(NANOSECONDS_ATTR_NAME).append(COL_SP).append(event.getNanoseconds());
101+
sb.append(VALUE_SEPARATOR);
102+
103+
104+
String levelStr = event.getLevel() != null ? event.getLevel().levelStr : NULL_STR;
105+
sb.append(LEVEL_ATTR_NAME).append(COL_SP).append(QUOTE).append(levelStr).append(QUOTE);
106+
sb.append(VALUE_SEPARATOR);
107+
108+
sb.append(THREAD_ATTR_NAME).append(COL_SP).append(QUOTE).append(jsonSafeStr(event.getThreadName())).append(QUOTE);
109+
sb.append(VALUE_SEPARATOR);
110+
111+
sb.append(LOGGER_ATTR_NAME).append(COL_SP).append(QUOTE).append(event.getLoggerName()).append(QUOTE);
112+
sb.append(VALUE_SEPARATOR);
113+
114+
appendMarkers(sb, event);
115+
appendMDC(sb, event);
116+
appendKeyValuePairs(sb, event);
117+
118+
sb.append(MESSAGE_ATTR_NAME).append(COL_SP).append(QUOTE).append(jsonSafeStr(event.getMessage())).append(QUOTE);
119+
sb.append(VALUE_SEPARATOR);
120+
121+
appendArgumentArray(sb, event);
122+
123+
sb.append(CLOSE_OBJ);
124+
return sb.toString().getBytes(UTF_8_CHARSET);
125+
}
126+
127+
private void appendKeyValuePairs(StringBuilder sb, ILoggingEvent event) {
128+
List<KeyValuePair> kvpList = event.getKeyValuePairs();
129+
if(kvpList == null || kvpList.isEmpty())
130+
return;
131+
132+
sb.append(KEY_VALUE_PAIRS_ATTR_NAME).append(ENTRY_SEPARATOR).append(SP).append(OPEN_ARRAY);
133+
final int len = kvpList.size();
134+
for(int i = 0; i < len; i++) {
135+
KeyValuePair kvp = kvpList.get(i);
136+
sb.append(QUOTE).append(jsonSafeToString(kvp.key)).append(QUOTE);
137+
sb.append(COL_SP);
138+
sb.append(QUOTE).append(jsonSafeToString(kvp.value)).append(QUOTE);
139+
140+
if(i != len)
141+
sb.append(VALUE_SEPARATOR);
142+
}
143+
sb.append(CLOSE_ARRAY);
144+
}
145+
146+
private void appendArgumentArray(StringBuilder sb, ILoggingEvent event) {
147+
Object[] argumentArray = event.getArgumentArray();
148+
if(argumentArray == null)
149+
return;
150+
151+
sb.append(ARGUMENT_ARRAY_ATTR_NAME).append(ENTRY_SEPARATOR).append(SP).append(OPEN_ARRAY);
152+
final int len = argumentArray.length;
153+
for(int i = 0; i < len; i++) {
154+
sb.append(QUOTE).append(jsonSafeToString(argumentArray[i])).append(QUOTE);
155+
if(i != len)
156+
sb.append(VALUE_SEPARATOR);
157+
}
158+
sb.append(CLOSE_ARRAY);
159+
}
160+
161+
private void appendMarkers(StringBuilder sb, ILoggingEvent event) {
162+
List<Marker> markerList = event.getMarkerList();
163+
if(markerList == null)
164+
return;
165+
166+
sb.append(MARKERS_ATTR_NAME).append(ENTRY_SEPARATOR).append(SP).append(OPEN_ARRAY);
167+
final int len = markerList.size();
168+
for(int i = 0; i < len; i++) {
169+
sb.append(QUOTE).append(jsonSafeToString(markerList.get(i))).append(QUOTE);
170+
if(i != len)
171+
sb.append(VALUE_SEPARATOR);
172+
}
173+
sb.append(CLOSE_ARRAY);
174+
}
175+
176+
private String jsonSafeToString(Object o) {
177+
if(o == null)
178+
return NULL_STR;
179+
return jsonEscapeString(o.toString());
180+
}
181+
182+
private String jsonSafeStr(String s) {
183+
if(s == null)
184+
return NULL_STR;
185+
return jsonEscapeString(s);
186+
}
187+
188+
189+
private void appendMDC(StringBuilder sb, ILoggingEvent event) {
190+
Map<String, String> map = event.getMDCPropertyMap();
191+
192+
sb.append(MDC_ATTR_NAME).append(ENTRY_SEPARATOR).append(SP).append(OPEN_OBJ);
193+
if(isNotEmptyMap(map)) {
194+
map.entrySet().stream().forEach(e -> appendMapEntry(sb, e));
195+
}
196+
sb.append(CLOSE_OBJ);
197+
198+
}
199+
200+
private void appendMapEntry(StringBuilder sb, Map.Entry<String, String> entry) {
201+
if(entry == null)
202+
return;
203+
204+
sb.append(QUOTE).append(jsonSafeToString(entry.getKey())).append(QUOTE).append(COL_SP).append(QUOTE)
205+
.append(jsonSafeToString(entry.getValue())).append(QUOTE);
206+
}
207+
208+
boolean isNotEmptyMap(Map map) {
209+
if(map == null)
210+
return false;
211+
return !map.isEmpty();
212+
}
213+
214+
@Override
215+
public byte[] footerBytes() {
216+
return EMPTY_BYTES;
217+
}
218+
2219
}

logback-core/src/main/java/ch/qos/logback/core/encoder/JsonEscapeUtil.java

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ public class JsonEscapeUtil {
1818

1919
protected final static char[] HEXADECIMALS_TABLE = "0123456789ABCDEF".toCharArray();
2020

21-
2221
static final int ESCAPE_CODES_COUNT = 32;
2322

2423
static final String[] ESCAPE_CODES = new String[ESCAPE_CODES_COUNT];
2524

26-
2725
// From RFC-8259 page 5
2826

2927
// %x22 / ; " quotation mark U+0022
@@ -37,26 +35,33 @@ public class JsonEscapeUtil {
3735
// %x72 / ; r carriage return U+000D
3836

3937
static {
40-
for(char c = 0; c < ESCAPE_CODES_COUNT; c++) {
38+
for (char c = 0; c < ESCAPE_CODES_COUNT; c++) {
4139

42-
switch(c) {
43-
case 0x08: ESCAPE_CODES[c] = "\\b";
44-
break;
45-
case 0x09: ESCAPE_CODES[c] = "\\t";
40+
switch (c) {
41+
case 0x08:
42+
ESCAPE_CODES[c] = "\\b";
43+
break;
44+
case 0x09:
45+
ESCAPE_CODES[c] = "\\t";
4646
break;
47-
case 0x0A: ESCAPE_CODES[c] = "\\n";
47+
case 0x0A:
48+
ESCAPE_CODES[c] = "\\n";
4849
break;
49-
case 0x0C: ESCAPE_CODES[c] = "\\f";
50+
case 0x0C:
51+
ESCAPE_CODES[c] = "\\f";
5052
break;
51-
case 0x0D: ESCAPE_CODES[c] = "\\r";
53+
case 0x0D:
54+
ESCAPE_CODES[c] = "\\r";
5255
break;
5356
default:
54-
ESCAPE_CODES[c] = getEscapeCodeBelowASCII32(c);
57+
ESCAPE_CODES[c] = _computeEscapeCodeBelowASCII32(c);
5558
}
5659
}
5760
}
58-
static String getEscapeCodeBelowASCII32(char c) {
59-
if(c > 32) {
61+
62+
// this method should not be called by methods except the static initializer
63+
private static String _computeEscapeCodeBelowASCII32(char c) {
64+
if (c > 32) {
6065
throw new IllegalArgumentException("input must be less than 32");
6166
}
6267

@@ -69,36 +74,36 @@ static String getEscapeCodeBelowASCII32(char c) {
6974
int lowPart = c & 0x0F;
7075
sb.append(HEXADECIMALS_TABLE[lowPart]);
7176

72-
7377
return sb.toString();
7478
}
7579

7680
// %x22 / ; " quotation mark U+0022
7781
// %x5C / ; \ reverse solidus U+005C
7882

7983
static String getObligatoryEscapeCode(char c) {
80-
if(c < 32)
81-
return getEscapeCodeBelowASCII32(c);
82-
if(c == 0x22)
84+
if (c < 32)
85+
return ESCAPE_CODES[c];
86+
if (c == 0x22)
8387
return "\\\"";
84-
if(c == 0x5C)
88+
if (c == 0x5C)
8589
return "\\/";
8690

8791
return null;
8892
}
8993

90-
static String jsonEscapeString(String input) {
94+
static public String jsonEscapeString(String input) {
9195
int length = input.length();
92-
int lenthWithLeeway = (int) (length*1.1);
96+
int lenthWithLeeway = (int) (length * 1.1);
9397

9498
StringBuilder sb = new StringBuilder(lenthWithLeeway);
95-
for(int i = 0; i < length; i++) {
99+
for (int i = 0; i < length; i++) {
96100
final char c = input.charAt(i);
97101
String escaped = getObligatoryEscapeCode(c);
98-
if(escaped == null)
102+
if (escaped == null)
99103
sb.append(c);
100-
else
104+
else {
101105
sb.append(escaped);
106+
}
102107
}
103108

104109
return sb.toString();

logback-core/src/test/java/ch/qos/logback/core/encoder/JsonEscapeUtilTest.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class JsonEscapeUtilTest {
2222

2323
@Test
24-
public void testEscapeCodes() {
24+
public void smokeTestEscapeCodes() {
2525
assertEquals("\\u0001", JsonEscapeUtil.ESCAPE_CODES[1]);
2626
assertEquals("\\u0005", JsonEscapeUtil.ESCAPE_CODES[5]);
2727
assertEquals("\\b", JsonEscapeUtil.ESCAPE_CODES[8]);
@@ -36,9 +36,23 @@ public void testEscapeCodes() {
3636
}
3737

3838
@Test
39-
public void testEscapeString() {
39+
public void smokeTestEscapeString() {
4040
assertEquals("abc", JsonEscapeUtil.jsonEscapeString("abc"));
4141
assertEquals("{world: \\\"world\\\"}", JsonEscapeUtil.jsonEscapeString("{world: \"world\"}"));
4242
assertEquals("{world: "+'\\'+'"'+"world\\\"}", JsonEscapeUtil.jsonEscapeString("{world: \"world\"}"));
4343
}
44+
45+
@Test
46+
public void testEscapingLF() {
47+
String input = "{\nhello: \"wo\nrld\"}";
48+
System.out.println(input);
49+
assertEquals("{\\nhello: "+'\\'+'"'+"wo\\nrld\\\"}", JsonEscapeUtil.jsonEscapeString(input));
50+
}
51+
52+
@Test
53+
public void testEscapingTab() {
54+
String input = "{hello: \"\tworld\"}";
55+
System.out.println(input);
56+
assertEquals("{hello: "+'\\'+'"'+"\\tworld\\\"}", JsonEscapeUtil.jsonEscapeString(input));
57+
}
4458
}

0 commit comments

Comments
 (0)