SyslogAppenderTest.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.net;

import ch.qos.logback.classic.util.LogbackMDCAdapter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.ClassicTestConstants;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.net.mock.MockSyslogServer;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.net.SyslogConstants;
import ch.qos.logback.core.recovery.RecoveryCoordinator;
import ch.qos.logback.core.testUtil.RandomUtil;
import ch.qos.logback.core.util.StatusPrinter;

import java.nio.charset.Charset;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class SyslogAppenderTest {

    private static final String SYSLOG_PREFIX_REGEX = "<\\d{2}>\\w{3} [\\d ]\\d \\d{2}(:\\d{2}){2} [\\w.-]* ";

    LoggerContext lc = new LoggerContext();
    LogbackMDCAdapter logbackMDCAdapter = new LogbackMDCAdapter();

    SyslogAppender sa = new SyslogAppender();
    MockSyslogServer mockServer;
    String loggerName = this.getClass().getName();
    Logger logger = lc.getLogger(loggerName);

    @BeforeEach
    public void setUp() throws Exception {
        lc.setName("test");
        lc.setMDCAdapter(logbackMDCAdapter);
        sa.setContext(lc);
    }

    @AfterEach
    public void tearDown() throws Exception {
    }

    public void setMockServerAndConfigure(int expectedCount, String suffixPattern) throws InterruptedException {
        int port = RandomUtil.getRandomServerPort();

        mockServer = new MockSyslogServer(expectedCount, port);
        mockServer.start();
        // give MockSyslogServer head start

        Thread.sleep(100);

        sa.setSyslogHost("localhost");
        sa.setFacility("MAIL");
        sa.setPort(port);
        sa.setSuffixPattern(suffixPattern);
        sa.setStackTracePattern("[%thread] foo " + CoreConstants.TAB);
        sa.start();
        assertTrue(sa.isStarted());

        String loggerName = this.getClass().getName();
        Logger logger = lc.getLogger(loggerName);
        logger.addAppender(sa);

    }

    public void setMockServerAndConfigure(int expectedCount) throws InterruptedException {
        this.setMockServerAndConfigure(expectedCount, "[%thread] %logger %msg");
    }

    @Test
    public void basic() throws InterruptedException {

        setMockServerAndConfigure(1);
        String logMsg = "hello";
        logger.debug(logMsg);

        // wait max 2 seconds for mock server to finish. However, it should
        // much sooner than that.
        mockServer.join(8000);

        assertTrue(mockServer.isFinished());
        assertEquals(1, mockServer.getMessageList().size());
        String msg = new String(mockServer.getMessageList().get(0));

        String threadName = Thread.currentThread().getName();

        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">";
        assertTrue(msg.startsWith(expected));

        checkRegexMatch(msg, SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + loggerName + " " + logMsg);

    }

    @Test
    public void suffixPatternWithTag() throws InterruptedException {
        setMockServerAndConfigure(1, "test/something [%thread] %logger %msg");
        String logMsg = "hello";
        logger.debug(logMsg);

        // wait max 2 seconds for mock server to finish. However, it should
        // much sooner than that.
        mockServer.join(8000);

        assertTrue(mockServer.isFinished());
        assertEquals(1, mockServer.getMessageList().size());
        String msg = new String(mockServer.getMessageList().get(0));

        String threadName = Thread.currentThread().getName();

        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">";
        assertTrue(msg.startsWith(expected));

        checkRegexMatch(msg,
                SYSLOG_PREFIX_REGEX + "test/something \\[" + threadName + "\\] " + loggerName + " " + logMsg);

    }

    @Test
    public void tException() throws InterruptedException {
        setMockServerAndConfigure(21);

        String logMsg = "hello";
        String exMsg = "just testing";
        Exception ex = new Exception(exMsg);
        logger.debug(logMsg, ex);
        StatusPrinter.print(lc);

        // wait max 2 seconds for mock server to finish. However, it should
        // much sooner than that.
        mockServer.join(8000);
        assertTrue(mockServer.isFinished());

        // message + 20 lines of stacktrace
        assertEquals(21, mockServer.getMessageList().size());
        // int i = 0;
        // for (String line: mockServer.msgList) {
        // System.out.println(i++ + ": " + line);
        // }

        String msg = new String(mockServer.getMessageList().get(0));
        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">";
        assertTrue(msg.startsWith(expected));

        String threadName = Thread.currentThread().getName();
        String regex = SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + loggerName + " " + logMsg;
        checkRegexMatch(msg, regex);

        msg = new String(mockServer.getMessageList().get(1));
        assertTrue(msg.contains(ex.getClass().getName()));
        assertTrue(msg.contains(ex.getMessage()));

        msg = new String(mockServer.getMessageList().get(2));
        assertTrue(msg.startsWith(expected));
        regex = SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + "foo " + CoreConstants.TAB + "at ch\\.qos.*";
        checkRegexMatch(msg, regex);
    }

    private void checkRegexMatch(String s, String regex) {
        assertTrue(s.matches(regex), "The string [" + s + "] did not match regex [" + regex + "]");
    }

    @Test
    public void large() throws Exception {
        setMockServerAndConfigure(2);
        StringBuilder largeBuf = new StringBuilder();
        for (int i = 0; i < 2 * 1024 * 1024; i++) {
            largeBuf.append('a');
        }
        logger.debug(largeBuf.toString());

        String logMsg = "hello";
        logger.debug(logMsg);
        Thread.sleep(RecoveryCoordinator.BACKOFF_COEFFICIENT_MIN + 10);
        logger.debug(logMsg);

        mockServer.join(8000);
        assertTrue(mockServer.isFinished());

        // both messages received
        assertEquals(2, mockServer.getMessageList().size());

        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">";
        String threadName = Thread.currentThread().getName();

        // large message is truncated
        final int maxMessageSize = sa.getMaxMessageSize();
        String largeMsg = new String(mockServer.getMessageList().get(0));
        assertTrue(largeMsg.startsWith(expected));
        String largeRegex = SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + loggerName + " " + "a{"
                + (maxMessageSize - 2000) + "," + maxMessageSize + "}";
        checkRegexMatch(largeMsg, largeRegex);

        String msg = new String(mockServer.getMessageList().get(1));
        assertTrue(msg.startsWith(expected));
        String regex = SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + loggerName + " " + logMsg;
        checkRegexMatch(msg, regex);
    }

    @Test
    public void LBCLASSIC_50() throws JoranException {

        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();

        JoranConfigurator configurator = new JoranConfigurator();
        configurator.setContext(lc);
        lc.reset();
        configurator.doConfigure(ClassicTestConstants.JORAN_INPUT_PREFIX + "syslog_LBCLASSIC_50.xml");

        org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
        logger.info("hello");
    }

    @Test
    public void unknownHostShouldNotCauseStopToFail() {
        // See LOGBACK-960
        sa.setSyslogHost("unknown.host");
        sa.setFacility("MAIL");
        sa.start();
        sa.stop();
    }

    @Test
    public void nonAsciiMessageEncoding() throws Exception {
        // See LOGBACK-732
        setMockServerAndConfigure(1);

        // Use a string that can be encoded in a somewhat odd encoding (ISO-8859-4) to
        // minimize
        // the probability of the encoding test to work by accident
        String logMsg = "R\u0129ga"; // Riga spelled with the i having a tilda on top

        Charset ISO_8859_4 = Charset.forName("ISO-8859-4");
        sa.setCharset(ISO_8859_4);
        logger.debug(logMsg);

        // wait max 8 seconds for mock server to finish. However, it should
        // be done much sooner than that.
        mockServer.join(8000);

        assertTrue(mockServer.isFinished());
        assertEquals(1, mockServer.getMessageList().size());
        String msg = new String(mockServer.getMessageList().get(0), ISO_8859_4);
        String threadName = Thread.currentThread().getName();

        String expected = "<" + (SyslogConstants.LOG_MAIL + SyslogConstants.DEBUG_SEVERITY) + ">";
        assertTrue(msg.startsWith(expected));

        System.out.println(logMsg);
        checkRegexMatch(msg, SYSLOG_PREFIX_REGEX + "\\[" + threadName + "\\] " + loggerName + " " + logMsg);

    }
}