背景

有时候生产环境需要临时调整日志级别 Level 或者修改日志输出路径 Appender,使用 Spring Boot 提供的日志刷新不能满足这个需求,最终还是重启服务才能生效。为了解决这个问题,需要实现 log4j2 配置文件的动态加载。

目标

实现零停机修改 log4j 配置,动态配置日志级别和输出路径。

实现

Nacos 配置监听通过 NacosConfigManager.getConfigService().addListener() 实现,为了避免 Nacos 服务端宕机,Nacos Client 实现了本地缓存机制,配置文件保存路径如下: ${user.home}/nacos/config/fixed-host_port-namespace_tenant/snapshot-tenant/namespace/group,将配置文件转化为 URI,利用 log4j2 的 API 重新加载日志配置 Configurator.reconfigure(uri)

首先,定义配置属性类 Log4j2NacosProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
@Setter
@Getter
@ConfigurationProperties(prefix = Log4j2NacosProperties.PREFIX)
public class Log4j2NacosProperties {

public static final String PREFIX = "log4j2.nacos";

private boolean enabled = false;

private String group;

private String dataId = "log4j2.yml";
}

编写自动装配组件 Log4j2NacosAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@ConditionalOnProperty(
prefix = Log4j2NacosProperties.PREFIX,
name = "enabled"
havingValue = "true"
)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureAfter(NacosConfigBootstrapConfiguration.class)
@EnableConfigurationProperties(Log4j2NacosProperties.class)
@ConditionalOnClass(LogManager.class)
@RequiredArgsConstructor
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Configuration(proxyBeanMethods = false)
public class Log4j2NacosAutoConfiguration implements InitializingBean {

private static final String RPC_CLIENT = "config_rpc_client";

private static final String FIXED = "fixed";

private final NacosConfigProperties nacosConfigProperties;

private final Log4j2NacosProperties log4j2ConfigProperties;

private final NacosConfigManager nacosConfigManager;

@Override
public void afterPropertiesSet() throws Exception {
File configFile = this.getFile(RPC_CLIENT);
if (!configFile.exists()) {
configFile = this.getFile(getServerName());
}
if (!configFile.exists()) {
log.warn("Loading log4j2 config file from nacos config cache failed");
return;
}
URI uri = configFile.toURI();
log.info("Loading log4j2 config file from nacos config cache: {}", uri);
Configurator.reconfigure(uri);
log.info("Loading log4j2 config file finished.");

nacosConfigManager.getConfigService().addListener(
log4j2ConfigProperties.getDataId(), log4j2ConfigProperties.getGroup(),
new Listener() {

@Override
public void receiveConfigInfo(String configInfo) {
log.info("Reloading log4j2 config file from nacos listener, changed info: \n{}", configInfo);
Configurator.reconfigure(uri);
}

@Override
public Executor getExecutor() {
return null;
}
});
}

private File getFile(String name) {
File file = getFailoverFile(name);
if (!file.exists()) {
file = getSnapshotFile(name);
}
return file;
}

private File getSnapshotFile(String name) {
return LocalConfigInfoProcessorExporter.getSnapshotFile(name,
log4j2ConfigProperties.getDataId(),
log4j2ConfigProperties.getGroup(), getNamespace());
}

private File getFailoverFile(String name) {
return LocalConfigInfoProcessorExporter.getFailoverFile(name,
log4j2ConfigProperties.getDataId(),
log4j2ConfigProperties.getGroup(), getNamespace());
}

private String getServerName() {
return StringUtils.join(FIXED, "-", getServerAddr());
}

private String getServerAddr() {
return nacosConfigProperties.getServerAddr()
.replaceAll("http(s)?://", "")
.replaceAll(":", "_");
}

private String getNamespace() {
return nacosConfigProperties.getNamespace();
}
}

由于 LocalConfigInfoProcessor 部分方法私有化,需要重写方法,暴露出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class LocalConfigInfoProcessorExporter extends LocalConfigInfoProcessor {

public static final String SUFFIX = "_nacos";

public static final String ENV_CHILD = "snapshot";

public static final String FAILOVER_FILE_CHILD_1 = "data";

public static final String FAILOVER_FILE_CHILD_2 = "config-data";

public static final String FAILOVER_FILE_CHILD_3 = "config-data-tenant";

public static final String SNAPSHOT_FILE_CHILD_1 = "snapshot";

public static final String SNAPSHOT_FILE_CHILD_2 = "snapshot-tenant";

public static File getFailoverFile(String serverName, String dataId, String group, String tenant) {
File tmp = new File(LOCAL_SNAPSHOT_PATH, serverName + SUFFIX);
tmp = new File(tmp, FAILOVER_FILE_CHILD_1);
if (StringUtils.isBlank(tenant)) {
tmp = new File(tmp, FAILOVER_FILE_CHILD_2);
} else {
tmp = new File(tmp, FAILOVER_FILE_CHILD_3);
tmp = new File(tmp, tenant);
}
return new File(new File(tmp, group), dataId);
}

public static File getSnapshotFile(String envName, String dataId, String group, String tenant) {
File tmp = new File(LOCAL_SNAPSHOT_PATH, envName + SUFFIX);
if (StringUtils.isBlank(tenant)) {
tmp = new File(tmp, SNAPSHOT_FILE_CHILD_1);
} else {
tmp = new File(tmp, SNAPSHOT_FILE_CHILD_2);
tmp = new File(tmp, tenant);
}
return new File(new File(tmp, group), dataId);
}
}

代码扩展完成,对应的 application.yaml 配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
cloud:
nacos:
config: # 配置中心
enabled: true # 默认关闭,请按需开启
server-addr: localhost:8848
namespace: demo
group: eden
username: nacos
password: nacos
extension-configs:
- group: eden
data-id: log4j2.yml
refresh: true

log4j2:
nacos: # Nacos 支持 log4j2 刷新,需要同时设置 spring.cloud.nacos.config.extension-configs
enabled: false
group: eden
data-id: log4j2.yml

关于 log4j2.yml 配置文件,直接在 Nacos 设置完成。

在 Nacos 修改 log4j.yml 发布后,可以从应用日志看到 Reloading log4j2 config file from nacos listener, changed info,表示日志已经重新加载。

产出

支持生产环境在线调整 Nacos 的日志配置,动态控制日志级别、输出路径,比 Spring 日志刷新操作更灵活。