why can't I log method name and code line in l

2019-02-11 08:58发布

问题:

I've extended log42 logger.

The idea: I should pass enum to log method, in order to choose appender in runtime.

My interface:

public interface MyLoggerInterface {
    void info(String logMessage, MyLoggerAppenderEnum... appender);
    public static MyLoggerInterface getLogger(Class aClass, MyLoggerAppenderEnum... appender) {
        return MyLoggerInterfaceImpl.getLogger(aClass, appender);
    }
}

implementation:

    public class MyLoggerInterfaceImpl extends Logger implements MyLoggerInterface {
    private static final String FQCN = MyLoggerInterfaceImpl.class.getName();

    protected MyLoggerInterfaceImpl(LoggerContext context, String name, MessageFactory messageFactory) {
        super(context, name, messageFactory);
    }

    public static MyLoggerInterface getLogger(Class aClass, MyLoggerAppenderEnum... appenders) {
        return getLogger(aClass.getName(), appenders);
    }

    private static MyLoggerInterface getLogger(String name, MyLoggerAppenderEnum... appenders) {
        return (MyLoggerInterfaceImpl) org.apache.logging.log4j.LogManager.getLogger(name);
    }

    @Override
    public void info(String logMessage, MyLoggerAppenderEnum... appenders) {
        this.log(FQCN, Level.INFO, null, new SimpleMessage(logMessage), null, appenders);
    }

    private void log(String fqcn, Level level, Marker marker, Message message, Throwable throwable, MyLoggerAppenderEnum... appenders) {
        Arrays.stream(appenders)
                .map(appender -> findAppenderByName(appender))
                .collect(Collectors.toList())
                .forEach(appender ->
                        appender.append(
                                new Log4jLogEvent(this.getName(), marker, fqcn, level, message, new ArrayList<Property>(), throwable)
                        )
                );
    }

    private Appender findAppenderByName(MyLoggerAppenderEnum appenders) {
        return this.getAppenders().get(appenders.name());
    }
}

But Note that in log4j 2.X LoggerFactory is removed from 1.X version. So I implement additional classes, in order to avoid ClassCastException (Logger to MyLoggerInterfaceImpl) .

So. MyContext:

    public class MyLoggerContext extends LoggerContext {
    public MyLoggerContext(String name) {
        super(name);
    }
    @Override
    protected Logger newInstance(final LoggerContext ctx, final String name, final MessageFactory messageFactory) {
        return new MyLoggerInterfaceImpl(ctx, name, messageFactory);
    }
}

Context Selector:

public class MyLoggerContextSelector implements ContextSelector {

    private final LoggerContext CONTEXT = new MyLoggerContext("MyLoggerContext");
    public LoggerContext getContext(String fqcn, ClassLoader loader, boolean currentContext) {
        return CONTEXT;
    }
    public LoggerContext getContext(String fqcn, ClassLoader loader, boolean currentContext, URI configLocation) {
        return CONTEXT;
    }
    public List<LoggerContext> getLoggerContexts() {
        return Arrays.asList(CONTEXT);
    }
    public void removeContext(LoggerContext context) {
    }
}

Context Factory:

public class MyLoggerLog4jContextFactory extends Log4jContextFactory {
    public MyLoggerLog4jContextFactory() {
        super(new MyLoggerContextSelector(), new DefaultShutdownCallbackRegistry());
    }
}

and manager:

public class MyLoggerManager {
    public static void initialize(String configURL) {
        try {
            System.setProperty("log4j2.loggerContextFactory", "ge.test.core.logging.MyLoggerLog4jContextFactory");
            System.setProperty("Log4jLogEventFactory", "org.apache.logging.log4j.core.impl.DefaultLogEventFactory");
            Configurator.initialize(null, configURL);
        } catch (Exception ex ) {
            System.err.println("Cannot initialize Log4J using configuration url:" + configURL);
        }
    }
}

Cool! Everything works fine!!! and usage:

 MyLoggerManager.initialize("Log4j2.xml");
 MyLoggerInterface logger = MyLoggerInterface.getLogger(AppLauncher.class);
 logger.info("test", MyLoggerAppenderEnum.Console);

BUT problem is that If I use extend custom logger, I can't log method name and line. Layout is correct! If I dont use extended custom logger, mehtod name and line is logged too!

<Configuration status="WARN">
<Appenders>
    <Console name="Console" target="SYSTEM_OUT">
        <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} %method:%line - %msg%n"/>
    </Console>
    ...
</Appenders>
<Loggers>
    ...
    <Logger name="Console" level="trace" additivity="false">
        <appender-ref ref="Console" level="trace"/>
    </Logger>
    ...
    <Root level="error">
        ...
        <AppenderRef ref="Console"/>
    </Root>
</Loggers>
</Configuration>

Question: I want to log method name and line too. but it does not work after I extend my logger class (layout syntaxt is correct!)

I found the extended logger example here

and my code in here in gitlab

I use log4j 2.9.1

回答1:

I think there are two questions here:

  1. What is a good way to create a custom or extended logger that can log location information correctly?
  2. How can the application dynamically select the target appender at runtime?

1. Custom Loggers

How Log4j can print the method name and line number is by walking the stack trace (for each event) and finding the class/method in the application that called the logger. It can do this because Log4j knows the fully qualified class name (FQCN) of the logger. With a custom logger this requires a different FQCN and also requires that the stack trace has the same structure: the custom logger FQCN and the application class/method are the same number of lines apart in the stack trace as with the standard Log4j logger. This can be tricky to get right.

Would it be possible for you to achieve your goals with a logger wrapper? Log4j comes with a Logger wrapper generator tool. This tool was originally meant to support custom log levels and is documented on the Custom Log Levels page of the manual.

The generated logger code will take care of the FQCN and you can use it as a base for the further enhancements you have in mind.

2. Dynamically Selecting an Appender at Runtime

This requirement is common enough that Log4j2 provides a built-in solution, so you should not need to create a custom logger for this.

The standard way to solve this is to configure a Routing Appender. This appender can route log events to a set of predefined appenders or it can dynamically add new appenders if necessary.

The manual page has three examples, but the example in the FAQ page ("How do I dynamically write to separate log files?") may be a fairly close fit to your requirements. That example uses the ThreadContext map to control which log file subsequent events (in the current thread) get logged to.