Log4J:如何将OutputStream或Writer重定向到日志记录器的writer?

39

我有一个方法,在启动后使用OutputStream或Writer作为参数异步运行。

它充当OutputStream或Writer的录制适配器(这是一个我无法更改的第三方API)。

我应该如何将Log4J的内部OutputStream或Writer传递给该方法?
...因为Log4J吞噬了我之前使用的System.out和System.err。


3
你是否可以实现自己的 OutputStream 并将 write(...) 委托给记录器? - Rekin
1
@Rekin 就是我下面所做的! - Arthur Neves
1
说Log4J吞噬System.err是什么意思? - Thorn G
1
Swallows output == 输出不会显示在任何地方 ;) - java.is.for.desktop.indeed
7个回答

17
我的建议是,为什么不自己编写OutputStream呢?我本来打算为你编写一个,但是我在网上找到了一个很好的例子,看看吧! LogOutputStream.java
/*
 * Jacareto Copyright (c) 2002-2005
 * Applied Computer Science Research Group, Darmstadt University of
 * Technology, Institute of Mathematics & Computer Science,
 * Ludwigsburg University of Education, and Computer Based
 * Learning Research Group, Aachen University. All rights reserved.
 *
 * Jacareto is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * Jacareto is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with Jacareto; if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

package jacareto.toolkit.log4j;


import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import java.io.OutputStream;

/**
 * This class logs all bytes written to it as output stream with a specified logging level.
 *
 * @author <a href="mailto:cspannagel@web.de">Christian Spannagel</a>
 * @version 1.0
 */
public class LogOutputStream extends OutputStream {
    /** The logger where to log the written bytes. */
    private Logger logger;

    /** The level. */
    private Level level;

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

    /**
     * Creates a new log output stream which logs bytes to the specified logger with the specified
     * level.
     *
     * @param logger the logger where to log the written bytes
     * @param level the level
     */
    public LogOutputStream (Logger logger, Level level) {
        setLogger (logger);
        setLevel (level);
        mem = "";
    }

    /**
     * Sets the logger where to log the bytes.
     *
     * @param logger the logger
     */
    public void setLogger (Logger logger) {
        this.logger = logger;
    }

    /**
     * Returns the logger.
     *
     * @return DOCUMENT ME!
     */
    public Logger getLogger () {
        return logger;
    }

    /**
     * Sets the logging level.
     *
     * @param level DOCUMENT ME!
     */
    public void setLevel (Level level) {
        this.level = level;
    }

    /**
     * Returns the logging level.
     *
     * @return DOCUMENT ME!
     */
    public Level getLevel () {
        return level;
    }

    /**
     * Writes a byte to the output stream. This method flushes automatically at the end of a line.
     *
     * @param b DOCUMENT ME!
     */
    public void write (int b) {
        byte[] bytes = new byte[1];
        bytes[0] = (byte) (b & 0xff);
        mem = mem + new String(bytes);

        if (mem.endsWith ("\n")) {
            mem = mem.substring (0, mem.length () - 1);
            flush ();
        }
    }

    /**
     * Flushes the output stream.
     */
    public void flush () {
        logger.log (level, mem);
        mem = "";
    }
}

1
很高兴找到这个。在我的使用场景中,给定的“LogOutputStream”存在以下问题,我需要解决:
  1. 它依赖于log4j而不是commons-logging,我进行了更改,并将“logger.info()”硬编码为commons-logging没有级别(就我所看到的而言)
  2. 它假设换行符为“\n”,我需要将其更改为“System.getProperty(“line.separator”)”
  3. 由于它的“flush()”没有检查“mem”是否为空,因此当包装的“Writer”在最后一个“println()”调用后调用“flush()”时,您会在输出上获得额外的空行。
- hokr
在你的类中,使用以下代码获取日志记录器:Logger logger = Logger.getLogger(new Object(){}.getClass().getEnclosingClass()); OutputStream stdout = new PrintStream(new LogOutputStream(logger, Level.INFO)); 然后将需要输出的内容写入stdout即可 ;) - saygley
虽然这是一个非常低效的实现。它不仅只逐字节写入,而且每行的写入都具有O(n²)的运行时和内存消耗,因为它使用每个字节创建了一个新字符串。如果使用StringBuffer而不是String,并在刷新后重用其内存,它已经可以得到改进。 - Jimmy T.

14

你可以使用Log4j IOStreams

IOStreams组件是Log4j API的扩展,它提供了许多来自java.io的类,这些类可以在写入另一个OutputStream或Writer时同时写入Logger,或者通过InputStream或Reader读取的内容可以被Logger监听。

你可以按照以下方式创建一个OutputStream:

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

以下是一个使用Appium的示例,通过编程方式启动它并使用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();

希望这能帮到你!!!

4

来源:http://sysgears.com/articles/how-to-redirect-stdout-and-stderr-writing-to-a-log4j-appender/

引用

Log4j默认不允许捕获标准输出和标准错误消息。但是,如果您正在使用第三方组件并且必须记录它们刷新到流中的消息,则可以使用一些技巧并实现支持日志记录的自定义输出流。

Jim Moore已经实现了这个功能(请参阅log4j源代码中的LoggingOutputStream)。唯一的问题是,JimMoore的LoggingOutputStream需要org.apache.log4j.Category和org.apache.log4j.Priority,这些方法现在部分已过时。

以下是修改后的LoggingOutputStream,避免使用过时的方法:

public class LoggingOutputStream extends OutputStream {

    /**
     * Default number of bytes in the buffer.
     */
    private static final int DEFAULT_BUFFER_LENGTH = 2048;

    /**
     * Indicates stream state.
     */
    private boolean hasBeenClosed = false;

    /**
     * Internal buffer where data is stored.
     */
    private byte[] buf;

    /**
     * The number of valid bytes in the buffer.
     */
    private int count;

    /**
     * Remembers the size of the buffer.
     */
    private int curBufLength;

    /**
     * The logger to write to.
     */
    private Logger log;

    /**
     * The log level.
     */
    private Level level;

    /**
     * Creates the Logging instance to flush to the given logger.
     *
     * @param log         the Logger to write to
     * @param level       the log level
     * @throws IllegalArgumentException in case if one of arguments
     *                                  is  null.
     */
    public LoggingOutputStream(final Logger log,
                               final Level level)
            throws IllegalArgumentException {
        if (log == null || level == null) {
            throw new IllegalArgumentException(
                    "Logger or log level must be not null");
        }
        this.log = log;
        this.level = level;
        curBufLength = DEFAULT_BUFFER_LENGTH;
        buf = new byte[curBufLength];
        count = 0;
    }

    /**
     * Writes the specified byte to this output stream.
     *
     * @param b the byte to write
     * @throws IOException if an I/O error occurs.
     */
    public void write(final int b) throws IOException {
        if (hasBeenClosed) {
            throw new IOException("The stream has been closed.");
        }
        // don't log nulls
        if (b == 0) {
            return;
        }
        // would this be writing past the buffer?
        if (count == curBufLength) {
            // grow the buffer
            final int newBufLength = curBufLength +
                    DEFAULT_BUFFER_LENGTH;
            final byte[] newBuf = new byte[newBufLength];
            System.arraycopy(buf, 0, newBuf, 0, curBufLength);
            buf = newBuf;
            curBufLength = newBufLength;
        }

        buf[count] = (byte) b;
        count++;
    }

    /**
     * Flushes this output stream and forces any buffered output
     * bytes to be written out.
     */
    public void flush() {
        if (count == 0) {
            return;
        }
        final byte[] bytes = new byte[count];
        System.arraycopy(buf, 0, bytes, 0, count);
        String str = new String(bytes);
        log.log(level, str);
        count = 0;
    }

    /**
     * Closes this output stream and releases any system resources
     * associated with this stream.
     */
    public void close() {
        flush();
        hasBeenClosed = true;
    }
}

现在,您可以通过以下方式捕获发送到 stderr 或 stdout 的消息:

System.setErr(new PrintStream(new LoggingOutputStream(
        Logger.getLogger("outLog"), Level.ERROR)));

log4j.properties 配置文件:

log4j.logger.outLog=error, out_log

log4j.appender.out_log=org.apache.log4j.RollingFileAppender
log4j.appender.out_log.file=/logs/error.log
log4j.appender.out_log.MaxFileSize=10MB
log4j.appender.out_log.threshold=error

德米特里•帕夫连科,SysGears公司

引用文本


4

Arthur Neves的回答基础上,我将其转移到了Slf4J。我还通过使用StringBuffer和直接将byte强制转换为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();
    }
}

1
在阅读了https://dev59.com/u2w05IYBdhLWcg3w11Qk#6996147的答案后,我开始查看现有的OutputStream实现,并偶然发现了org.apache.commons.exec.LogOutputStream
您只需要像这样使用Maven将其包含到您的项目中即可:
// ...
<dependencies>
    // ...
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-exec</artifactId>
        <version>1.3</version>
    </dependency>
    // ...
</dependencies>
// ...

以下是我在使用Selenium和Chrome Driver以及Google的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");
    }
}

通常不应该有任何奇怪的logLevel,但是可以通过寻找最接近的已知日志级别来处理这种情况。

希望这可以帮助你。


1
浏览这里的答案,似乎没有一个清晰地解释如何将字节解码为字符串(扩展CharSequence)。字节和字符是不等价的(请参见Java中的OutputStream与Writer)。一个简单的非拉丁字符,例如羼,可以表示为一系列字节:E7 BE BC(羼的UTF-8序列)。
其他人不考虑特定编码的原因:
- (char) b将转换非拉丁字符而不解释UTF-8,所以羼变成了ç¾¼,或者“Róbert”变成了“Róbert”(哦,我已经看到这个多少次了)。您可能更熟悉这个美丽的东西:(UTF-8 BOM) - new String(bytes)创建一个“使用平台默认字符集”的字符串,这取决于您运行代码的位置,因此您可能会在服务器和本地机器上获得不同的行为。这比(char)b好,因为您至少可以指定编码。 - log4j-iostreams的IoBuilder默认也使用平台默认字符集,但是可配置。这也不是通用解决方案,仅适用于在Log4j 2上使用SLF4J。尽管这是OP的问题。
(请原谅我的Kotlin,您可以使用不同的语法在Java中完全相同。)
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
}

通过这个解决方案:
  • 过渡性能高(缓冲)
  • 编码一致(BufferedReader有一个默认参数:charset = Charsets.UTF_8,可以根据需要更改)
  • 编码开销在后台线程中进行,虽然有点奇怪,但这就是管道的工作方式。
  • 代码简单/高级别
    (没有字节数组、索引、拷贝、字符串构造函数等)

注意:我使用这个来重定向Selenium ChromeDriver的输出(默认为stderr)到SLF4J over Log4J 2中:

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

0

2
虽然这理论上回答了问题,但最好在此处包含答案的基本部分,并提供参考链接。 - Kalle Richter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接