log4j2.yaml 动态更新,免重启生效!

42 阅读3分钟

背景

有时候生产环境需要临时调整日志级别 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

@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

@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 部分方法私有化,需要重写方法,暴露出来。

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 配置文件如下:

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,表示日志已经重新加载。

产出

研发团队引入这个组件后,可以根据自己的需求在线调整生产环境的日志配置,动态控制日志级别、输出路径,提高线上排查效率。

本文涉及的代码完全开源,感兴趣的伙伴可以查阅 eden-nacos-config-spring-cloud-starter