1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, 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 v2.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  package ch.qos.logback.classic;
15  
16  import ch.qos.logback.classic.joran.JoranConfigurator;
17  import ch.qos.logback.classic.pattern.ConverterTest;
18  import ch.qos.logback.classic.pattern.ExceptionalConverter2;
19  import ch.qos.logback.classic.spi.ILoggingEvent;
20  import ch.qos.logback.classic.spi.LoggingEvent;
21  import ch.qos.logback.classic.pattern.SampleConverter;
22  import ch.qos.logback.classic.util.LogbackMDCAdapter;
23  import ch.qos.logback.core.Context;
24  import ch.qos.logback.core.joran.spi.JoranException;
25  import ch.qos.logback.core.pattern.PatternLayoutBase;
26  import ch.qos.logback.core.pattern.parser.test.AbstractPatternLayoutBaseTest;
27  import ch.qos.logback.core.spi.ScanException;
28  import ch.qos.logback.core.testUtil.RandomUtil;
29  import ch.qos.logback.core.testUtil.StringListAppender;
30  import ch.qos.logback.core.util.OptionHelper;
31  import ch.qos.logback.core.util.StatusPrinter;
32  import org.junit.jupiter.api.BeforeEach;
33  import org.junit.jupiter.api.Test;
34  import org.slf4j.MDC;
35  
36  import static ch.qos.logback.classic.ClassicTestConstants.ISO_REGEX;
37  import static ch.qos.logback.classic.ClassicTestConstants.MAIN_REGEX;
38  import static org.junit.jupiter.api.Assertions.assertEquals;
39  import static org.junit.jupiter.api.Assertions.assertNotNull;
40  import static org.junit.jupiter.api.Assertions.assertTrue;
41  
42  import java.time.Instant;
43  
44  public class PatternLayoutTest extends AbstractPatternLayoutBaseTest<ILoggingEvent> {
45  
46      private PatternLayout pl = new PatternLayout();
47      private LoggerContext loggerContext = new LoggerContext();
48  
49      LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
50      Logger logger = loggerContext.getLogger(ConverterTest.class);
51      Logger root = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
52  
53      int diff = RandomUtil.getPositiveInt();
54  
55      String aMessage = "Some message";
56  
57      Exception ex = new Exception("Bogus exception");
58  
59  
60  
61      @BeforeEach
62      public void setUp() {
63          loggerContext.setMDCAdapter(logbackMDCAdapter);
64          pl.setContext(loggerContext);
65          //le = makeLoggingEvent(aMessage, ex);
66      }
67  
68      /**
69       * Circumvent JMPS issue: java.lang.NoClassDefFoundError: ch/qos/logback/core/pattern/ExceptionalConverter
70       * Is logback-clasic not open to logback-core?
71       * @return
72       */
73      protected String getExceptionalConverterClassName() {
74          return ExceptionalConverter2.class.getName();
75      }
76  
77      LoggingEvent makeLoggingEvent(String msg, Exception ex) {
78          return new LoggingEvent(ch.qos.logback.core.pattern.FormattingConverter.class.getName(), logger, Level.INFO,
79                  msg, ex, null);
80      }
81  
82      public ILoggingEvent getEventObject() {
83          return makeLoggingEvent("Some message", null);
84      }
85  
86      public PatternLayoutBase<ILoggingEvent> getPatternLayoutBase() {
87          return new PatternLayout();
88      }
89  
90      @Test
91      public void testOK() {
92          pl.setPattern("%d %le [%t] %lo{30} - %m%n");
93          pl.start();
94          String val = pl.doLayout(getEventObject());
95          // 2006-02-01 22:38:06,212 INFO [main] c.q.l.pattern.ConverterTest - Some
96          // message
97          // 2010-12-29 19:04:26,137 INFO [pool-1-thread-47] c.q.l.c.pattern.ConverterTest
98          // - Some message
99          String regex = ISO_REGEX + " INFO " + MAIN_REGEX + " c.q.l.c.pattern.ConverterTest - Some message\\s*";
100 
101         assertTrue( val.matches(regex), "val=" + val);
102     }
103 
104     @Test
105     public void testNoExeptionHandler() {
106         pl.setPattern("%m%n");
107         pl.start();
108         String val = pl.doLayout(makeLoggingEvent(aMessage, ex));
109         assertTrue(val.contains("java.lang.Exception: Bogus exception"));
110     }
111 
112     @Test
113     public void testCompositePattern() {
114         pl.setPattern("%-56(%d %lo{20}) - %m%n");
115         pl.start();
116         String val = pl.doLayout(getEventObject());
117         // 2008-03-18 21:55:54,250 c.q.l.c.pattern.ConverterTest - Some message
118         String regex = ISO_REGEX + " c.q.l.c.p.ConverterTest          - Some message\\s*";
119         assertTrue(val.matches(regex));
120     }
121 
122     @Test
123     public void contextProperty() {
124         pl.setPattern("%property{a}");
125         pl.start();
126         loggerContext.putProperty("a", "b");
127 
128         String val = pl.doLayout(getEventObject());
129         assertEquals("b", val);
130     }
131 
132     @Test
133     public void testNopExeptionHandler() {
134         pl.setPattern("%nopex %m%n");
135         pl.start();
136         String val = pl.doLayout(makeLoggingEvent(aMessage, ex));
137         assertTrue(!val.contains("java.lang.Exception: Bogus exception"));
138     }
139 
140     @Test
141     public void testWithParenthesis() {
142         pl.setPattern("\\(%msg:%msg\\) %msg");
143         pl.start();
144         LoggingEvent le = makeLoggingEvent(aMessage, null);
145         String val = pl.doLayout(le);
146         assertEquals("(Some message:Some message) Some message", val);
147     }
148 
149     @Test
150     public void testWithLettersComingFromLog4j() {
151         // Letters: p = level and c = logger
152         pl.setPattern("%d %p [%t] %c{30} - %m%n");
153         pl.start();
154         String val = pl.doLayout(getEventObject());
155         // 2006-02-01 22:38:06,212 INFO [main] c.q.l.pattern.ConverterTest - Some
156         // message
157         String regex = ClassicTestConstants.ISO_REGEX + " INFO " + MAIN_REGEX
158                 + " c.q.l.c.pattern.ConverterTest - Some message\\s*";
159         assertTrue(val.matches(regex));
160     }
161 
162     @Test
163     public void mdcWithDefaultValue() throws ScanException {
164         String pattern = "%msg %mdc{foo1} %mdc{bar:-[null]}";
165         pl.setPattern(OptionHelper.substVars(pattern, loggerContext));
166         pl.start();
167 
168         String key = "foo1";
169 
170         logbackMDCAdapter.put(key, key);
171         try {
172             String val = pl.doLayout(getEventObject());
173             assertEquals("Some message foo1 [null]", val);
174         } finally {
175             logbackMDCAdapter.remove(key);
176         }
177     }
178 
179     @Test
180     public void contextNameTest() {
181         pl.setPattern("%contextName");
182         loggerContext.setName("aValue");
183         pl.start();
184         String val = pl.doLayout(getEventObject());
185         assertEquals("aValue", val);
186     }
187 
188     @Test
189     public void cnTest() {
190         pl.setPattern("%cn");
191         loggerContext.setName("aValue");
192         pl.start();
193         String val = pl.doLayout(getEventObject());
194         assertEquals("aValue", val);
195     }
196 
197     @Test
198     public void micros() {
199         verifyMicros(122_891_479, "2011-12-03 10:15:30.122 891 Some message");
200         verifyMicros(122_091_479, "2011-12-03 10:15:30.122 091 Some message");
201         verifyMicros(122_001_479, "2011-12-03 10:15:30.122 001 Some message");
202     }
203 
204     void verifyMicros(int nanos, String expected) {
205         Instant instant = Instant.parse("2011-12-03T10:15:30Z");
206         instant = instant.plusNanos(nanos);
207         LoggingEvent le = makeLoggingEvent(aMessage, null);
208         le.setInstant(instant);
209 
210         pl.setPattern("%date{yyyy-MM-dd HH:mm:ss.SSS, UTC} %micros %message%nopex");
211         pl.start();
212 
213         String val = pl.doLayout(le);
214         assertEquals(expected, val);
215     }
216 
217     @Test
218     public void epoch() {
219         verifyEpoch(123, false, "2026-01-15 10:15:30.123 1768472130123 Some message");
220         verifyEpoch(456, false, "2026-01-15 10:15:30.456 1768472130456 Some message");
221         verifyEpoch(123, true, "2026-01-15 10:15:30.123 1768472130 Some message");
222         verifyEpoch(456, true, "2026-01-15 10:15:30.456 1768472130 Some message");
223     }
224 
225     void verifyEpoch(int millis, boolean secondsNotMillis, String expected) {
226         Instant instant = Instant.parse("2026-01-15T10:15:30Z");
227         instant = instant.plusMillis(millis);
228         LoggingEvent le = makeLoggingEvent(aMessage, null);
229         le.setInstant(instant);
230 
231         String option = secondsNotMillis ? "{seconds}" : "";
232         pl.setPattern("%date{yyyy-MM-dd HH:mm:ss.SSS, UTC} %epoch"+option+" %message%nopex");
233         pl.start();
234 
235         String val = pl.doLayout(le);
236         assertEquals(expected, val);
237     }
238 
239     @Override
240     public Context getContext() {
241         return loggerContext;
242     }
243 
244     void configure(String file) throws JoranException {
245         JoranConfigurator jc = new JoranConfigurator();
246         jc.setContext(loggerContext);
247         jc.doConfigure(file);
248     }
249 
250     @Test
251     public void testConversionRuleSupportInPatternLayout() throws JoranException {
252         configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/patternLayout0.xml");
253         root.getAppender("LIST");
254         String msg = "Simon says";
255         logger.debug(msg);
256         StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
257         assertNotNull(sla);
258         assertEquals(1, sla.strList.size());
259         assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
260     }
261 
262     @Test
263     public void testConversionRuleAtEnd() throws JoranException {
264         configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleAtEnd.xml");
265         root.getAppender("LIST");
266         String msg = "testConversionRuleAtEnd";
267         logger.debug(msg);
268         StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
269         assertNotNull(sla);
270         assertEquals(1, sla.strList.size());
271         assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
272     }
273 
274     @Test
275     public void testConversionRuleInIncluded() throws JoranException {
276         configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleTop0.xml");
277         StatusPrinter.print(loggerContext);
278         root.getAppender("LIST");
279         String msg = "testConversionRuleInIncluded";
280         logger.debug(msg);
281         StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
282         assertNotNull(sla);
283         assertEquals(1, sla.strList.size());
284         assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
285     }
286 
287     @Test
288     public void smokeReplace() {
289         pl.setPattern("%replace(a1234b){'\\d{4}', 'XXXX'}");
290         pl.start();
291         String val = pl.doLayout(getEventObject());
292         assertEquals("aXXXXb", val);
293     }
294 
295     @Test
296     public void replaceNewline() throws ScanException {
297         String pattern = "%replace(A\nB){'\n', '\n\t'}";
298         String substPattern = OptionHelper.substVars(pattern, null, loggerContext);
299         assertEquals(pattern, substPattern);
300         pl.setPattern(substPattern);
301         pl.start();
302         //StatusPrinter.print(lc);
303         String val = pl.doLayout(makeLoggingEvent("", null));
304         assertEquals("A\n\tB", val);
305     }
306 
307     @Test
308     public void replaceWithJoran() throws JoranException {
309         configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "pattern/replace0.xml");
310         //StatusPrinter.print(lc);
311         root.getAppender("LIST");
312         String msg = "And the number is 4111111111110000, expiring on 12/2010";
313         logger.debug(msg);
314         StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
315         assertNotNull(sla);
316         assertEquals(1, sla.strList.size());
317         assertEquals("And the number is XXXX, expiring on 12/2010", sla.strList.get(0));
318     }
319 
320     @Test
321     public void replaceWithJoran_NEWLINE() throws JoranException {
322         loggerContext.putProperty("TAB", "\t");
323         configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "pattern/replaceNewline.xml");
324         //StatusPrinter.print(lc);
325         root.getAppender("LIST");
326         String msg = "A\nC";
327         logger.debug(msg);
328         StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
329         assertNotNull(sla);
330         assertEquals(1, sla.strList.size());
331         assertEquals("A\n\tC", sla.strList.get(0));
332     }
333 
334     @Test
335     public void prefixConverterSmoke() {
336         String pattern = "%prefix(%logger) %message";
337         pl.setPattern(pattern);
338         pl.start();
339         String val = pl.doLayout(makeLoggingEvent("hello", null));
340         assertEquals("logger=" + logger.getName() + " hello", val);
341     }
342 
343     @Test
344     public void prefixConverterWithMDC() {
345         String mdcKey = "boo";
346         String mdcVal = "moo";
347 
348         String pattern = "%prefix(%level %logger %X{" + mdcKey + "}) %message";
349         pl.setPattern(pattern);
350         pl.start();
351         logbackMDCAdapter.put(mdcKey, mdcVal);
352         try {
353             String val = pl.doLayout(makeLoggingEvent("hello", null));
354 
355             assertEquals("level=" + "INFO logger=" + logger.getName() + " " + mdcKey + "=" + mdcVal + " hello", val);
356 
357         } finally {
358             MDC.remove(mdcKey);
359         }
360     }
361 
362     @Test
363     public void prefixConverterWithProperty() {
364 
365         try {
366             String propertyKey = "px1953";
367             String propertyVal = "pxVal";
368 
369             System.setProperty(propertyKey, propertyVal);
370 
371             String pattern = "%prefix(%logger %property{" + propertyKey + "}) %message";
372             pl.setPattern(pattern);
373             pl.start();
374 
375             String val = pl.doLayout(makeLoggingEvent("hello", null));
376 
377             assertEquals("logger=" + logger.getName() + " " + propertyKey + "=" + propertyVal + " hello", val);
378 
379         } finally {
380             System.clearProperty("px");
381         }
382     }
383 
384 }