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

2

2

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.

Grotto answered 10/9, 2021 at 8:45 Comment(0)
P
1

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.
Paulettepauley answered 10/9, 2021 at 9:10 Comment(6)
That's not answering the question I made... Why "encapsulating" is the wrong way to go? And.. why do you think I'm encapsulating my logging framework? For me, it seems more like a centralizaed code rather than an encapsulated one. Btw, it seems like a lot of people is interested in it. I just can't figure out why is the wrong way to go.Grotto
You are right in regards to the directly answering the question, I rephrased my answer. In regards to encapsulation - well, you're gaining nothing (logger initialization is one-liner that happens in one place or another) while forgoing some if it's capabilities (like the ability to write the caller method to the log). So, you can see why that might be considered as "the wrong way" in my book. In your utility class, it seems like you're trying to cache logger instances, which is also done for you behind the scenes by the log4j2 framework, so it's unnecessary, at best.Paulettepauley
The reason for caching the instances is just for avoiding re-creating them, but, ignoring that, I just want to know how could I prevent log4j2 to show on the stack trace the helper class. But, as I can see, no one is answering any way, so I just have the "normal" option, which is to create the logger for each class, at least afaik. Thank you for your opinion either way <3 :)Grotto
As I explained, no need to cache instances, as the log4j2 does that for you. The reason for me pointing that out, is that if you drop this functionality, there's no benefit in using your wrapper (as you didn't mention you care about decoupling the rest of your code from log4j2). You can also have a look at scala/kotlin/Lombok log4j2 extensions, that also greatly favor instantiating loggers directly.Paulettepauley
Thanks for that recommendation, I'll take a look!Grotto
No prob. If you find it helpful, please accept my answer.Paulettepauley
C
0

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!
Cartwright answered 25/5 at 16:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.