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");
        }
    }

}