錯誤日誌告警實戰

1. 錯誤日誌告警實戰

1.1. 需求

爲了更方便的實時瞭解系統報錯狀況,我開始尋找告警解決方案html

1.2. 思路

1.2.1. 不差錢的方案

若是不差錢,更系統更完善的解決方案,我首先想到的是CAT,它不但能實現錯誤告警,且更加智能,告警的錯誤間隔,錯誤告警內容,QPS告警等等方式更多樣化,還能查看接口QPS流量等等,奈何經費有限,放棄java

1.2.2. 考慮本身實現

  1. 本身實現考慮能否對log.error方法進行攔截,因而各類找logback是否提供了攔截器過濾器等等,後查到官網發現logback自己提供了appender到郵件的方式,很是棒直接集成

1.3. 配置文件

pomspring

<dependency>
    <groupId>org.codehaus.janino</groupId>
    <artifactId>janino</artifactId>
    <version>2.7.8</version>
</dependency>
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.7</version>
</dependency>
複製代碼
<configuration>
    <contextName>logback</contextName>
    <!--配置文件中參數-->
    <springProperty scope="context" name="applicationName" source="spring.application.name"/>
    <springProperty scope="context" name="alertEmail" source="onegene.alert.email"/>
    <springProperty scope="context" name="profile" source="spring.profiles.active"/>
    <!--  郵件 -->
    <!-- SMTP server的地址,必需指定。如網易的SMTP服務器地址是: smtp.163.com -->
    <property name="smtpHost" value="hwhzsmtp.qiye.163.com"/><!--填入要發送郵件的smtp服務器地址(問DBA或者經理啥的就知道)-->
    <!-- SMTP server的端口地址。默認值:25 -->
    <property name="smtpPort" value="465"/>
    <!-- 發送郵件帳號,默認爲null -->
    <property name="username" value="xxxx@163.com.cn"/><!--發件人帳號-->
    <!-- 發送郵件密碼,默認爲null -->
    <property name="password" value="rVgkwPL4WsWmGV72"/><!--發件人密碼-->
    <!-- 若是設置爲true,appender將會使用SSL鏈接到日誌服務器。默認值:false -->
    <property name="SSL" value="true"/>
    <!-- 指定發送到那個郵箱,可設置多個<to>屬性,指定多個目的郵箱 -->
    <property name="email_to" value="${alertEmail}"/><!--收件人帳號多個能夠逗號隔開-->
    <!-- 指定發件人名稱。若是設置成「&lt;ADMIN&gt; 」,則郵件發件人將會是「<ADMIN> 」 -->
    <property name="email_from" value="xxxx@163.com"/>
    <!-- 指定emial的標題,它須要知足PatternLayout中的格式要求。若是設置成「Log: %logger - %msg 」,就案例來說,則發送郵件時,標題爲「【Error】: com.foo.Bar - Hello World 」。 默認值:"%logger{20} - %m". -->
    <property name="email_subject" value="【${applicationName}:${profile}:Error】: %logger"/>
    <!-- ERROR郵件發送 -->
    <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
        <smtpHost>${smtpHost}</smtpHost>
        <smtpPort>${smtpPort}</smtpPort>
        <username>${username}</username>
        <password>${password}</password>
        <asynchronousSending>true</asynchronousSending>
        <SSL>${SSL}</SSL>
        <to>${email_to}</to>
        <from>${email_from}</from>
        <subject>${email_subject}</subject>
        &emsp;&emsp;&emsp;&emsp; <!-- html格式-->
        <layout class="ch.qos.logback.classic.html.HTMLLayout">
            <Pattern>%date%level%thread%logger{0}%line%message</Pattern>
        </layout>
        &emsp;&emsp;&emsp;&emsp; <!-- 這裏採用等級過濾器 指定等級相符才發送 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
            <!-- 每一個電子郵件只發送一個日誌條目 -->
            <bufferSize>1</bufferSize>
        </cyclicBufferTracker>
    </appender>

    <!-- name的值是變量的名稱,value的值時變量定義的值。經過定義的值會被插入到logger上下文中。定義變量後,可使「${}」來使用變量。 -->
    <property name="log.path" value="log"/>

    <!-- 彩色日誌 -->
    <!-- 彩色日誌依賴的渲染類 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日誌格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


    <!--輸出到控制檯-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日誌appender是爲開發使用,只配置最底級別,控制檯輸出的日誌級別是大於或等於此級別的日誌信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 設置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--輸出到文件-->
    <!-- 時間滾動輸出 level爲 DEBUG 日誌 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在記錄的日誌文件的路徑及文件名 -->
        <file>${log.path}/${applicationName}-log.log</file>
        <!--日誌文件輸出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 設置字符集 -->
        </encoder>
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日誌歸檔 -->
            <fileNamePattern>${log.path}/${applicationName}-log-%d{yyyyMMdd}.log.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日誌文件保留天數-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
    </appender>
    <logger name="com.onegene.platform" level="debug"/>
    <logger name="com.onegene.platform.auth.authority" level="info"/>
    <logger name="org.springframework.security.oauth2.provider.endpoint" additivity="false"/>
    <springProfile name="local">
        <root level="info">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="DEBUG_FILE"/>
        </root>
    </springProfile>
    <springProfile name="dev,pro">
        <root level="info">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="DEBUG_FILE"/>
            <appender-ref ref="EMAIL"/>
        </root>
    </springProfile>
</configuration>
複製代碼

1.4. 配置文件解讀

  1. 配置文件的重點
<springProperty scope="context" name="applicationName" source="spring.application.name"/>
    <springProperty scope="context" name="alertEmail" source="onegene.alert.email"/>
    <springProperty scope="context" name="profile" source="spring.profiles.active"/>
複製代碼
  1. 我已經把大多可抽出的可變參數拉出來了,該配置文件能夠直接放入任意工程,日誌名稱隨bootstrap.ymlspring.application.name參數變更
  2. 告警發送郵件人也可在配置文件中配置,這裏注意onegene.alert.emailspring.application.name參數都最好在bootstrap.yml中配置,而不是application.yml,由於bootstrap.yml的讀取優先級高於application.yml,不然可能讀不到這兩個參數

UTOOLS1586500939194.png

到這一步,只要咱們打印log.error日誌就會把錯誤日誌都發到指定郵件上了,但這樣確定還不夠,咱們須要配合@ControllerAdvice能夠作到只要報異常,就能夠統一進行日誌郵件發送,同時咱們又會有特殊的需求,好比個別的錯誤日誌頻繁且不可避免,並且不須要處理,那麼咱們能夠稍稍作些擴展,定義個接口注入,在業務代碼中去處理是否不須要發送錯誤郵件bootstrap

1.5. 代碼

  1. 異常處理
@ControllerAdvice
@Slf4j
public class SystemExceptionHandler {

    @Autowired(required = false)
    private IExceptionFilter exceptionFilter;

    @ExceptionHandler(value = {DuplicateUniqueException.class, DuplicateKeyException.class})
    @ResponseBody
    public Result duplicateUniqueExceptionExceptionHandler(HttpServletRequest request, Exception e) {
        return getExceptionResult(e, StatusCode.FAILURE_SYSTEM_CODE, "惟一主鍵重複(或聯合惟一鍵)", false);
    }

    @ExceptionHandler(value = {FeignException.class, RuntimeException.class})
    @ResponseBody
    public Result FeignExceptionHandler(HttpServletRequest request, Exception e) throws Exception {
        throw e;
    }

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result commonExceptionHandler(HttpServletRequest request, Exception e) {
        return getExceptionResult(e, StatusCode.FAILURE_CODE, true);
    }


    private Result getExceptionResult(Exception e, int statusCode, boolean ignoreAlert) {
        return getExceptionResult(e, statusCode, e.getMessage(), ignoreAlert);
    }

    private Result getExceptionResult(Exception e, int statusCode, String msg, boolean ignoreAlert) {
        e.printStackTrace();
        String exceptionName = ClassUtils.getShortName(e.getClass());
        StackTraceElement[] stackTrace = e.getStackTrace();
        StringBuilder sb = new StringBuilder();
        for (StackTraceElement stackTraceElement : stackTrace) {
            sb.append(stackTraceElement.toString()).append("\n");
        }
        String message = e.getMessage();
        if (ignoreAlert && filter(e)) {
            log.error("ExceptionName ==> {},message:{},detail:{}", exceptionName, message, sb.toString());
        }
        return Result.failure(statusCode, msg);
    }

    private boolean filter(Exception e) {
        if (exceptionFilter != null) {
            return exceptionFilter.filter(e);
        }
        return true;
    }
}
複製代碼

接口很簡單bash

public interface IExceptionFilter {
    boolean filter(Exception e);
}
複製代碼

對於不須要處理的異常這裏處理服務器

/**
 * @author: laoliangliang
 * @description: 過濾不須要報警的異常
 * @create: 2020/4/9 10:00
 **/
@Component
@Slf4j
public class FilterAlert implements IExceptionFilter {
    @Override
    public boolean filter(Exception e) {
        if (e instanceof ConnectException) {
            return false;
        }
        return true;
    }
}

複製代碼

1.6. 總結

  1. 至此已經徹底實現錯誤告警方案,後續就是優化工做了,實現效果以下

錯誤郵件列表 app

UTOOLS1586502074518.png

錯誤郵件內容 async

UTOOLS1586502211700.png
相關文章
相關標籤/搜索