Печать имени потока с использованием java.util.logging

Можно ли напечатать имя потока в операторах журнала, сгенерированных java.util.logging.Logger?

Один из вариантов - сделать что-то вроде следующего:

logger.info(thread.getName() + " some useful info");

но он повторяется, и структура ведения журналов должна справиться с этим.

Ответ 1

Смущающе, но выглядит как java.util.logging не может этого сделать...

По умолчанию java.util.logging.SimpleFormatter не имеет возможности регистрировать имя потока вообще. java.util.logging.FileHandler поддерживает несколько заполнителей шаблонов, ни одно из них не является именем потока.

java.util.logging.XMLFormatter является самым близким, но только logs thread id:

<record>
  <date>2011-07-31T13:15:32</date>
  <millis>1312110932680</millis>
  <sequence>0</sequence>
  <logger></logger>
  <level>INFO</level>
  <class>java.util.logging.LogManager$RootLogger</class>
  <method>log</method>
  <thread>10</thread>
  <message>Test</message>
</record>

Если вы думаете, что мы приближаемся, мы не знаем. LogRecord класс содержит только идентификатор потока, а не его имя - не очень полезно.

Ответ 2

У меня была аналогичная проблема. Как показано здесь Как выровнять сообщения журнала с помощью java.util.logging, вы можете расширить java.util.logging.Formatter, но вместо получения LogRecord#getThreadID() вы можете получить имя потока, вызвав Thread.currentThread().getName() например:

public class MyLogFormatter extends Formatter
{

    private static final MessageFormat messageFormat = new MessageFormat("[{3,date,hh:mm:ss} {2} {0} {5}]{4} \n");

    public MyLogFormatter()
    {
        super();
    }

    @Override
    public String format(LogRecord record)
    {
        Object[] arguments = new Object[6];
        arguments[0] = record.getLoggerName();
        arguments[1] = record.getLevel();
        arguments[2] = Thread.currentThread().getName();
        arguments[3] = new Date(record.getMillis());
        arguments[4] = record.getMessage();
        arguments[5] = record.getSourceMethodName();
        return messageFormat.format(arguments);
    }

}

Ответ 3

Некоторые серверы приложений неявно регистрируют идентификатор потока (я знаю WebSphere). Вы можете создать свой собственный LogFormatter. Записи, переданные в форматтера, содержат идентификатор потока, см. здесь. Я реализовал этот подход для Tomcat несколько раз, но он будет работать и в средах Java SE.

BTW: Имя Thread не доступно для LogRecord.

Ответ 4

java.util.logging имеет много любопытных особенностей. вы можете добавить API-интерфейс фасада, чтобы настроить его поведение

public class Log

    Logger logger;

    static public Log of(Class clazz)
        return new Log( Logger.getLogger( clazz.getName() ));

    public void error(Throwable thrown, String msg, Object... params)
    {
        log(ERROR, thrown, msg, params);
    }

    void log(Level level, Throwable thrown, String msg, Object... params)
    {
        if( !logger.isLoggable(level) ) return;

        // bolt on thread name somewhere
        LogRecord record = new LogRecord(...);
        record.setXxx(...);
        ...
        logger.log(record);
    }

----

static final Log log = Log.of(Foo.class);
....
log.error(...);

Люди используют java-протоколирование в основном потому, что не хотят иметь зависимостей сторонних разработчиков. Именно поэтому они не могут зависеть от существующих фасадов, таких как apache или slf4j.

Ответ 5

С пользовательским Formatter

К счастью, LogRecord содержит идентификатор потока, который создал сообщение журнала. Мы можем заполучить этот LogRecord при написании собственного Formatter. Как только мы это получим, нам нужно только получить имя потока через его идентификатор.

Есть несколько способов получить объект Thread соответствующий этому идентификатору, здесь мой:

static Optional<Thread> getThread(long threadId) {
    return Thread.getAllStackTraces().keySet().stream()
            .filter(t -> t.getId() == threadId)
            .findFirst();
}

Ниже приведен минимальный Formatter который печатает только имя потока и сообщение журнала:

private static Formatter getMinimalFormatter() {
    return new Formatter() {

        @Override
        public String format(LogRecord record) {

            int threadId = record.getThreadID();
            String threadName = getThread(threadId)
                    .map(Thread::getName)
                    .orElseGet(() -> "Thread with ID " + threadId);

            return threadName + ": " + record.getMessage() + "\n";
        }
    };
}

Чтобы использовать свой пользовательский форматтер, снова есть разные варианты, один из способов - изменить ConsoleHandler по умолчанию:

public static void main(final String... args) {

    getDefaultConsoleHandler().ifPresentOrElse(
            consoleHandler -> consoleHandler.setFormatter(getMinimalFormatter()),
            () -> System.err.println("Could not get default ConsoleHandler"));

    Logger log = Logger.getLogger(MyClass.class.getName());
    log.info("Hello from the main thread");
    SwingUtilities.invokeLater(() -> log.info("Hello from the event dispatch thread"));
}

static Optional<Handler> getDefaultConsoleHandler() {
    // All the loggers inherit configuration from the root logger. See:
    // https://docs.oracle.com/javase/8/docs/technotes/guides/logging/overview.html#a1.3
    var rootLogger = Logger.getLogger("")
    // The root logger first handler is the default ConsoleHandler
    return first(Arrays.asList(rootLogger.getHandlers()));
}

static <T> Optional<T> first(List<T> list) {
    return list.isEmpty() ?
            Optional.empty() :
            Optional.ofNullable(list.get(0));
}

Ваш минимальный Formatter должен затем создать следующие сообщения журнала, содержащие имя потока:

main: привет из основного потока

а также

AWT-EventQueue-0: Привет из потока отправки события


Это средство Formatter которое показывает, как регистрировать больше, чем имя потока и сообщение журнала:

private static Formatter getCustomFormatter() {
    return new Formatter() {

        @Override
        public String format(LogRecord record) {

            var dateTime = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault());

            int threadId = record.getThreadID();
            String threadName = getThread(threadId)
                    .map(Thread::getName)
                    .orElse("Thread with ID " + threadId);

            // See also: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Formatter.html
            var formatString = "%1$tF %1$tT %2$-7s [%3$s] %4$s.%5$s: %6$s %n%7$s";

            return String.format(
                    formatString,
                    dateTime,
                    record.getLevel().getName(),
                    threadName,
                    record.getSourceClassName(),
                    record.getSourceMethodName(),
                    record.getMessage(),
                    stackTraceToString(record)
            );
        }
    };
}

private static String stackTraceToString(LogRecord record) {
    final String throwableAsString;
    if (record.getThrown() != null) {
        var stringWriter = new StringWriter();
        var printWriter = new PrintWriter(stringWriter);
        printWriter.println();
        record.getThrown().printStackTrace(printWriter);
        printWriter.close();
        throwableAsString = stringWriter.toString();
    } else {
        throwableAsString = "";
    }
    return throwableAsString;
}

Тот Formatter производит сообщения журнала как эти:

2019-04-27 13:21:01 ИНФОРМАЦИЯ [AWT-EventQueue-0] package.ClassName.method: сообщение журнала

Ответ 6

Несколько приведенных выше ответов показывают, что LogRecord.getThreadId() возвращает значащий идентификатор потока, и все, что нам не хватает, это способ сопоставить это с именем потока.

К сожалению, LogRecord.getThreadId() возвращает значение int, которое не соответствует длинному идентификатору потока, который вызвал сообщение журнала.

Поэтому мы не можем просто использовать ManagementFactory.getThreadMXBean() для разрешения имени потока. Это приводит к случайным именам потоков.

Если вы уверены, что ваше средство ведения журнала всегда форматируется в том же потоке, что и вызывающий, тогда вы можете создать пользовательский Formatter, как было предложено выше, и вызвать Thread.currentThread(). getName().

Кажется, что Logging facade или сторонняя библиотека являются единственными полностью безопасными опциями.

Ответ 7

Чтобы дополнить @l245c4l ответ: вместо использования SimpleFormatter() используйте:

//fileHandler.setFormatter(new SimpleFormatter());

class MyFormatter extends Formatter {
    private final MessageFormat messageFormat = new MessageFormat("{0,date}, {0,time} {1} {2}: {3} [T:{4}] {5}\n");

    public String format(LogRecord record)
    {
        Object[] arguments = new Object[6];
        arguments[0] = new Date( record.getMillis() );
        arguments[1] = record.getSourceClassName();
        arguments[2] = record.getSourceMethodName();
        arguments[3] = record.getLevel();
        arguments[4] = Long.toString( Thread.currentThread().getId() );
        arguments[5] = record.getMessage();

        return messageFormat.format(arguments);
    }
}

fileHandler.setFormatter( new MyFormatter() ); 

Logger myLogger = Logger.getLogger("<LOGGER_NAME>");
myLogger.addHandler(fileHandler);

где T:{4} - это идентификатор потока (аргумент 4).