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.ClassicTestConstants;
18  import ch.qos.logback.classic.Level;
19  import ch.qos.logback.classic.Logger;
20  import ch.qos.logback.classic.LoggerContext;
21  import ch.qos.logback.classic.joran.JoranConfigurator;
22  import ch.qos.logback.classic.jsonTest.JsonLoggingEvent;
23  import ch.qos.logback.classic.jsonTest.JsonStringToLoggingEventMapper;
24  import ch.qos.logback.classic.jsonTest.ThrowableProxyComparator;
25  import ch.qos.logback.classic.spi.ILoggingEvent;
26  import ch.qos.logback.classic.spi.LoggingEvent;
27  import ch.qos.logback.classic.util.LogbackMDCAdapter;
28  import ch.qos.logback.core.joran.spi.JoranException;
29  import ch.qos.logback.core.read.ListAppender;
30  import ch.qos.logback.core.status.testUtil.StatusChecker;
31  import ch.qos.logback.core.testUtil.RandomUtil;
32  import ch.qos.logback.core.util.StatusPrinter;
33  import com.fasterxml.jackson.core.JsonProcessingException;
34  import org.junit.jupiter.api.AfterEach;
35  import org.junit.jupiter.api.BeforeEach;
36  import org.junit.jupiter.api.Test;
37  import org.slf4j.Marker;
38  import org.slf4j.event.KeyValuePair;
39  import org.slf4j.helpers.BasicMarkerFactory;
40  
41  import java.io.IOException;
42  import java.nio.charset.StandardCharsets;
43  import java.nio.file.Files;
44  import java.nio.file.Path;
45  import java.util.Arrays;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Objects;
50  
51  import static org.junit.jupiter.api.Assertions.assertEquals;
52  import static org.junit.jupiter.api.Assertions.assertTrue;
53  
54  // When running from an IDE, add the following on the command line
55  //
56  //          --add-opens ch.qos.logback.classic/ch.qos.logback.classic.jsonTest=ALL-UNNAMED
57  //
58  class JsonEncoderTest {
59  
60      int diff = RandomUtil.getPositiveInt();
61  
62      LoggerContext loggerContext = new LoggerContext();
63      StatusChecker statusChecker = new StatusChecker(loggerContext);
64      Logger logger = loggerContext.getLogger(JsonEncoderTest.class);
65  
66      JsonEncoder jsonEncoder = new JsonEncoder();
67  
68      BasicMarkerFactory markerFactory = new BasicMarkerFactory();
69  
70      Marker markerA = markerFactory.getMarker("A");
71  
72      Marker markerB = markerFactory.getMarker("B");
73  
74      ListAppender<ILoggingEvent> listAppender = new ListAppender();
75      JsonStringToLoggingEventMapper stringToLoggingEventMapper = new JsonStringToLoggingEventMapper(markerFactory);
76  
77      LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
78  
79      @BeforeEach
80      void setUp() {
81          loggerContext.setName("test_" + diff);
82          loggerContext.setMDCAdapter(logbackMDCAdapter);
83  
84          jsonEncoder.setContext(loggerContext);
85          jsonEncoder.start();
86  
87          listAppender.setContext(loggerContext);
88          listAppender.start();
89      }
90  
91      @AfterEach
92      void tearDown() {
93      }
94  
95      @Test
96      void smoke() throws JsonProcessingException {
97          LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, null);
98  
99          byte[] resultBytes = jsonEncoder.encode(event);
100         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
101         //System.out.println(resultString);
102         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
103         compareEvents(event, resultEvent);
104     }
105 
106     @Test
107     void contextWithProperties() throws JsonProcessingException {
108         loggerContext.putProperty("k", "v");
109         loggerContext.putProperty("k" + diff, "v" + diff);
110 
111         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, null);
112 
113         byte[] resultBytes = jsonEncoder.encode(event);
114         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
115         // System.out.println(resultString);
116 
117         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
118         compareEvents(event, resultEvent);
119 
120     }
121 
122     private static void compareEvents(LoggingEvent event, JsonLoggingEvent resultEvent) {
123         assertEquals(event.getSequenceNumber(), resultEvent.getSequenceNumber());
124         assertEquals(event.getTimeStamp(), resultEvent.getTimeStamp());
125         assertEquals(event.getLevel(), resultEvent.getLevel());
126         assertEquals(event.getLoggerName(), resultEvent.getLoggerName());
127         assertEquals(event.getThreadName(), resultEvent.getThreadName());
128         assertEquals(event.getMarkerList(), resultEvent.getMarkerList());
129         assertEquals(event.getMDCPropertyMap(), resultEvent.getMDCPropertyMap());
130         assertTrue(compareKeyValuePairLists(event.getKeyValuePairs(), resultEvent.getKeyValuePairs()));
131 
132         assertEquals(event.getLoggerContextVO(), resultEvent.getLoggerContextVO());
133         assertTrue(ThrowableProxyComparator.areEqual(event.getThrowableProxy(), resultEvent.getThrowableProxy()));
134 
135         assertEquals(event.getMessage(), resultEvent.getMessage());
136         assertEquals(event.getFormattedMessage(), resultEvent.getFormattedMessage());
137 
138         assertTrue(Arrays.equals(event.getArgumentArray(), resultEvent.getArgumentArray()));
139 
140     }
141 
142     private static boolean compareKeyValuePairLists(List<KeyValuePair> leftList, List<KeyValuePair> rightList) {
143         if (leftList == rightList)
144             return true;
145 
146         if (leftList == null || rightList == null)
147             return false;
148 
149         int length = leftList.size();
150         if (rightList.size() != length) {
151             System.out.println("length discrepancy");
152             return false;
153         }
154 
155         //System.out.println("checking KeyValuePair lists");
156 
157         for (int i = 0; i < length; i++) {
158             KeyValuePair leftKVP = leftList.get(i);
159             KeyValuePair rightKVP = rightList.get(i);
160 
161             boolean result = Objects.equals(leftKVP.key, rightKVP.key) && Objects.equals(leftKVP.value, rightKVP.value);
162 
163             if (!result) {
164                 System.out.println("mismatch oin kvp " + leftKVP + " and " + rightKVP);
165                 return false;
166             }
167         }
168         return true;
169 
170     }
171 
172     @Test
173     void withMarkers() throws JsonProcessingException {
174         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, null);
175         event.addMarker(markerA);
176         event.addMarker(markerB);
177 
178         byte[] resultBytes = jsonEncoder.encode(event);
179         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
180         //System.out.println(resultString);
181 
182         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
183         compareEvents(event, resultEvent);
184     }
185 
186     @Test
187     void withArguments() throws JsonProcessingException {
188         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, new Object[] { "arg1", "arg2" });
189 
190         byte[] resultBytes = jsonEncoder.encode(event);
191         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
192         //System.out.println(resultString);
193 
194         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
195         compareEvents(event, resultEvent);
196     }
197 
198     @Test
199     void withKeyValuePairs() throws JsonProcessingException {
200         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello kvp", null,
201                 new Object[] { "arg1", "arg2" });
202         event.addKeyValuePair(new KeyValuePair("k1", "v1"));
203         event.addKeyValuePair(new KeyValuePair("k2", "v2"));
204 
205         byte[] resultBytes = jsonEncoder.encode(event);
206         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
207         //System.out.println(resultString);
208         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
209         compareEvents(event, resultEvent);
210     }
211 
212     @Test
213     void withFormattedMessage() throws JsonProcessingException {
214         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello {} {}", null,
215                 new Object[] { "arg1", "arg2" });
216         jsonEncoder.setWithFormattedMessage(true);
217 
218         byte[] resultBytes = jsonEncoder.encode(event);
219         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
220         //System.out.println(resultString);
221 
222         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
223         compareEvents(event, resultEvent);
224     }
225 
226     @Test
227     void withMDC() throws JsonProcessingException {
228         Map<String, String> map = new HashMap<>();
229         map.put("key", "value");
230         map.put("a", "b");
231 
232         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello kvp", null,
233                 new Object[] { "arg1", "arg2" });
234         Map<String, String> mdcMap = new HashMap<>();
235         mdcMap.put("mdcK1", "v1");
236         mdcMap.put("mdcK2", "v2");
237 
238         event.setMDCPropertyMap(mdcMap);
239 
240         byte[] resultBytes = jsonEncoder.encode(event);
241         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
242         //System.out.println(resultString);
243         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
244         compareEvents(event, resultEvent);
245     }
246 
247     @Test
248     void withThrowable() throws JsonProcessingException {
249         Throwable t = new RuntimeException("test");
250         LoggingEvent event = new LoggingEvent("in withThrowable test", logger, Level.WARN, "hello kvp", t, null);
251 
252         byte[] resultBytes = jsonEncoder.encode(event);
253         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
254         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
255         compareEvents(event, resultEvent);
256     }
257 
258     @Test
259     void withThrowableDisabled() throws JsonProcessingException {
260         Throwable t = new RuntimeException("withThrowableDisabled");
261         LoggingEvent event = new LoggingEvent("in withThrowable test", logger, Level.WARN, "hello kvp", t, null);
262         jsonEncoder.setWithThrowable(false);
263         byte[] resultBytes = jsonEncoder.encode(event);
264         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
265         //System.out.println(resultString);
266         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
267 
268         LoggingEvent eventWithNoThrowable = new LoggingEvent("in withThrowable test", logger, Level.WARN, "hello kvp", null, null);
269         eventWithNoThrowable.setTimeStamp(event.getTimeStamp());
270 
271         compareEvents(eventWithNoThrowable, resultEvent);
272     }
273 
274 
275     @Test
276     void withThrowableHavingCause() throws JsonProcessingException {
277         Throwable cause = new IllegalStateException("test cause");
278 
279         Throwable t = new RuntimeException("test", cause);
280 
281         LoggingEvent event = new LoggingEvent("in withThrowableHavingCause test", logger, Level.WARN, "hello kvp", t,
282                 null);
283 
284         byte[] resultBytes = jsonEncoder.encode(event);
285         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
286         //System.out.println(resultString);
287         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
288         compareEvents(event, resultEvent);
289     }
290 
291     @Test
292     void withThrowableHavingCyclicCause() throws JsonProcessingException {
293         Throwable cause = new IllegalStateException("test cause");
294 
295         Throwable t = new RuntimeException("test", cause);
296         cause.initCause(t);
297 
298         LoggingEvent event = new LoggingEvent("in withThrowableHavingCyclicCause test", logger, Level.WARN, "hello kvp",
299                 t, null);
300 
301         byte[] resultBytes = jsonEncoder.encode(event);
302         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
303         //System.out.println(resultString);
304         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
305         compareEvents(event, resultEvent);
306     }
307 
308     @Test
309     void withThrowableHavingSuppressed() throws JsonProcessingException {
310         Throwable suppressed = new IllegalStateException("test suppressed");
311 
312         Throwable t = new RuntimeException("test");
313         t.addSuppressed(suppressed);
314 
315         LoggingEvent event = new LoggingEvent("in withThrowableHavingCause test", logger, Level.WARN, "hello kvp", t,
316                 null);
317 
318         byte[] resultBytes = jsonEncoder.encode(event);
319         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
320         //System.out.println(resultString);
321         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
322         compareEvents(event, resultEvent);
323     }
324 
325     void configure(String file) throws JoranException {
326         JoranConfigurator jc = new JoranConfigurator();
327         jc.setContext(loggerContext);
328         loggerContext.putProperty("diff", "" + diff);
329         jc.doConfigure(file);
330     }
331 
332     @Test
333     void withJoran() throws JoranException, IOException {
334         String configFilePathStr = ClassicTestConstants.JORAN_INPUT_PREFIX + "json/jsonEncoder.xml";
335 
336         configure(configFilePathStr);
337         Logger logger = loggerContext.getLogger(this.getClass().getName());
338         logger.addAppender(listAppender);
339 
340         logger.debug("hello");
341         logbackMDCAdapter.put("a1", "v1" + diff);
342         logger.atInfo().addKeyValue("ik" + diff, "iv" + diff).addKeyValue("a", "b").log("bla bla \"x\" foobar");
343         logbackMDCAdapter.put("a2", "v2" + diff);
344         logger.atWarn().addMarker(markerA).setMessage("some warning message").log();
345         logbackMDCAdapter.remove("a2");
346         logger.atError().addKeyValue("ek" + diff, "v" + diff).setCause(new RuntimeException("an error"))
347                 .log("some error occurred");
348 
349         //StatusPrinter.print(loggerContext);
350 
351         Path outputFilePath = Path.of(ClassicTestConstants.OUTPUT_DIR_PREFIX + "json/test-" + diff + ".json");
352         List<String> lines = Files.readAllLines(outputFilePath);
353         int count = 4;
354         assertEquals(count, lines.size());
355 
356         for (int i = 0; i < count; i++) {
357             //System.out.println("i = " + i);
358             LoggingEvent withnessEvent = (LoggingEvent) listAppender.list.get(i);
359             JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(lines.get(i));
360             compareEvents(withnessEvent, resultEvent);
361         }
362     }
363 
364     @Test
365     void withJoranAndEnabledFormattedMessage() throws JoranException, IOException {
366         String configFilePathStr =
367                 ClassicTestConstants.JORAN_INPUT_PREFIX + "json/jsonEncoderAndEnabledFormattedMessage.xml";
368 
369         configure(configFilePathStr);
370         Logger logger = loggerContext.getLogger(this.getClass().getName());
371 
372         //StatusPrinter.print(loggerContext);
373         statusChecker.isWarningOrErrorFree(0);
374 
375         logger.atError().addKeyValue("ek1", "v1").addArgument("arg1").log("this is {}");
376 
377         Path outputFilePath = Path.of(ClassicTestConstants.OUTPUT_DIR_PREFIX + "json/test-" + diff + ".json");
378         List<String> lines = Files.readAllLines(outputFilePath);
379 
380         int count = 1;
381         assertEquals(count, lines.size());
382 
383         String withness = "{\"sequenceNumber\":0,\"level\":\"ERROR\",\"threadName\":\"main\","
384                 + "\"loggerName\":\"ch.qos.logback.classic.encoder.JsonEncoderTest\",\"mdc\": {},"
385                 + "\"kvpList\": [{\"ek1\":\"v1\"}],\"formattedMessage\":\"this is arg1\",\"throwable\":null}";
386 
387         assertEquals(withness, lines.get(0));
388     }
389 }