Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,8 @@
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.LoggerConfig;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URI;
import java.util.Map;

/**
Expand All @@ -44,14 +39,18 @@ public class Log4J2NacosLoggingAdapter implements NacosLoggingAdapter {

private static final String NACOS_LOG4J2_LOCATION = "classpath:nacos-log4j2.xml";

private static final String FILE_PROTOCOL = "file";

private static final String NACOS_LOGGER_PREFIX = "com.alibaba.nacos";

private static final String APPENDER_MARK = "ASYNC_NAMING";

private static final String LOG4J2_CLASSES = "org.apache.logging.slf4j.Log4jLogger";

private final NacosLog4j2Configurator configurator;

public Log4J2NacosLoggingAdapter() {
this.configurator = new NacosLog4j2Configurator();
}

@Override
public boolean isAdaptedLogger(Class<?> loggerClass) {
Class<?> expectedLoggerClass = getExpectedLoggerClass();
Expand Down Expand Up @@ -94,44 +93,31 @@ private void loadConfiguration(String location) {
if (StringUtils.isBlank(location)) {
return;
}
final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
final Configuration contextConfiguration = loggerContext.getConfiguration();

// load and start nacos configuration
Configuration configuration = loadConfiguration(loggerContext, location);
configuration.start();
final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);

// append loggers and appenders to contextConfiguration
Map<String, Appender> appenders = configuration.getAppenders();
for (Appender appender : appenders.values()) {
contextConfiguration.addAppender(appender);
}
Map<String, LoggerConfig> loggers = configuration.getLoggers();
for (String name : loggers.keySet()) {
if (name.startsWith(NACOS_LOGGER_PREFIX)) {
contextConfiguration.addLogger(name, loggers.get(name));
}
// Fast path: check if already loaded (avoid lock contention in normal case)
if (loggerContext.getConfiguration().getAppender(APPENDER_MARK) != null) {
return;
}

loggerContext.updateLoggers();
}

private Configuration loadConfiguration(LoggerContext loggerContext, String location) {
try {
URL url = ResourceUtils.getResourceUrl(location);
ConfigurationSource source = getConfigurationSource(url);
// since log4j 2.7 getConfiguration(LoggerContext loggerContext, ConfigurationSource source)
return ConfigurationFactory.getInstance().getConfiguration(loggerContext, source);
} catch (Exception e) {
throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, e);
}
}

private ConfigurationSource getConfigurationSource(URL url) throws IOException {
InputStream stream = url.openStream();
if (FILE_PROTOCOL.equals(url.getProtocol())) {
return new ConfigurationSource(stream, ResourceUtils.getResourceAsFile(url));
// Thread-safe double-checked locking to prevent duplicate loading in concurrent scenarios
// Although normal usage is single-threaded (via ScheduledExecutorService), this ensures
// robustness in edge cases like concurrent framework initialization or testing scenarios
synchronized (loggerContext) {
final Configuration config = loggerContext.getConfiguration();
if (config.getAppender(APPENDER_MARK) != null) {
return;
}

try {
// Use custom NacosLog4j2Configurator (framework-compliant approach similar to Logback)
URI configUri = ResourceUtils.getResourceUrl(location).toURI();
configurator.configure(loggerContext, configUri);

} catch (Exception e) {
throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, e);
}
}
return new ConfigurationSource(stream, url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ public static void setProperties(NacosLoggingProperties properties) {
INSTANCE.properties = properties;
}

public static NacosLoggingProperties getProperties() {
return INSTANCE.properties;
}

public static String getValue(String key) {
return null == INSTANCE.properties ? null : INSTANCE.properties.getValue(key, null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 1999-2023 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.nacos.logger.adapter.log4j2;

import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;

/**
* Custom Log4j2 Configurator for Nacos logging.
*
* <p>This class provides a framework-compliant way to load Nacos logging configuration
* without interfering with user's application logging setup. It follows the same design
* pattern as Logback's NacosLogbackConfiguratorAdapterV1.
*
* <p>Key features:
* <ul>
* <li>Uses {@link Configuration#initialize()} instead of {@link Configuration#start()}
* to avoid ClassUnload issue (#13940)</li>
* <li>Additively merges Nacos configuration into existing LoggerContext</li>
* <li>Non-invasive: does not replace user's logging configuration</li>
* </ul>
*
* @author xiweng.yy
* @see <a href="https://github.com/alibaba/nacos/issues/13940">#13940</a>
* @since 3.2.0
*/
public class NacosLog4j2Configurator {

private static final String NACOS_LOGGER_PREFIX = "com.alibaba.nacos";

/**
* Configure LoggerContext by loading Nacos configuration from URI.
* This method additively merges Nacos appenders and loggers into the existing configuration.
*
* @param loggerContext The LoggerContext to configure
* @param configLocation URI of the Nacos Log4j2 configuration file
* @throws IOException if configuration file cannot be read
*/
public void configure(LoggerContext loggerContext, URI configLocation) throws IOException {
Configuration nacosConfig = loadConfiguration(loggerContext, configLocation);

// Key fix for issue #13940: Use initialize() instead of start()
// initialize() sets up the configuration without triggering plugin reinitialization
nacosConfig.initialize();

// Get the current active configuration
Configuration currentConfig = loggerContext.getConfiguration();

// Additively merge Nacos appenders (non-invasive approach for middleware)
// Note: Appenders are started individually and added to currentConfig
// They are NOT removed from nacosConfig to avoid lifecycle issues
nacosConfig.getAppenders().values().forEach(appender -> {
if (!appender.isStarted()) {
appender.start();
}
currentConfig.addAppender(appender);
});

// Add only Nacos-specific loggers to avoid interfering with user configuration
nacosConfig.getLoggers().entrySet().stream()
.filter(entry -> entry.getKey().startsWith(NACOS_LOGGER_PREFIX))
.forEach(entry -> currentConfig.addLogger(entry.getKey(), entry.getValue()));

// Apply the merged configuration
loggerContext.updateLoggers();

// Important: Do NOT call nacosConfig.stop() here!
// The appenders and loggers have been transferred to currentConfig.
// Calling stop() would shut down the appenders that are now owned by currentConfig.
// nacosConfig will be garbage collected naturally, and since we only called initialize()
// (not start()), there are no active background threads or resources to clean up.
}

/**
* Load Log4j2 configuration from URI using ConfigurationFactory.
* This is the standard Log4j2 way to parse configuration files.
*
* @param ctx LoggerContext
* @param configLocation URI of configuration file
* @return Parsed Configuration object
* @throws IOException if configuration cannot be loaded
*/
private Configuration loadConfiguration(LoggerContext ctx, URI configLocation) throws IOException {
try (InputStream stream = configLocation.toURL().openStream()) {
ConfigurationSource source = new ConfigurationSource(stream, configLocation.toURL());
return ConfigurationFactory.getInstance().getConfiguration(ctx, source);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -30,13 +29,6 @@
import org.mockito.junit.jupiter.MockitoExtension;

import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Map;
import java.util.logging.Logger;

Expand All @@ -45,10 +37,8 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class Log4J2NacosLoggingAdapterTest {
Expand Down Expand Up @@ -135,20 +125,4 @@ void testLoadConfigurationWithWrongLocation() {
});
}

@Test
void testGetConfigurationSourceForNonFileProtocol()
throws NoSuchMethodException, IOException, InvocationTargetException, IllegalAccessException, URISyntaxException {
Method getConfigurationSourceMethod = Log4J2NacosLoggingAdapter.class.getDeclaredMethod("getConfigurationSource", URL.class);
getConfigurationSourceMethod.setAccessible(true);
URL url = mock(URL.class);
URI uri = mock(URI.class);
InputStream inputStream = mock(InputStream.class);
when(uri.toURL()).thenReturn(url);
when(url.toURI()).thenReturn(uri);
when(url.openStream()).thenReturn(inputStream);
when(url.getProtocol()).thenReturn("http");
ConfigurationSource actual = (ConfigurationSource) getConfigurationSourceMethod.invoke(log4J2NacosLoggingAdapter, url);
assertEquals(inputStream, actual.getInputStream());
assertEquals(url, actual.getURL());
}
}
Loading