Log4J: How do I redirect an OutputStream or Writer to logger's writer(s)?

Viewed 47238

I have a method which runs asynchronously after start, using OutputStream or Writer as parameter.

It acts as a recording adapter for an OutputStream or Writer (it's a third party API I can't change).

How could I pass Log4J's internal OutputStream or Writer to that method?
...because Log4J swallows System.out and System.err, I was using before.

7 Answers

You can use Log4j IOStreams

The IOStreams component is a Log4j API extension that provides numerous classes from java.io that can either write to a Logger while writing to another OutputStream or Writer, or the contents read by an InputStream or Reader can be wiretapped by a Logger.

You can create an OutputStream in this way:

OutputStream outputStream = IoBuilder
            .forLogger(logger)
            .buildOutputStream();

Below is an example with Appium, starting it programmatically and controlling its log with log4j.

    final Logger logger = LogManager.getLogger(getClass());

    cap = new DesiredCapabilities();
    cap.setCapability("noReset", "false");

    //Build the Appium service
    builder = new AppiumServiceBuilder();
    builder.withIPAddress("127.0.0.1");
    builder.usingPort(4723);
    builder.withCapabilities(cap);
    builder.withArgument(GeneralServerFlag.SESSION_OVERRIDE);
    builder.withArgument(GeneralServerFlag.LOG_LEVEL,"debug");

    //Start the server with the builder
    service = AppiumDriverLocalService.buildService(builder);

    OutputStream outputStream = IoBuilder
            .forLogger(logger)
            .buildOutputStream();
    service.addOutPutStream(outputStream);

    service.start();

Hope this helps!!!

Building on Arthur Neves answer, I transferred this for Slf4J. I also improved this a bit using StringBuffer and directly casting byte to char:

import java.io.OutputStream;

import org.slf4j.Logger;

public class LogOutputStream extends OutputStream {
    private final Logger logger;

    /** The internal memory for the written bytes. */
    private StringBuffer mem;

    public LogOutputStream( final Logger logger ) {
        this.logger = logger;
        mem = new StringBuffer();
    }

    @Override
    public void write( final int b ) {
        if ( (char) b == '\n' ) {
            flush();
            return;
        }
        mem = mem.append( (char) b );
    }

    @Override
    public void flush() {
        logger.info( mem.toString() );
        mem = new StringBuffer();
    }
}

Looking through the answers here, it seems that none of them account clearly for decoding of bytes into Strings (extends CharSequence). bytes are chars are not equivalent (see OutputStream vs Writer in Java). A simple non-latin character e.g. 羼 may be represented as a series of bytes: E7 BE BC (UTF-8 sequence of 羼).

Reasons why others don't account for specific encoding:

  • (char) b will transform non-latin characters without interpreting UTF-8, so 羼 becomes ç¾¼, or "Róbert" becomes "Róbert" (oh, home many times I've seen this). You might be more familiar with this beauty:  (UTF-8 BOM)
  • new String(bytes) creates a String "using the platform's default charset", which is dependent on where you run the code, so you will probably get different behavior on server and local machines. This is one better than (char)b, because you can at least specify the encoding.
  • log4j-iostreams's IoBuilder by default also uses the platform's default charset, but is configurable. Also this is not a generic solution, only works if you use SLF4J over Log4j 2. Although that was OP's question .

(Forgive my Kotlin, you can do exactly the same in Java with different syntax.)

private fun loggerStream(outputLine: (line: String) -> Unit): PipedOutputStream {
    val output = PipedOutputStream()
    val input = PipedInputStream(output).bufferedReader()
    thread(isDaemon = true) {
        input.lineSequence().forEach(outputLine)
    }
    return output
}

With this solution:

  • The transition is performant (buffered)
  • The coding is specified consistently (bufferedReader has a default parameter: charset = Charsets.UTF_8, can be changed if necessary)
  • The overhead of encoding is in a background thread, although a bit strange, but this is how pipes work.
  • The code is simple/high level
    (no byte arrays, indices, copies, String constructors, etc.)

Note: I'm using this to redirect Selenium ChromeDriver's output (defaults to stderr) into SLF4J over Log4J 2:

    val service = ChromeDriverService.createServiceWithConfig(options).apply {
        sendOutputTo(loggerStream(LoggerFactory.getLogger(ChromeDriver::class.java)::info))
    }
    val driver = ChromeDriver(service, options)

After reading the https://stackoverflow.com/a/6996147/1773490 answer, I started having a look at existing OutputStream implementations and I stumbled upon org.apache.commons.exec.LogOutputStream.

You just need to include it to your project, like this with Maven :

// ...
<dependencies>
    // ...
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-exec</artifactId>
        <version>1.3</version>
    </dependency>
    // ...
</dependencies>
// ...

And this is an implementation example I use on a project using Selenium with the Chrome Driver and Google's Flogger :

package com.vk.logging;

import com.google.common.flogger.FluentLogger;
import org.apache.commons.exec.LogOutputStream;

import java.util.List;
import java.util.logging.Level;

public class FloggerOutputStream extends LogOutputStream {
    
    // TODO This can be replaced with your favorite logger
    private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass();

    private static final List<Level> KNOWN_LEVELS = List.of(
            Level.OFF,
            Level.SEVERE,
            Level.WARNING,
            Level.INFO,
            Level.CONFIG,
            Level.FINE,
            Level.FINER,
            Level.FINEST,
            Level.ALL
    );

    @Override
    protected void processLine(String line, int logLevel) {
        // TODO This can be replaced with your favorite logger
        LOGGER.at(findClosestLevel(logLevel))
              .log(line);
    }

    private Level findClosestLevel(int logLevel) {
        try {
            return Level.parse(String.valueOf(logLevel));
        } catch (IllegalArgumentException e) {
            // Find the closest level
            for (Level knownLevel : KNOWN_LEVELS) {
                if (knownLevel.intValue() < logLevel) {
                    return knownLevel;
                }
            }
        }

        throw new IllegalArgumentException(
                "Log level " + logLevel + " cannot be mapped to a known log level");
    }
}

Normally there shouldn't be any weird logLevel but that corner case is managed by finding the closest known log level.

Hope this helps

Related