Nakarukatoshi Uzumaki
Nakarukatoshi Uzumaki

Reputation: 365

How to log the caller of a method instead of the method which is calling the Logger

Given a logging utility class, how to log everything through that class instead of creating a Logger object per class?

For example, instead of:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {

    private static final Logger LOG = LogManager.getLogger(Main.class);

    public static void main(String[] args) {
        LOG.info("Application started!");
    }
}

I would like to do something like this:

import my.utils.LogUtils;

public class Main {

    public static void main(String[] args) {
        LogUtils.info("Application started!");
    }
}

My LogUtils class looks like this:

package my.utils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.Map;

public final class LogUtils {

    private LogUtils() {
        throw new AssertionError("private constructor: " + LogUtils.class.getName());
    }

    private static final Map<Class<?>, Logger> LOGGERS = new HashMap<>();

    static {
        Class<?> current = LogUtils.class;
        LOGGERS.put(current, LogManager.getLogger(current));
    }

    public static void info(Object msg) {
        Logger logger = getFor(getCallerClass());
        // logger.info()... Here's where I am stuck! What I want to log in the stack trace is the *caller* of the "info" method, not the "info" method.
    }

    private static Logger getFor(Class<?> clazz) { return LOGGERS.computeIfAbsent(clazz, key -> LogManager.getLogger(key)); }

    private static Class<?> getCallerClass() {
        try {
            return Class.forName(getCaller(3).getClassName());
        } catch (ClassNotFoundException e) {
            return LogUtils.class;
        }
    }

    // This method should return "main" method name, but it's not being used because I don't know what should I do now
    private static String getCallerMethod() { return getCaller(3).getMethodName(); }

    private static StackTraceElement getCaller(int level) { return Thread.currentThread().getStackTrace()[level]; }
}

I have read several log4j2 documentation pages, but I found nothing regarding my question, and I also checked several stack overflow questions, but it seems like, whatever I try to search, results in a completely different question.

Is this even possible to do? Because I am starting to doubt it. Denote that I am trying to avoid the usage of a Logger per class... I wouldn't ask the question otherwise. It is, at least, possible to create a custom logger which logs a custom stack trace level?

As a side note, my Maven dependencies are the ones given on the log4j2 page:

<dependencies>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>2.14.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.14.1</version>
    </dependency>
</dependencies>

I also must mention that, on an answer, there's this call:

LOG.log(LoggingHelper.class.getCanonicalName(), Level.INFO, message, null);

I can't find a method in org.apache.logging.log4j.Logger which (Javadoc like) refers like this:

Logger#log(String, Level, Object, Throwable);

It simply doesn't exist.

Upvotes: 2

Views: 1975

Answers (2)

PhilKes
PhilKes

Reputation: 138

I wrote a maven plugin exactly for this purpose when using SLF4J (supports Log4j): slf4j-caller-info-maven-plugin

This plugin injects the caller of the logging methods to the MDC which can be simply used in the log pattern.

To achieve what you want:

  1. pom.xml:
<build>
    <plugins>
        <plugin>
            <groupId>io.github.philkes</groupId>
            <artifactId>slf4j-caller-info-maven-plugin</artifactId>
            <version>1.1.0</version>
            <executions>
                <execution>
                    <goals>
                        <goal>inject</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <!-- Inject only the class name-->
                <injection>%class</injection>
                <!-- Method descriptors for all logging methods of your Util class -->
                <injectedMethods>
                    <injectedMethod>my/utils/LogUtils#info</injectedMethod>
                </injectedMethods>
                <!-- If you want the package name of the class to be included -->
                <includePackageName>true</includePackageName>
            </configuration>
        </plugin>
    </plugins>
</build>
  1. Make sure to include the callerInformation parameter in your log4j.xml pattern, e.g.:
<PatternLayout>
    <Pattern>%d %p %X{callerInformation} - %m %ex%n</Pattern>
</PatternLayout>

Then your LogUtils.info("Application started!"); should output:

18:50:12.344 INFO my.Main - Application Started!

Upvotes: 0

Sheinbergon
Sheinbergon

Reputation: 3063

While it is possible when using Log4j2 Loggers directly (using the %M parameter), which is what you are trying to avoid, IMO, encapsulating the logging framework is the wrong way to go.

  • If you feel you wish to decouple the logging implementation from your code, use slf4j.
  • If you care about performance, use log4j2 directly (though, logging the caller method using %M is very expensive, as described here
  • If you care about logging GC overhead due to logging instances being re-created (which you shouldn't with the new ZGC/Shenandoah GCs), well, log4j2 takes care of that behind the scenes (as described here), so no worries
  • Also consider using Lombok for Log4j2 logger instantiation. Note that Lombok/Scala/Kotlin Log4j2 extensions heavily favor direct logger instantiation, which might be good indication that is the right way to go.

Upvotes: 1

Related Questions