1. 程式人生 > >spring-boot推送實時日誌到前端頁面顯示

spring-boot推送實時日誌到前端頁面顯示

網上有很多後臺推送日誌到前端頁面的例子,這裡我也借鑑了別人的做法 稍加改進一下。以前做前端頁面顯示日誌一般都會想到ajax輪詢去做,這樣太耗費伺服器資源了,效能也很差。使用長連線來做更穩妥,這就想到了用websocket。剛好spring-boot對websocket支援非常不錯,使用很方便,接下來開整。

簡單的spring-boot工程這裡就不做過多的講解了,只敘述核心部分

首先匯入依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

首先在專案的日誌配置檔案中新增/修改此節點配置

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--encoder 預設配置為PatternLayoutEncoder-->
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <!--<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level -&#45;&#45; [%thread] %logger Line:%-3L - %msg%n</pattern>-->
            <charset>UTF-8</charset>
        </encoder>
        <!--此日誌appender是為開發使用,只配置最底級別,控制檯輸出的日誌級別是大於或等於此級別的日誌資訊-->
        <filter class="com.xcloud.api.system.core.LogFilter"></filter>
</appender>

此節點配置主要是為了把控制檯輸出的日誌交給我們自己的日誌過濾器進行處理(此節點不要指定日誌等級,記得<appender-ref ref="STDOUT" />)

接下來實現自己的日誌過濾器


import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import com.xcloud.api.system.entity.LoggerMessage;
import org.springframework.stereotype.Service;

import java.text.DateFormat;
import java.util.Date;

/**
 * Xcloud-Api By IDEA
 * Created by LaoWang on 2018/8/25.
 */
@Service
public class LogFilter extends Filter<ILoggingEvent> {

    @Override
    public FilterReply decide(ILoggingEvent event) {
        String exception = "";
        IThrowableProxy iThrowableProxy1 = event.getThrowableProxy();
        if(iThrowableProxy1!=null){
                exception = "<span class='excehtext'>"+iThrowableProxy1.getClassName()+" "+iThrowableProxy1.getMessage()+"</span></br>";
            for(int i=0; i<iThrowableProxy1.getStackTraceElementProxyArray().length;i++){
                exception += "<span class='excetext'>"+iThrowableProxy1.getStackTraceElementProxyArray()[i].toString()+"</span></br>";
            }
        }
        LoggerMessage loggerMessage = new LoggerMessage(
                event.getMessage()
                , DateFormat.getDateTimeInstance().format(new Date(event.getTimeStamp())),
                event.getThreadName(),
                event.getLoggerName(),
                event.getLevel().levelStr,
                exception,
                ""
        );
        LoggerQueue.getInstance().push(loggerMessage);
        return FilterReply.ACCEPT;
    }
}

這部分程式碼網上大部分都是一樣的,但是並沒有對異常具體資訊進行處理,所以這裡我把具體異常資訊日誌也新增進去了

建立一個阻塞佇列,作為日誌系統輸出的日誌的一個臨時載體

import com.xcloud.api.system.entity.LoggerMessage;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;


public class LoggerQueue {
    //佇列大小
    public static final int QUEUE_MAX_SIZE = 10000;
    private static LoggerQueue alarmMessageQueue = new LoggerQueue();
    //阻塞佇列
    private BlockingQueue blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);

    private LoggerQueue() {
    }

    public static LoggerQueue getInstance() {
        return alarmMessageQueue;
    }

    /**
     * 訊息入隊
     *
     * @param log
     * @return
     */
    public boolean push(LoggerMessage log) {
        return this.blockingQueue.add(log);//佇列滿了就丟擲異常,不阻塞
    }

    /**
     * 訊息出隊
     *
     * @return
     */
    public LoggerMessage poll() {
        LoggerMessage result = null;
        try {
            result = (LoggerMessage) this.blockingQueue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }
}

建立一個日誌實體(這裡我使用了lombok,如果沒用的話,請自行生成get,set和全引數構造方法)

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * 日誌訊息實體
 */
@Getter
@Setter
@ToString
@AllArgsConstructor
public class LoggerMessage {
    private String body;
    private String timestamp;
    private String threadName;
    private String className;
    private String level;
    private String exception;
    private String cause;
}

接下來配置WebSocket

import com.xcloud.api.system.core.LoggerQueue;
import com.xcloud.api.system.entity.LoggerMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import javax.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Xcloud-Api By IDEA
 * 配置WebSocket訊息代理端點,即stomp服務端
 * 為了連線安全,setAllowedOrigins設定的允許連線的源地址
 * 如果在非這個配置的地址下發起連線會報403
 * 進一步還可以使用addInterceptors設定攔截器,來做相關的鑑權操作
 * Created by LaoWang on 2018/8/25.
 */
@Slf4j
@Configuration
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    /**
     * 推送日誌到/topic/pullLogger
     */
    @PostConstruct
    public void pushLogger(){
        ExecutorService executorService= Executors.newFixedThreadPool(2);
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        LoggerMessage log = LoggerQueue.getInstance().poll();
                        if(log!=null){
                            if(messagingTemplate!=null)
                                messagingTemplate.convertAndSend("/topic/pullLogger",log);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        executorService.submit(runnable);
        executorService.submit(runnable);
    }
}

如果專案配置有攔截器獲取其他許可權控制的,請開放/websocket

前端頁面:這裡我使用了原生swagger+layer,請自行根據自己頁面進行配置

配置一個按鈕點選彈出layer彈窗,日誌顏色,具體的一些樣式可根據自己喜好選擇

$(".log").click(function () {
        //iframe層
        layer.open({
            type: 1,
            title: '<span class="laytit">介面實時日誌</span>',
            shadeClose: false,
            shade: 0.7,
            maxmin: true,
            area: ['80%', '70%'],
            content: $("#logdiv").html(), //iframe的url
            cancel: function(index){
                closeSocket();
            }
        });
    });

    <!-- 日誌實時推送業務處理 -->
    var stompClient = null;

    function openSocket() {
        if (stompClient == null) {
            if($("#log-container").find("span").length==0){
                $("#log-container div").after("<span>通道連線成功,靜默等待.....</span><img src='images/loading.gif'>");
            }
            var socket = new SockJS('websocket?token=kl');
            stompClient = Stomp.over(socket);
            stompClient.connect({token: "kl"}, function (frame) {
                stompClient.subscribe('/topic/pullLogger', function (event) {
                    var content = JSON.parse(event.body);
                    var leverhtml = '';
                    var className = '<span class="classnametext">' + content.className + '</span>';
                    switch (content.level) {
                        case 'INFO':
                            leverhtml = '<span class="infotext">' + content.level + '</span>';
                            break;
                        case 'DEBUG':
                            leverhtml = '<span class="debugtext">' + content.level + '</span>';
                            break;
                        case 'WARN':
                            leverhtml = '<span class="warntext">' + content.level + '</span>';
                            break;
                        case 'ERROR':
                            leverhtml = '<span class="errortext">' + content.level + '</span>';
                            break;
                    }
                    $("#log-container div").append("<p class='logp'>" + content.timestamp + " " + leverhtml + " --- [" + content.threadName + "] " + className + " :" + content.body + "</p>");
                    if (content.exception != "") {
                        $("#log-container div").append("<p class='logp'>" + content.exception + "</p>");
                    }
                    if (content.cause != "") {
                        $("#log-container div").append("<p class='logp'>" + content.cause + "</p>");
                    }
                    $("#log-container").scrollTop($("#log-container div").height() - $("#log-container").height());
                }, {
                    token: "kltoen"
                });
            });
        }
    }

    function closeSocket() {
        if (stompClient != null) {
            stompClient.disconnect();
            stompClient = null;
        }
    }

最後來一個效果圖(圖中日誌等級動態配置將在下篇部落格說明)有什麼不對的地方歡迎大牛們評論!!!