View Javadoc
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 
103         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
104         compareEvents(event, resultEvent);
105 
106     }
107 
108     @Test
109     void contextWithProperties() throws JsonProcessingException {
110         loggerContext.putProperty("k", "v");
111         loggerContext.putProperty("k" + diff, "v" + diff);
112 
113         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, null);
114 
115         byte[] resultBytes = jsonEncoder.encode(event);
116         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
117         // System.out.println(resultString);
118 
119         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
120         compareEvents(event, resultEvent);
121 
122     }
123 
124     private static void compareEvents(LoggingEvent event, JsonLoggingEvent resultEvent) {
125         assertEquals(event.getSequenceNumber(), resultEvent.getSequenceNumber());
126         assertEquals(event.getTimeStamp(), resultEvent.getTimeStamp());
127         assertEquals(event.getLevel(), resultEvent.getLevel());
128         assertEquals(event.getLoggerName(), resultEvent.getLoggerName());
129         assertEquals(event.getThreadName(), resultEvent.getThreadName());
130         assertEquals(event.getMarkerList(), resultEvent.getMarkerList());
131         assertEquals(event.getMDCPropertyMap(), resultEvent.getMDCPropertyMap());
132         assertTrue(compareKeyValuePairLists(event.getKeyValuePairs(), resultEvent.getKeyValuePairs()));
133 
134         assertEquals(event.getLoggerContextVO(), resultEvent.getLoggerContextVO());
135         assertTrue(ThrowableProxyComparator.areEqual(event.getThrowableProxy(), resultEvent.getThrowableProxy()));
136 
137         assertEquals(event.getMessage(), resultEvent.getMessage());
138         assertEquals(event.getFormattedMessage(), resultEvent.getFormattedMessage());
139 
140         assertTrue(Arrays.equals(event.getArgumentArray(), resultEvent.getArgumentArray()));
141 
142     }
143 
144     private static boolean compareKeyValuePairLists(List<KeyValuePair> leftList, List<KeyValuePair> rightList) {
145         if (leftList == rightList)
146             return true;
147 
148         if (leftList == null || rightList == null)
149             return false;
150 
151         int length = leftList.size();
152         if (rightList.size() != length) {
153             System.out.println("length discrepancy");
154             return false;
155         }
156 
157         //System.out.println("checking KeyValuePair lists");
158 
159         for (int i = 0; i < length; i++) {
160             KeyValuePair leftKVP = leftList.get(i);
161             KeyValuePair rightKVP = rightList.get(i);
162 
163             boolean result = Objects.equals(leftKVP.key, rightKVP.key) && Objects.equals(leftKVP.value, rightKVP.value);
164 
165             if (!result) {
166                 System.out.println("mismatch oin kvp " + leftKVP + " and " + rightKVP);
167                 return false;
168             }
169         }
170         return true;
171 
172     }
173 
174     @Test
175     void withMarkers() throws JsonProcessingException {
176         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, null);
177         event.addMarker(markerA);
178         event.addMarker(markerB);
179 
180         byte[] resultBytes = jsonEncoder.encode(event);
181         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
182         //System.out.println(resultString);
183 
184         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
185         compareEvents(event, resultEvent);
186     }
187 
188     @Test
189     void withArguments() throws JsonProcessingException {
190         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello", null, new Object[] { "arg1", "arg2" });
191 
192         byte[] resultBytes = jsonEncoder.encode(event);
193         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
194         //System.out.println(resultString);
195 
196         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
197         compareEvents(event, resultEvent);
198     }
199 
200     @Test
201     void withKeyValuePairs() throws JsonProcessingException {
202         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello kvp", null,
203                 new Object[] { "arg1", "arg2" });
204         event.addKeyValuePair(new KeyValuePair("k1", "v1"));
205         event.addKeyValuePair(new KeyValuePair("k2", "v2"));
206 
207         byte[] resultBytes = jsonEncoder.encode(event);
208         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
209         //System.out.println(resultString);
210         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
211         compareEvents(event, resultEvent);
212     }
213 
214     @Test
215     void withFormattedMessage() throws JsonProcessingException {
216         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello {} {}", null,
217                 new Object[] { "arg1", "arg2" });
218         jsonEncoder.setWithFormattedMessage(true);
219 
220         byte[] resultBytes = jsonEncoder.encode(event);
221         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
222         System.out.println(resultString);
223 
224         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
225         compareEvents(event, resultEvent);
226     }
227 
228     @Test
229     void withMDC() throws JsonProcessingException {
230         Map<String, String> map = new HashMap<>();
231         map.put("key", "value");
232         map.put("a", "b");
233 
234         LoggingEvent event = new LoggingEvent("x", logger, Level.WARN, "hello kvp", null,
235                 new Object[] { "arg1", "arg2" });
236         Map<String, String> mdcMap = new HashMap<>();
237         mdcMap.put("mdcK1", "v1");
238         mdcMap.put("mdcK2", "v2");
239 
240         event.setMDCPropertyMap(mdcMap);
241 
242         byte[] resultBytes = jsonEncoder.encode(event);
243         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
244         //System.out.println(resultString);
245         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
246         compareEvents(event, resultEvent);
247     }
248 
249     @Test
250     void withThrowable() throws JsonProcessingException {
251         Throwable t = new RuntimeException("test");
252         LoggingEvent event = new LoggingEvent("in withThrowable test", logger, Level.WARN, "hello kvp", t, null);
253 
254         byte[] resultBytes = jsonEncoder.encode(event);
255         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
256         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
257         compareEvents(event, resultEvent);
258     }
259 
260     @Test
261     void withThrowableHavingCause() throws JsonProcessingException {
262         Throwable cause = new IllegalStateException("test cause");
263 
264         Throwable t = new RuntimeException("test", cause);
265 
266         LoggingEvent event = new LoggingEvent("in withThrowableHavingCause test", logger, Level.WARN, "hello kvp", t,
267                 null);
268 
269         byte[] resultBytes = jsonEncoder.encode(event);
270         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
271         //System.out.println(resultString);
272         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
273         compareEvents(event, resultEvent);
274     }
275 
276     @Test
277     void withThrowableHavingCyclicCause() throws JsonProcessingException {
278         Throwable cause = new IllegalStateException("test cause");
279 
280         Throwable t = new RuntimeException("test", cause);
281         cause.initCause(t);
282 
283         LoggingEvent event = new LoggingEvent("in withThrowableHavingCyclicCause test", logger, Level.WARN, "hello kvp",
284                 t, null);
285 
286         byte[] resultBytes = jsonEncoder.encode(event);
287         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
288         //System.out.println(resultString);
289         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
290         compareEvents(event, resultEvent);
291     }
292 
293     @Test
294     void withThrowableHavingSuppressed() throws JsonProcessingException {
295         Throwable suppressed = new IllegalStateException("test suppressed");
296 
297         Throwable t = new RuntimeException("test");
298         t.addSuppressed(suppressed);
299 
300         LoggingEvent event = new LoggingEvent("in withThrowableHavingCause test", logger, Level.WARN, "hello kvp", t,
301                 null);
302 
303         byte[] resultBytes = jsonEncoder.encode(event);
304         String resultString = new String(resultBytes, StandardCharsets.UTF_8);
305         //System.out.println(resultString);
306         JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(resultString);
307         compareEvents(event, resultEvent);
308     }
309 
310     void configure(String file) throws JoranException {
311         JoranConfigurator jc = new JoranConfigurator();
312         jc.setContext(loggerContext);
313         loggerContext.putProperty("diff", "" + diff);
314         jc.doConfigure(file);
315     }
316 
317     @Test
318     void withJoran() throws JoranException, IOException {
319         String configFilePathStr = ClassicTestConstants.JORAN_INPUT_PREFIX + "json/jsonEncoder.xml";
320 
321         configure(configFilePathStr);
322         Logger logger = loggerContext.getLogger(this.getClass().getName());
323         logger.addAppender(listAppender);
324 
325         logger.debug("hello");
326         logbackMDCAdapter.put("a1", "v1" + diff);
327         logger.atInfo().addKeyValue("ik" + diff, "iv" + diff).addKeyValue("a", "b").log("bla bla \"x\" foobar");
328         logbackMDCAdapter.put("a2", "v2" + diff);
329         logger.atWarn().addMarker(markerA).setMessage("some warning message").log();
330         logbackMDCAdapter.remove("a2");
331         logger.atError().addKeyValue("ek" + diff, "v" + diff).setCause(new RuntimeException("an error"))
332                 .log("some error occurred");
333 
334         //StatusPrinter.print(loggerContext);
335 
336         Path outputFilePath = Path.of(ClassicTestConstants.OUTPUT_DIR_PREFIX + "json/test-" + diff + ".json");
337         List<String> lines = Files.readAllLines(outputFilePath);
338         int count = 4;
339         assertEquals(count, lines.size());
340 
341         for (int i = 0; i < count; i++) {
342             //System.out.println("i = " + i);
343             LoggingEvent withnessEvent = (LoggingEvent) listAppender.list.get(i);
344             JsonLoggingEvent resultEvent = stringToLoggingEventMapper.mapStringToLoggingEvent(lines.get(i));
345             compareEvents(withnessEvent, resultEvent);
346         }
347     }
348 
349     @Test
350     void withJoranAndEnabledFormattedMessage() throws JoranException, IOException {
351         String configFilePathStr =
352                 ClassicTestConstants.JORAN_INPUT_PREFIX + "json/jsonEncoderAndEnabledFormattedMessage.xml";
353 
354         configure(configFilePathStr);
355         Logger logger = loggerContext.getLogger(this.getClass().getName());
356 
357         //StatusPrinter.print(loggerContext);
358         statusChecker.isWarningOrErrorFree(0);
359 
360         logger.atError().addKeyValue("ek1", "v1").addArgument("arg1").log("this is {}");
361 
362         Path outputFilePath = Path.of(ClassicTestConstants.OUTPUT_DIR_PREFIX + "json/test-" + diff + ".json");
363         List<String> lines = Files.readAllLines(outputFilePath);
364 
365         int count = 1;
366         assertEquals(count, lines.size());
367 
368         String withness = "{\"sequenceNumber\":0,\"level\":\"ERROR\",\"threadName\":\"main\","
369                 + "\"loggerName\":\"ch.qos.logback.classic.encoder.JsonEncoderTest\",\"mdc\": {},"
370                 + "\"kvpList\": [{\"ek1\":\"v1\"}],\"formattedMessage\":\"this is arg1\",\"throwable\":null}";
371 
372         assertEquals(withness, lines.get(0));
373     }
374 }