0

I am trying to use tinylog but can't figure out how to redirect System.out and System.err to the logger. With log4j2, I did it like this (from java code):

System.setErr(IoBuilder.forLogger(LogManager.getRootLogger()).setLevel(Level.ERROR).buildPrintStream());
System.setOut(IoBuilder.forLogger(LogManager.getRootLogger()).setLevel(Level.INFO).buildPrintStream());

I assume I should use same mechanism (System.setOut and setErr), however I'm not sure what is the right way to do it. I've read the online documentation but could not find any answer to my question (which is strange, since this is such a basic functionality that any logger must support in my opinion).

Any ideas?

Progman
  • 16,827
  • 6
  • 33
  • 48
Betalord
  • 109
  • 7

1 Answers1

1

I solved the problem myself, not sure if my solution is the best, but anyway. I introduced these two methods:

    /** Redirects all writing to System.out stream to the logger (logging messages at INFO level). */
    public static void redirectSystemOut() {
        System.setOut(new PrintStream(new LineReadingOutputStream(Logger::info), true));
    }
    
    /** Redirects all writing to System.err stream to the logger (logging messages at ERROR level). */
    public static void redirectSystemErr() {
        System.setErr(new PrintStream(new LineReadingOutputStream(Logger::error), true));
    }

As far as LineReadingOutputStream class goes, I found it here: Java OutputStream reading lines of strings

One final element of this solution is creation of custom console writer, since the default one outputs everything to System.out / System.err, and that would create an infinite loop. The trick is to wrap the two streams and send strings to them. This is the code of my class that I call "IsolatedConsoleWriter" and has to be registered via META-INF (as described in the tinylog docs):

package com.betalord.sgx.core;

import org.tinylog.Level;
import org.tinylog.core.ConfigurationParser;
import org.tinylog.core.LogEntry;
import org.tinylog.core.LogEntryValue;
import org.tinylog.provider.InternalLogger;
import org.tinylog.writers.AbstractFormatPatternWriter;

import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * Console writer based on org.tinylog.writers.ConsoleWriter class, with one difference: this one
 * doesn't write logs to System.out/err but rather to our own streams, that output to the same system
 * streams but wrapped. This enables us to redirect System.out/err to the logger (if we were to use
 * normal console writer, we would create an infinite loop upon writing to System.out(err).
 *
 * Comes as a solution to this problem:
 * <a href="https://stackoverflow.com/questions/75776644/how-to-redirect-system-out-and-system-err-to-tinylog-logger">https://stackoverflow.com/questions/75776644/how-to-redirect-system-out-and-system-err-to-tinylog-logger</a>
 *
 * @author Betalord
 */
public class IsolatedConsoleWriter extends AbstractFormatPatternWriter {
    private final Level errorLevel;
    private final PrintStream outStream, errStream;
    
    public IsolatedConsoleWriter() {
        this(Collections.<String, String>emptyMap());
    }
    
    public IsolatedConsoleWriter(final Map<String, String> properties) {
        super(properties);
        
        // Set the default level for stderr logging
        Level levelStream = Level.WARN;
        
        // Check stream property
        String stream = getStringValue("stream");
        if (stream != null) {
            // Check whether we have the err@LEVEL syntax
            String[] streams = stream.split("@", 2);
            if (streams.length == 2) {
                levelStream = ConfigurationParser.parse(streams[1], levelStream);
                if (!streams[0].equals("err")) {
                    InternalLogger.log(Level.ERROR, "Stream with level must be \"err\", \"" + streams[0] + "\" is an invalid name");
                }
                stream = null;
            }
        }
        
        if (stream == null) {
            errorLevel = levelStream;
        } else if ("err".equalsIgnoreCase(stream)) {
            errorLevel = Level.TRACE;
        } else if ("out".equalsIgnoreCase(stream)) {
            errorLevel = Level.OFF;
        } else {
            InternalLogger.log(Level.ERROR, "Stream must be \"out\" or \"err\", \"" + stream + "\" is an invalid stream name");
            errorLevel = levelStream;
        }
        
        outStream = new PrintStream(new FileOutputStream(FileDescriptor.out), true);
        errStream = new PrintStream(new FileOutputStream(FileDescriptor.err), true);
    }
    
    @Override
    public Collection<LogEntryValue> getRequiredLogEntryValues() {
        Collection<LogEntryValue> logEntryValues = super.getRequiredLogEntryValues();
        logEntryValues.add(LogEntryValue.LEVEL);
        return logEntryValues;
    }
    
    @Override
    public void write(final LogEntry logEntry) {
        if (logEntry.getLevel().ordinal() < errorLevel.ordinal()) {
            outStream.print(render(logEntry));
        } else {
            errStream.print(render(logEntry));
        }
    }
    
    @Override
    public void flush() {
        outStream.flush();
        errStream.flush();
    }
    
    @Override
    public void close() {
        outStream.close();
        errStream.close();
    }
}

So this writer is exact copy of ConsoleWriter, it add only two fields: errStream and outStream. So, putting all of these elements together, I managed to achieve what I wanted - all System.out.println() and similar calls are rerouted to my logger, which formats all the data according to the defined rules (I actually use several writers - isolated console, as shown here, then the rolling file one, the "normal" file one, and also logcat, when I run my app under android).

If anyone comes up with a better solution, please let me know. However I believe that the functionality that achieves what I want should be part of the tinylog library itself (rather than using custom hacks like this one).

Betalord
  • 109
  • 7
  • Great solution! Feel free to submit your changes as pull request on https://github.com/tinylog-org/tinylog – Martin Mar 27 '23 at 16:00