PatternLayoutTest.java
/*
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2026, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v2.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.classic;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.pattern.ConverterTest;
import ch.qos.logback.classic.pattern.ExceptionalConverter2;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.pattern.SampleConverter;
import ch.qos.logback.classic.util.LogbackMDCAdapter;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.pattern.PatternLayoutBase;
import ch.qos.logback.core.spi.ScanException;
import ch.qos.logback.core.testUtil.RandomUtil;
import ch.qos.logback.core.testUtil.StringListAppender;
import ch.qos.logback.core.util.OptionHelper;
import ch.qos.logback.core.util.StatusPrinter;
import ch.qos.logback.core.status.testUtil.StatusChecker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.MDC;
import static ch.qos.logback.classic.ClassicTestConstants.ISO_REGEX;
import static ch.qos.logback.classic.ClassicTestConstants.MAIN_REGEX;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Instant;
import ch.qos.logback.core.pattern.parser.test.AbstractPatternLayoutBaseTest;
public class PatternLayoutTest extends AbstractPatternLayoutBaseTest<ILoggingEvent> {
private PatternLayout pl = new PatternLayout();
private LoggerContext loggerContext = new LoggerContext();
LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();
Logger logger = loggerContext.getLogger(ConverterTest.class);
Logger root = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
int diff = RandomUtil.getPositiveInt();
String aMessage = "Some message";
Exception ex = new Exception("Bogus exception");
@BeforeEach
public void setUp() {
loggerContext.setMDCAdapter(logbackMDCAdapter);
pl.setContext(loggerContext);
//le = makeLoggingEvent(aMessage, ex);
}
/**
* Circumvent JMPS issue: java.lang.NoClassDefFoundError: ch/qos/logback/core/pattern/ExceptionalConverter
* Is logback-clasic not open to logback-core?
* @return
*/
protected String getExceptionalConverterClassName() {
return ExceptionalConverter2.class.getName();
}
LoggingEvent makeLoggingEvent(String msg, Exception ex) {
return new LoggingEvent(ch.qos.logback.core.pattern.FormattingConverter.class.getName(), logger, Level.INFO,
msg, ex, null);
}
public ILoggingEvent getEventObject() {
return makeLoggingEvent("Some message", null);
}
public PatternLayoutBase<ILoggingEvent> getPatternLayoutBase() {
return new PatternLayout();
}
@Test
public void testOK() {
pl.setPattern("%d %le [%t] %lo{30} - %m%n");
pl.start();
String val = pl.doLayout(getEventObject());
// 2006-02-01 22:38:06,212 INFO [main] c.q.l.pattern.ConverterTest - Some
// message
// 2010-12-29 19:04:26,137 INFO [pool-1-thread-47] c.q.l.c.pattern.ConverterTest
// - Some message
String regex = ISO_REGEX + " INFO " + MAIN_REGEX + " c.q.l.c.pattern.ConverterTest - Some message\\s*";
assertTrue( val.matches(regex), "val=" + val);
}
@Test
public void testNoExeptionHandler() {
pl.setPattern("%m%n");
pl.start();
String val = pl.doLayout(makeLoggingEvent(aMessage, ex));
assertTrue(val.contains("java.lang.Exception: Bogus exception"));
}
@Test
public void testCompositePattern() {
pl.setPattern("%-56(%d %lo{20}) - %m%n");
pl.start();
String val = pl.doLayout(getEventObject());
// 2008-03-18 21:55:54,250 c.q.l.c.pattern.ConverterTest - Some message
String regex = ISO_REGEX + " c.q.l.c.p.ConverterTest - Some message\\s*";
assertTrue(val.matches(regex));
}
@Test
public void contextProperty() {
pl.setPattern("%property{a}");
pl.start();
loggerContext.putProperty("a", "b");
String val = pl.doLayout(getEventObject());
assertEquals("b", val);
}
@Test
public void testNopExeptionHandler() {
pl.setPattern("%nopex %m%n");
pl.start();
String val = pl.doLayout(makeLoggingEvent(aMessage, ex));
assertTrue(!val.contains("java.lang.Exception: Bogus exception"));
}
@Test
public void testWithParenthesis() {
pl.setPattern("\\(%msg:%msg\\) %msg");
pl.start();
LoggingEvent le = makeLoggingEvent(aMessage, null);
String val = pl.doLayout(le);
assertEquals("(Some message:Some message) Some message", val);
}
@Test
public void testWithLettersComingFromLog4j() {
// Letters: p = level and c = logger
pl.setPattern("%d %p [%t] %c{30} - %m%n");
pl.start();
String val = pl.doLayout(getEventObject());
// 2006-02-01 22:38:06,212 INFO [main] c.q.l.pattern.ConverterTest - Some
// message
String regex = ClassicTestConstants.ISO_REGEX + " INFO " + MAIN_REGEX
+ " c.q.l.c.pattern.ConverterTest - Some message\\s*";
assertTrue(val.matches(regex));
}
@Test
public void mdcWithDefaultValue() throws ScanException {
String pattern = "%msg %mdc{foo1} %mdc{bar:-[null]}";
pl.setPattern(OptionHelper.substVars(pattern, loggerContext));
pl.start();
String key = "foo1";
logbackMDCAdapter.put(key, key);
try {
String val = pl.doLayout(getEventObject());
assertEquals("Some message foo1 [null]", val);
} finally {
logbackMDCAdapter.remove(key);
}
}
@Test
public void contextNameTest() {
pl.setPattern("%contextName");
loggerContext.setName("aValue");
pl.start();
String val = pl.doLayout(getEventObject());
assertEquals("aValue", val);
}
@Test
public void cnTest() {
pl.setPattern("%cn");
loggerContext.setName("aValue");
pl.start();
String val = pl.doLayout(getEventObject());
assertEquals("aValue", val);
}
@Test
public void micros() {
verifyMicros(122_891_479, "2011-12-03 10:15:30.122 891 Some message");
verifyMicros(122_091_479, "2011-12-03 10:15:30.122 091 Some message");
verifyMicros(122_001_479, "2011-12-03 10:15:30.122 001 Some message");
}
void verifyMicros(int nanos, String expected) {
Instant instant = Instant.parse("2011-12-03T10:15:30Z");
instant = instant.plusNanos(nanos);
LoggingEvent le = makeLoggingEvent(aMessage, null);
le.setInstant(instant);
pl.setPattern("%date{yyyy-MM-dd HH:mm:ss.SSS, UTC} %micros %message%nopex");
pl.start();
String val = pl.doLayout(le);
assertEquals(expected, val);
}
@Test
public void epoch() {
verifyEpoch(123, false, "2026-01-15 10:15:30.123 1768472130123 Some message");
verifyEpoch(456, false, "2026-01-15 10:15:30.456 1768472130456 Some message");
verifyEpoch(123, true, "2026-01-15 10:15:30.123 1768472130 Some message");
verifyEpoch(456, true, "2026-01-15 10:15:30.456 1768472130 Some message");
}
void verifyEpoch(int millis, boolean secondsNotMillis, String expected) {
Instant instant = Instant.parse("2026-01-15T10:15:30Z");
instant = instant.plusMillis(millis);
LoggingEvent le = makeLoggingEvent(aMessage, null);
le.setInstant(instant);
String option = secondsNotMillis ? "{seconds}" : "";
pl.setPattern("%date{yyyy-MM-dd HH:mm:ss.SSS, UTC} %epoch"+option+" %message%nopex");
pl.start();
String val = pl.doLayout(le);
assertEquals(expected, val);
}
@Override
public Context getContext() {
return loggerContext;
}
void configure(String file) throws JoranException {
JoranConfigurator jc = new JoranConfigurator();
jc.setContext(loggerContext);
jc.doConfigure(file);
}
@Test
public void testConversionRuleSupportInPatternLayout() throws JoranException {
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/patternLayout0.xml");
root.getAppender("LIST");
String msg = "Simon says";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
}
@Test
public void testConversionRuleAtEnd() throws JoranException {
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleAtEnd.xml");
root.getAppender("LIST");
String msg = "testConversionRuleAtEnd";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
}
@Test
public void testConversionRuleInIncluded() throws JoranException {
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleTop0.xml");
StatusPrinter.print(loggerContext);
root.getAppender("LIST");
String msg = "testConversionRuleInIncluded";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
}
@Test
public void testConversionRuleWithBothAttributes() throws JoranException {
// Test that both converterClass and class attributes can be specified together
// The 'class' attribute should take precedence without deprecation warning
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleBothAttributes.xml");
root.getAppender("LIST");
String msg = "testConversionRuleWithBothAttributes";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
// Verify that the conversion rule works (uses 'class' attribute)
assertEquals(SampleConverter.SAMPLE_STR + " - " + msg, sla.strList.get(0));
}
@Test
public void testConversionRuleWithDifferentAttributes() throws JoranException {
// Test that when both converterClass and class attributes are specified with different values, an error is reported
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "conversionRule/conversionRuleDifferentAttributes.xml");
StatusPrinter.print(loggerContext);
StatusChecker checker = new StatusChecker(loggerContext.getStatusManager());
checker.assertContainsMatch("Both \\[converterClass\\] and \\[class\\] attributes are specified but have different values.");
}
@Test
public void smokeReplace() {
pl.setPattern("%replace(a1234b){'\\d{4}', 'XXXX'}");
pl.start();
String val = pl.doLayout(getEventObject());
assertEquals("aXXXXb", val);
}
@Test
public void replaceNewline() throws ScanException {
String pattern = "%replace(A\nB){'\n', '\n\t'}";
String substPattern = OptionHelper.substVars(pattern, null, loggerContext);
assertEquals(pattern, substPattern);
pl.setPattern(substPattern);
pl.start();
//StatusPrinter.print(lc);
String val = pl.doLayout(makeLoggingEvent("", null));
assertEquals("A\n\tB", val);
}
@Test
public void replaceWithJoran() throws JoranException {
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "pattern/replace0.xml");
//StatusPrinter.print(lc);
root.getAppender("LIST");
String msg = "And the number is 4111111111110000, expiring on 12/2010";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
assertEquals("And the number is XXXX, expiring on 12/2010", sla.strList.get(0));
}
@Test
public void replaceWithJoran_NEWLINE() throws JoranException {
loggerContext.putProperty("TAB", "\t");
configure(ClassicTestConstants.JORAN_INPUT_PREFIX + "pattern/replaceNewline.xml");
//StatusPrinter.print(lc);
root.getAppender("LIST");
String msg = "A\nC";
logger.debug(msg);
StringListAppender<ILoggingEvent> sla = (StringListAppender<ILoggingEvent>) root.getAppender("LIST");
assertNotNull(sla);
assertEquals(1, sla.strList.size());
assertEquals("A\n\tC", sla.strList.get(0));
}
@Test
public void prefixConverterSmoke() {
String pattern = "%prefix(%logger) %message";
pl.setPattern(pattern);
pl.start();
String val = pl.doLayout(makeLoggingEvent("hello", null));
assertEquals("logger=" + logger.getName() + " hello", val);
}
@Test
public void prefixConverterWithMDC() {
String mdcKey = "boo";
String mdcVal = "moo";
String pattern = "%prefix(%level %logger %X{" + mdcKey + "}) %message";
pl.setPattern(pattern);
pl.start();
logbackMDCAdapter.put(mdcKey, mdcVal);
try {
String val = pl.doLayout(makeLoggingEvent("hello", null));
assertEquals("level=" + "INFO logger=" + logger.getName() + " " + mdcKey + "=" + mdcVal + " hello", val);
} finally {
MDC.remove(mdcKey);
}
}
@Test
public void prefixConverterWithProperty() {
try {
String propertyKey = "px1953";
String propertyVal = "pxVal";
System.setProperty(propertyKey, propertyVal);
String pattern = "%prefix(%logger %property{" + propertyKey + "}) %message";
pl.setPattern(pattern);
pl.start();
String val = pl.doLayout(makeLoggingEvent("hello", null));
assertEquals("logger=" + logger.getName() + " " + propertyKey + "=" + propertyVal + " hello", val);
} finally {
System.clearProperty("px");
}
}
}