背景
有时候生产环境需要临时调整日志级别 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: 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 日志刷新操作更灵活。