Is there anyway to stop Spring Boot from overwriting my log config? - java

I have a logger that has the following properties:
PrivateConfig [loggerConfig=Special, config=org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration#709a148, loggerConfigLevel=INFO, intLevel=400, logger=Special.com:INFO in 73d16e93]
This is a Special logger that has a specific format and specific destination needed for my application
This Special logger is created through:
Level level = Level.toLevel(logLevel);
ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();
builder.setStatusLevel(level);
builder.setConfigurationName("whatever");
LoggerComponentBuilder specialLogger = builder.newLogger("Special", level);
AppenderComponentBuilder specialAppenderBuilder = builder
.newAppender("Special", "RollingFile")
.otherComponents();
specialAppenderBuilder.add(
builder
.newLayout("PatternLayout")
.otherComponents();
builder.add(specialAppenderBuilder);
specialLogger = specialLogger
.add(builder.newAppenderRef("Special"))
.addAttribute("additivity", false);
builder.add(specialLogger);
LoggerContext ctx = Configurator.initialize(builder.build());
ctx.start();
This logger work in Application A, which is not using Spring Boot
I import part of Application A to Application B, which is Spring Boot based, but then the exact same logger become:
PrivateConfig [loggerConfig=root, config=XmlConfiguration[location=jar:file:/Users/stamaki/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/2.4.4/38392ae406009e55efe873baee4633bfa6b766b3/spring-boot-2.4.4.jar!/org/springframework/boot/logging/log4j2/log4j2.xml], loggerConfigLevel=INFO, intLevel=400, logger=Special.com:INFO in 18b4aac2]
Note that the logger config is being overwritten from Special to root, it no longer follows my custom written BuiltConfiguration but use something from Springboot's internal xml.
How do I stop Spring Boot from overwriting my config?
There appears to be a similar question:
Spring is resetting my logging configuration - how do I work around this?
But the solution doesn't work
I also tried Spring Boot: LoggingApplicationListener interfering with Application Server logging But their solution still don't work for me as it change my logger into
PrivateConfig [loggerConfig=root, config=org.springframework.boot.logging.log4j2.SpringBootConfigurationFactory$SpringBootConfiguration#16a3cc88, loggerConfigLevel=ERROR, intLevel=200, logger=NonSensitive.com.textiq.precisionenhancer.service.PrecisionEnhancerBackendService:ERROR in 18b4aac2]
i.e. still use Root Config, not the Special Config I set up, only different from above being that this is the default Root Logger config.
Please do not make suggestions like "Update XML so it has the same format", this is not modular enough. I just wanted to use the same logger created by my library without having it being overwritten by Spring Boot.

Figure out what happened.
LoggerContext ctx = Configurator.initialize(builder.build());
ctx.start();
In both Application A and B, builder.build() has the right information, but Application B's ctx does not have the custom configuration
Investigate inside Configurator.initalize, this attempts to trigger Log4jContextFactory:
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
final boolean currentContext, final Configuration configuration) {
final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext, null);
if (externalContext != null && ctx.getExternalContext() == null) {
ctx.setExternalContext(externalContext);
}
if (ctx.getState() == LifeCycle.State.INITIALIZED) {
ContextAnchor.THREAD_CONTEXT.set(ctx);
try {
ctx.start(configuration);
} finally {
ContextAnchor.THREAD_CONTEXT.remove();
}
}
return ctx;
}
Note that the configuration is not used unless the context currently has state INITIALIZED
Probably because Spring will initialize some sort of Log4j internally in either way, even if you set -Dorg.springframework.boot.logging.LoggingSystem=none, the returned ctx is always STARTED.
The hack to it is that always call reinitailize to force configuration being updated to whatever that's inside builder.build():
BuiltConfiguration configuration = builder.build();
LoggerContext ctx = Configurator.initialize(configuration);
ctx.reconfigure(configuration);
ctx.start();

Related

capture logs in a test

I'm trying to capture bean allocation logs in a test - I've got code that I've tested, and will successfully capture logs from my classes - but when I try to do it on spring classes it is seemingly not working - here is the code I use to try and capture:
LoggerContext context = (LoggerContext) (LoggerFactory.getILoggerFactory());
Logger log = context.getLogger("org.springframework.beans.factory.support.DefaultListableBeanFactory");
log.setLevel(Level.DEBUG);
MyAppender appender = new MyAppender();
appender.setContext( context);
log.addAppender( appender );
SpringApplication newApplication = new SpringApplication( Application.class);
newApplication.run( new String [] {});
Now if I trace in and look at the logger that spring is using - it looks like a completely different style of logger - (its hooked to a logmanager, not a loggercontext) - and go into that and it seems like it might be a different context?
Any idea what I'm doing wrong, and how I can in a unit test capture spring bean creation logs?
Spring boot is using Logback logger by default
It uses LogbackLoggingSystem implementation which
extends from AbstractLoggingSystem
Spring boot LoggingSystem runs before context is initialized
To override default properties you can define logback.xml or logback-spring.xml
Or you can use application.yml or properties file to define log configurations :
logging.level.* : It is used as prefix with package name to set log level.
logging.file : It configures a log file name to log message in file. We can also configure file name with absolute path.
logging.path : It only configures path for log file. Spring boot creates a log file with name spring.log
logging.pattern.console : It defines logging pattern in console.
logging.pattern.file: It defines logging pattern in file.
logging.pattern.level: It defines the format to render log level. Default is %5p.
As documentation says:
You can force Spring Boot to use a particular logging system by using the org.springframework.boot.logging.LoggingSystem system property. The value should be the fully qualified class name of a LoggingSystem implementation. You can also disable Spring Boot’s logging configuration entirely by using a value of none.
If you use static Loggers in your Class under Test, you could use Powermock to mock the logger and assert the output, as descirbed in this question.
We use it in our Spring-Tests and formatting and style is the same.
for anyone interested - this finally worked for me:
Logger logger = Logger.getLogger(
"org.springframework.beans.factory.support.DefaultListableBeanFactory");
logger.addHandler( this );
logger.setLevel( java.util.logging.Level.FINE);
_logger = logger;
now I can capture, trace and time all bean allocations.

Equivalent programmatic logger initialization for log4j to log4j2 migration

I am trying to understand the programmatic side of log4j2, as I am migrating a lot of log4j 1.2 code. The following seems to be very different and more complicated to accomplish:
org.apache.log4j.Logger.getRootLogger().setLevel(Level.FATAL);
org.apache.log4j.Logger.getRootLogger().addAppender(new ConsoleAppender(new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN), "System.err"));
Can someone with plenty of log4j2 experience explain to me what the simple way to migrate the 2 above lines is?
EDIT: This is what I have so far:
LoggerContext context = (LoggerContext) LogManager.getContext(false);
Configuration config = context.getConfiguration();
config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).setLevel(Level.FATAL);
PatternLayout patternLayout = PatternLayout.createLayout(PatternLayout.TTCC_CONVERSION_PATTERN, config, null, null, true, true, null, null);
Layout<? extends Serializable> layout = patternLayout;
ConsoleAppender consoleAppender = ConsoleAppender.createAppender(layout , null, "SYSTEM_ERR", "System.err", null, null);
consoleAppender.start();
config.addAppender(consoleAppender);
context.updateLoggers();
There is no way it is that complicated, right?
Unfortunately creating programmatic configuration wasn't the main concern of Log4j2. You may check on their AbstractConfiguration class sources line 580 to see how default configuration is set programmatically internally.
Log4j2 library have good support for different types of configuration files (xml, json, properties, yaml) or you can build composite configuration out of several sources. Also it tracks configuration files and is capable of dynamic reloading.
I would remmend you evaluating features mentioned above to update configuration from code. E.g.
final URL log4j = Resources.getResource("log4j2-test.xml");
ConfigurationSource configurationSource = new ConfigurationSource(
Resources.asByteSource(log4j).openStream(), log4j);
LoggerContext context = LoggerContext.getContext(false);
XmlConfiguration xmlConfiguration = new XmlConfiguration(context, configurationSource);
context.start(xmlConfiguration);
Seem easier to manage than programmatic way with larger configurations.

Shutting up log4j for good

Im using a 3rd party jar that uses log4j and spams really way too much (makes eclipse crash), is there a way to wipe all logging as if log4j never existed in the first place? programmatically or by project setup anything is fine as long as it stfu.
i tried
List<Logger> loggers = Collections.<Logger>list(LogManager.getCurrentLoggers());
loggers.add(LogManager.getRootLogger());
for ( Logger logger : loggers ) {
logger.setLevel(Level.OFF);
}
But doesnt compile in my setup:
LogManager.getCurrentLoggers() and
LogManager.getRootLogger() do not exist.
Log4j2 uses multiple contexts. Adapting the above code:
LoggerContext ctx = (LoggerContext) LogManager.getContext();
Configuration config = ctx.getConfiguration();
Collection<LoggerConfig> loggers = config.getLoggers().values();
for(LoggerConfig cfg: loggers) {
cfg.setLevel(Level.OFF);
}

How to allow user to make permanent changes to log levels?

I have a Spring Boot application that uses log4j2 for logging. I have a need to adjust the logging level at run time and I have done that with a simple RESTful interface that accepts a logger name and the level it needs to be set at. I also need to be able to make permanent changes to the logging level (on just certain loggers).
a) Is there a way to persist my changes back to the log4j config file so that the next time the application is brought up, the log levels are where they had been left at, on the previous run?
b) Is there a way to read the list of loggers listed in the config file?
Thank you
a) If you have a config file, every time the server starts it will be used to configure log4j2. You could create a new config file (outside the container) and use it to configure log4j2 when the server starts:
File file = new File("/config/new/log4j2.xml");
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
ctx.setConfigLocation(file.toURI());
Or just store the new info and modify log4j programatically
b) Try this:
public Collection<Logger> getLoggers()
https://logging.apache.org/log4j/2.0/log4j-core/apidocs/org/apache/logging/log4j/core/LoggerContext.html#getLoggers()
Here's a code example (executed at the application startup):
File file = new File(log4jConfigFilePath);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
ctx.setConfigLocation(file.toURI());
Collection<org.apache.logging.log4j.core.Logger> collection = ctx.getLoggers();
System.out.println(collection.size()); // returns 0 (No loggers instantiated)
Logger logger = LoggerFactory.getLogger("myLogger");
collection = ctx.getLoggers();
System.out.println(collection.size()); // returns 1 (myLogger instantiated)

Logback defaulting to debug mode for some third party libraries

We use slf4j and logback for logging in our java ee web application. Recently in order to move these libraries to a common location in the glassfish app server (to glassfish\lib directory specifically) we made a couple of configuration changes. We made changes in the web.xml file to add JNDI entries and renamed the logback.xml to logback-<context-name>.xml as specified in here. The logback-<context-name>.xml is placed in WEB-INF\classes directory.
After this change, logging for hibernate and http-client libraries is defaulting to debug mode and hence there is lot of logging being done. The root logger has INFO log level and there are no specific loggers defined for hibernate & http-client libraries.
If I revert this change i.e. remove the jndi entry in web.xml and rename the configuration file to logback.xml, only INFO logs are logged as expected.
Any suggestions on what could be the issue?
Thanks.
Update
On troubleshooting further I released that hibernate and apache client libraries use the default logger context created during the initialization time. This logger context has the root log level set to DEBUG.
We use JNDI as the context selector.
It is strange that the context name is not found at runtime when the ConnectionManager class (hibernate class - org.hibernate.jdbc.ConnectionManager) tries to instantiate a logger. Logger instance creation calls ContextJNDISelector.getLoggerContext() method. This method does a JNDI lookup but does not find the entry.
I troubleshooted further by spawning a thread in a servlet context listener to print the JNDI entry (java:comp/env/logback/context-name) every 3 seconds. The logs from the thread indicate that the JNDI entries are always present.
Any ideas why is contextName is not found in JNDI when queried by ContextJNDISelector?
This seemed to be a problem because the application used EJB's. The application server (glassfish) loaded the EJB's before the logging context was actually set. Hence the log messages for some of the libraries got logged under DEBUG level. Using a wrapper class around the logger resolved the issue. The wrapper class delayed the logger creation until its first use.
public class LogWrapper {
private Class loggerClass;
private Logger logger;
public LogWrapper(Class loggerClass) {
this.loggerClass = loggerClass; //lazy logging context creation (to avoid issues with static instances in EJBs which get loaded on startup before logging ctx name is actually set)
}
/**
* For lazy init of logger, on first actual use, so the logger context will be correctly set even when used by EJBs (loaded by classloader too soon, before logging context is actually set)
*/
private Logger getLogger() {
if (logger == null) {
logger = LoggerFactory.getLogger(loggerClass);
loggerClass = null;
}
return logger;
}
public void info(String message) {
getLogger().info(message);
}

Categories