GitHub 19k Star Путь Java-инженера к тому, чтобы стать богом, разве вы не хотите узнать об этом!
Я написал статью раньшеЯвно есть автоматическая очистка, но лог все равно взрывает мой сервис!", привел к большому сбою продвижения, потому что объем журнала резко увеличился, что привело к почти зависанию сервера.
После того, как возникла эта проблема, я разработал простой инструмент понижения версии журнала, который может динамически проталкивать уровень журнала через конфигурацию и динамически изменять уровень вывода журнала в строке. И настройте модификацию этой конфигурации на нашу платформу плана и выполняйте обычную или экстренную обработку плана в период действия акции.
Итак, в этой статье мы кратко познакомим вас с идеей и реализацией кода.
уровень журнала
Прежде чем начать текст, я кратко расскажу об уровне логирования.Разные фреймворки поддерживают разные уровни логирования.Наиболее распространенными являются Log4j и Logback.
В Log4j поддерживается 8 уровней журналов, приоритет от высокого к низкому: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL.
Logback поддерживает 7 уровней ведения журнала, приоритет от высокого к низкому: OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL.
Вы можете увидеть общие ERROR, WARN, INFO, DEBUG, оба из которых поддерживаются.
Так называемая установка уровня вывода лога означает, чтоМинимальный уровень выходных логов, то есть, если мы установим уровень INFO, то могут быть выведены журналы, включающие INFO и более высокие уровни приоритета, чем INFO.
Будь то Log4j или Logback, уровень вывода журнала контролируется через файл конфигурации журнала. Я не буду здесь вдаваться в подробности.
структура регистрации
Выше мы упомянули Log4j и Logback, которые являются наиболее часто используемыми платформами ведения журналов.
Но часто мы не используем эту структуру ведения журналов напрямую для печати журналов в коде, а полагаемся на фасад журнала, такой как slf4j, commons-logging и т. д.
Как правило, наиболее часто используемый метод заключается в получении Logger с помощью getLogger LoggerFactory, предоставляемого slf4j, а затем распечатке журнала.
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerService.class);
public void test(){
LOGGER.info("hollis log test");
}
Когда мы используем метод LoggerFactory.getLogger для создания объекта Logger, мы передаем loggerName и используем это loggerName для уникальной идентификации Logger.Вышеупомянутый метод заключается в использовании полного имени пути класса LoggerService в качестве его loggerName.
loggerName является частью информации о конфигурации каждого регистратора, в дополнение к уровню вывода журнала и другой информации.
Что касается того, почему бы не использовать log4j и logback для прямой печати журналов, я нахожусь в "Почему Alibaba запрещает инженерам напрямую использовать API в системах логирования (Log4j, Logback)"был проанализирован.
Уровень журнала изменений Артаса
Прежде чем приступить к реализации кода, давайте представим инструмент, который также может помочь нам динамически изменять уровень журнала.
Это артефакт Али с открытым исходным кодом - Артас (arthas.aliyun.com/doc/).
Arthas предоставляет команду регистратора, которая может просматривать и обновлять информацию регистратора, включая уровни журнала.
Просмотр информации о регистраторе с указанным именем
[arthas@2062]$ logger -n org.springframework.web
name org.springframework.web
class ch.qos.logback.classic.Logger
classLoader sun.misc.Launcher$AppClassLoader@2a139a55
classLoaderHash 2a139a55
level null
effectiveLevel INFO
additivity true
codeSource file:/Users/hengyunabc/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
обновить уровень логгера
[arthas@2062]$ logger --name ROOT --level debug
update logger level success.
Просто используйте одну команду, чтобы изменить уровень журнала машины.
Однако в настоящее время Arthas не особенно дружит с поддержкой кластеров.Хотя он поддерживает удаленное управление/подключение нескольких Агентов через Arthas Tunnel Server/Client, он не очень удобен в использовании, а требования к использованию команд относительно высоки.
Кроме того, в нашей системе используется инструмент, облегчающий нам динамическую настройку уровня лога через план в период действия акции, в связи с этим использовать Arthas не очень удобно.
Код
Функция этого инструмента, который я написал, очень проста, то есть он предоставляет запись для динамического изменения уровня журнала, чтобы пользователи могли динамически изменять уровень.
А для удобства использования я инкапсулировал его в Spring Boot Starter, и напрямую подключил к внутреннему конфигурационному центру компании, который может легко модифицировать уровень лога одним кликом через конфигурационный центр.
Во-первых, давайте взглянем на основную функцию, которая является частью динамического изменения уровня журнала.Код выглядит следующим образом:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggerConfiguration;
import org.springframework.boot.logging.LoggingSystem;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.springframework.boot.logging.LoggingSystem.ROOT_LOGGER_NAME;
/**
* 日志级别设置服务类
*
* @author Hollis
*/
public class LoggerLevelSettingService {
@Autowired
private LoggingSystem loggingSystem;
private static final Logger LOGGER = LoggerFactory.getLogger(LoggerLevelSettingService.class);
public void setRootLoggerLevel(String level) {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(ROOT_LOGGER_NAME);
if (loggerConfiguration == null) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("no loggerConfiguration with loggerName " + level);
}
return;
}
if (!supportLevels().contains(level)) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + level);
}
return;
}
if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("setRootLoggerLevel success,old level is '" + loggerConfiguration.getEffectiveLevel()
+ "' , new level is '" + level + "'");
}
loggingSystem.setLogLevel(ROOT_LOGGER_NAME, LogLevel.valueOf(level));
}
}
private List<String> supportLevels() {
return loggingSystem.getSupportedLogLevels().stream().map(Enum::name).collect(Collectors.toList());
}
}
Приведенный выше код предназначен для изменения уровня вывода журнала ROOT приложения в соответствии с уровнем, переданным пользователем.
Здесь используется ключевой сервис:org.springframework.boot.logging.LoggingSystem
Этот сервис является абстракцией SpringBoot системы ведения журналов и представляет собой абстрактный класс верхнего уровня. У него много конкретных реализаций:

Из приведенного выше рисунка видно, что SpringBoot в настоящее время поддерживает 4 типа журналов, а именно встроенный журнал JDK (JavaLoggingSystem) и Log4j (Log4JLoggingSystem), Log4j2 (Log4J2LoggingSystem) и Logback (LogbackLoggingSystem).
LoggingSystem — это абстрактный класс со следующими методами внутри:
- Метод beforeInitialize: вещи, которые необходимо обработать перед инициализацией системы журналов. Абстрактный метод, разные архитектуры журналов обрабатываются по-разному
- Метод инициализации: Инициализировать систему ведения журнала. По умолчанию обработка не выполняется, и для инициализации требуются подклассы.
- Метод cleanUp: очистка лог-системы. По умолчанию обработка не выполняется, и для очистки требуются подклассы.
- Метод getShutdownHandler: возвращает Runnable для обработки операций, которые необходимо выполнить после закрытия системы ведения журнала при выходе из jvm.По умолчанию он возвращает null, то есть ничего не делает.
- Метод setLogLevel: абстрактный метод, используемый для установки уровня соответствующего регистратора.
Когда SpringBoot запустится, он завершит инициализацию LoggingSystem.Эта часть кода реализована в LoggingApplicationListener:
/**
* 执行LoggingSystem初始化的前置操作
*/
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
//获取LoggingSystem的真实实现,
// 此处会根据不同的日志框架获取不同的实现,
// logback : LogbackLoggingSystem
// log4j2: Log4J2LoggingSystem
// javalog: JavaLoggingSystem
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
//执行beforeInitialize方法完成初始化前置操作
this.loggingSystem.beforeInitialize();
}
С помощью LoggingSystem мы можем динамически изменять уровень журнала через него. Он помог нам защитить основную структуру ведения журналов.
В дополнение к поддержке изменения журнала уровня ROOT, он также может поддерживать изменение пользовательского уровня журнала.
Сначала определите LoggerConfig для инкапсуляции конфигурации журнала:
/**
* the config of logger
*
* @author Hollis
*/
public class LoggerConfig {
/**
* the name of the logger
*/
private String loggerName;
/**
* the log level
*
* @see LogLevel
*/
private String level;
public String getLoggerName() {
return loggerName;
}
public void setLoggerName(String loggerName) {
this.loggerName = loggerName;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
}
Затем предоставьте метод для динамического изменения уровня журнала, код реализован следующим образом:
public void setLoggerLevel(List<LoggerConfig> configList) {
Optional.ofNullable(configList).orElse(Collections.emptyList()).forEach(
config -> {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(config.getLoggerName());
if (loggerConfiguration == null) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("no loggerConfiguration with loggerName " + config.getLoggerName());
}
return;
}
if (!supportLevels().contains(config.getLevel())) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + config.getLevel());
}
return;
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("setLoggerLevel success for logger '" + config.getLoggerName() + "' ,old level is '"
+ loggerConfiguration.getEffectiveLevel()
+ "' , new level is '" + config.getLevel() + "'");
}
loggingSystem.setLogLevel(config.getLoggerName(), LogLevel.valueOf(config.getLevel()));
}
);
}
Выше, в соответствии с LoggerConfig, переданным пользователем, измените loggerLevel, соответствующий указанному loggerName. Что касается происхождения LoggerLevel, его можно передать через конфигурацию, например, при анализе конфигурации в формате JSON или файлов YML.
Например, мы можем использовать следующую конфигурацию в центре конфигурации для управления уровнем журнала и push:
[{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'WARN'}]
Приведенная выше конфигурация сделает loggerName какcom.hollis.degradation.core.logger.LoggerLevelSettingService
Уровень журнала динамически изменяется на WARN, кроме того, если информация о конфигурации выглядит следующим образом:
[{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}]
Тогда это будетcom.hollis.degradation.core.logger
Все уровни вывода журнала класса LoggerName в этом пути к пакету динамически изменяются на WARN.
Конечно, эта конфигурация также поддерживает настройку нескольких уровней Logger, если это следующая конфигурация:
[
{'loggerName':'com.hollis.degradation.core.logger','level':'WARN'}
,{'loggerName':'com.hollis.degradation.core.logger.LoggerLevelSettingService','level':'INFO'}
]
В коде соединения есть несколько журналов, и методы их определения
private static final Logger LOGGER1 = LoggerFactory.getLogger(LoggerLevelSettingService.class);
private static final Logger LOGGER2 = LoggerFactory.getLogger(TestService.class);
private static final Logger LOGGER3 = LoggerFactory.getLogger(DebugService.class);
Затем, после того, как конфигурация вступит в силу, выходной уровень вышеуказанного LOGGER1 будет INFO, а уровень LOGGER2 и LOGGER3 будет WARN.
Кроме того, вышеуказанная модификация уровня журнала может повлиять на вывод журнала самого нашего собственного инструмента, поэтому мы предоставляем метод для прямого изменения уровня журнала нашей собственной службы журнала:
public void setDegradationLoggerLevel(String level) {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(
this.getClass().getName());
if (loggerConfiguration == null) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("no loggerConfiguration with loggerName " + level);
}
return;
}
if (!supportLevels().contains(level)) {
if (LOGGER.isErrorEnabled()) {
LOGGER.error("current Level is not support : " + level);
}
return;
}
if (!loggerConfiguration.getEffectiveLevel().equals(LogLevel.valueOf(level))) {
loggingSystem.setLogLevel(this.getClass().getName(), LogLevel.valueOf(level));
}
}
С помощью приведенного выше класса LoggerLevelSettingService у вас есть возможность динамически изменять журнал.Следующий шаг — найти способ динамического изменения уровня журнала через центр конфигурации.
Поскольку использование разных центров конфигурации различно, я просто возьму наш собственный центр конфигурации в качестве простого примера:
/**
* 降级开关注册器
*
* @author Hollis
*/
public class DegradationSwitchInitializer implements Listener, InitializingBean {
//从配置项中读取应用名,方便注册到配置中心
@Value("${project.name}")
private String appName;
@Autowired
private LoggerLevelSettingService loggerLevelSettingService;
//配置中心值发生变化会自动回调该方法
@Override
public void valueChange(String appName, String nameSpace, String name,
String value) {
if (name.equals(rootLogLevel.name())) {
loggerLevelSettingService.setRootLoggerLevel(value);
}
if (name.equals(logLevelConfig.name())) {
List<LoggerConfig> loggerConfigs = JSON.parseArray(value, LoggerConfig.class);
loggerLevelSettingService.setLoggerLevel(loggerConfigs);
}
//将降级工具的日志输出级别设置成INFO,保证其日志可以正常输出
loggerLevelSettingService.setDegradationLoggerLevel("INFO");
}
@Override
public void afterPropertiesSet() {
//将服务配置到配置中心
ConfigCenterManager.addListener(this);
ConfigCenterManager.init(appName, DegradationConfig.class);
}
}
Выше мы реализовали отслеживание изменений значения центра конфигурации и динамическое изменение уровня журнала.
Основные функции завершены.Далее мы можем рассмотреть, как разрешить другим приложениям быстрый доступ, то есть определить Starter, который может облегчить и быстро получить доступ. Основной код выглядит следующим образом:
Сначала определите класс конфигурации:
/**
* @author Hollis
*/
@Configuration
@ConditionalOnProperty(prefix = "hollis.degradation", name = "enable", havingValue = "true")
public class HollisDegradationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "project.name")
public LoggerLevelSettingService loggerLevelSettingService() {
return new LoggerLevelSettingService();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(value = LoggerLevelSettingService.class)
public DegradationSwitchInitializer degradationSwitchInitializer() {
return new DegradationSwitchInitializer();
}
}
В этом классе определены два bean-компонента, и предпосылка определения bean-компонента заключается в том, что в приложении настроены следующие два элемента конфигурации:
hollis.degradation.enable = true
project.name = test
Следующим шагом является определение файла spring.factories со следующими определениями:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hollis.degradation.starter.autoconfiguration.HollisDegradationAutoConfiguration
Выше нам нужно только ввести наш стартер в приложение, которому нужно ввести инструмент понижения версии, и настроить два элемента конфигурации.
После доступа удобно динамически изменять уровень вывода журнала отдельной машины или кластера в центре конфигурации, и его можно настроить на платформе плана в течение большого периода продвижения, и его можно быстро выполнить с помощью аварийных планов.
Суммировать
В вышеизложенном в основном были реализованы многие основные функции, и основными факторами, которые необходимо учитывать при реализации, являются следующие:
- 1. Генеральный. Для одновременной поддержки разных фреймворков журналов используемый клиентом каркас журналов не влияет на наши функции, и клиенту не нужно заботиться о разнице между его собственными каркасами журналов.
- 2. Настраиваемый. Информацию о конфигурации можно передать через внешний центр конфигурации, и можно быстро внести коррективы.
- 3. Простота в использовании. Инкапсулируя его в SpringBoot Starter, клиенты могут быстро получить к нему доступ.
- 4. Неинвазивный. Использование фреймворков не должно влиять на нормальную работу приложения.
Конечно, на разработку этого инструмента у меня ушло всего несколько часов, и в нем все еще много недостатков.На самом деле, есть еще много вещей, которые можно оптимизировать, например, формат конфигурации может поддерживаться в различных форматах, а конфигурацию журнала можно просмотреть через EndPoint Не реализовано.
Эта статья дает только идею, я надеюсь, что каждый сможет научиться решать проблемы, возникающие в повседневной работе, с помощью инструментов и научиться строить колеса.
Об авторе:Hollis, человек с уникальным увлечением программированием, технический эксперт Alibaba, соавтор «Трех курсов для программистов» и автор серии статей «Дорога к Java-инженерам».
Если у вас есть комментарии, предложения или вы хотите пообщаться с автором, вы можете подписаться на официальный аккаунт [Hollis], оставьте мне сообщение прямо в фоновом режиме.