当前位置:首页 > 生活百科

linux查看实时日志命令(查看日志的三种命令分享)

栏目:生活百科日期:2025-01-21浏览:0

前言

最近在做一个小工具,有个需求是在Web端能实时查看日志文件,也就是相当于在终端执行tail -f命令,对此没有找到好的解决方式,一开始想得直接通过FileInputStream来读取,因为他也能直接跳过n个字节来读取,就像下面这样。

public static void main(String&[] args) throws Exception {    File file = new File("/home/1.txt");    FileInputStream fin = new FileInputStream(file);    int ch;    fin.skip(10);    while ((ch = fin.read()) != -1){      System.out.print((char) ch);    }  }

如果不跳过的话,那么每次读取全部内容并展示显然不现实,我们要做的是像tail一样,每次从后n行开始读取,并且会持续输出最新的行。

还有一个问题就是对文件的变化要能感知到,所以最后选择直接调用tail命令,并且通过WebSocket输出到网页上。

tail用法

在java中调用tail命令后,拿到它的输入流并且包装成BufferedReader,如果通过readLine()读取不到数据,那么他会一直阻塞,并不会返回null,这也就代表日志文件中暂时还没有新数据写入,一旦readLine()方法返回,那么就代表有新数据到达了。另外一个问题就是如何终止,我们不可能让他一直读取,要在一个合适的时间终止,答案就是在WebSocket断开连接时,并且Process类提供了destroy()方法用来终止这个进程,相当于按下了Ctrl+C

 public static void main(String&[] args) throws Exception {     Process exec = Runtime.getRuntime().exec(new String&[]{"bash", "-c", "tail -F /home/HouXinLin/test.txt"});     InputStream inputStream = exec.getInputStream();     BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));     for (;;){         System.out.println(bufferedReader.readLine()+"r");     } }

实现过程

在Spring Boot中加入WebSocket功能有很多方式,目前感觉普遍的文章都是介绍以ServerEndpointExporter、@OnOpen、 @OnClose、@OnMessage这种方式来实现的,这种方式需要声明一个Bean,也就是ServerEndpointExporter,但是我记得如果要打包成war放入Tomcat中运行时,还需要把这个Bean取消掉,否则还会报错,非常的麻烦,当然也有办法解决。

还有其他集成的办法,比如实现WebSocketConfigurer或者
WebSocketMessageBrokerConfigurer接口,而我目前采用的是实现WebSocketMessageBrokerConfigurer接口,并且前端还需要两个库,SockJS和Stomp(更具选择,也可以不使用)。

SockJS提供类似于WebSocket的对象,还有一套跨浏览器的API,可以在浏览器和Web服务器之间创建了低延迟,全双工,跨域的通信通道,如果浏览器不支持 WebSocket,它还可以模拟对WebSocket的支持。

Stomp即Simple Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。

首先看一下连接处理层的逻辑,其中一部分非必要的代码就不展示了。

@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {    private static final Logger log = LoggerFactory.getLogger(WebSocketConfig.class.getName());    @Autowired    SimpMessagingTemplate mSimpMessagingTemplate;    @Autowired    WebSocketManager mWebSocketManager;    @Autowired    TailLog mTailLog;    @Override    public void configureMessageBroker(MessageBrokerRegistry registry) {        registry.enableSimpleBroker("/topic/path");    }    @Override    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {            @Override            public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {                return new WebSocketHandlerDecorator(webSocketHandler) {                    @Override                    public void afterConnectionEstablished(WebSocketSession session) throws Exception {                        log.info("日志监控WebSocket连接,sessionId={}", session.getId());                        mWebSocketManager.add(session);                        super.afterConnectionEstablished(session);                    }                    @Override                    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {                        mWebSocketManager.remove(session.getId());                        super.afterConnectionClosed(session, closeStatus);                    }                };            }        });    }    @Override    public void registerStompEndpoints(StompEndpointRegistry registry) {        registry.addEndpoint("/socket-log")                .addInterceptors(new HttpHandshakeInterceptor())                .setHandshakeHandler(new DefaultHandshakeHandler() {                    @Override                    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map&<String, Object&> attributes) {                        return new StompPrincipal(UUID.randomUUID().toString());                    }                })                .withSockJS();    }    @EventListener    public void handlerSessionCloseEvent(SessionDisconnectEvent sessionDisconnectEvent) {        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(sessionDisconnectEvent.getMessage());        mTailLog.stopMonitor(headerAccessor.getSessionId());    }    /**     * 路径订阅     *     * @param sessionSubscribeEvent     */    @EventListener    public void handlerSessionSubscribeEvent(SessionSubscribeEvent sessionSubscribeEvent) {        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(sessionSubscribeEvent.getMessage());        if (mTailLog.isArriveMaxLog()) {            mWebSocketManager.sendMessage(headerAccessor.getSessionId(), "监控数量已经达到限制,无法查看"");            log.info("日志监控WebSocket连接已经到达最大数量,将断开sessionId={}", headerAccessor.getSessionId());            mWebSocketManager.close(headerAccessor.getSessionId());            return;        }        String destination = headerAccessor.getDestination();        String userId = headerAccessor.getUser().getName();        if (destination.startsWith("/user/topic/path")) {            String path = destination.substring("/user/topic/path".length());            File file = new File(StringUtils.urlDecoder(path));            if (!file.exists()) {                mWebSocketManager.sendMessage(headerAccessor.getSessionId(), "what are you 弄啥嘞,文件找不到啊");                mWebSocketManager.close(headerAccessor.getSessionId());                return;            }            TailLogListenerImpl tailLogListener = new TailLogListenerImpl(mSimpMessagingTemplate, userId);            mTailLog.addMonitor(new LogMonitorObject(file.getName(), file.getParent(),                    tailLogListener, "" + headerAccessor.getSessionId(), userId));        }    }}

对于上面的几个接口可能没使用过他的人有点蒙,至少我在学习他的时候是这样的,看上面的代码,我们先要理清逻辑,才能明白为什么要这样写。

实现registerStompEndpoints方法

首先是
WebSocketMessageBrokerConfigurer接口,Spring Boot提供的一个WebSocket配置接口,只需要简简单单地配置两下,就可以实现一个WebSocket程序,这个接口中有8个方法,而我们只需要用到三个个。

然后就是给出前端连接WebSocket所需要的地址,如果连连接地址都不给,后面步骤怎么继续?这个就是通过实现registerStompEndpoints方法来完成,只需要向StompEndpointRegistry中通过addEndpoint添加一个新的&”连接点&”就可以,还可以设置拦截器,也就是在前端试图连接的时候,如果后端发现这个连接不对劲,有猫腻,可以拒绝和他连接,这步可以通过addInterceptors来完成。

切记如果使用了SocketJs库,那么一定要加入withSockJS。

@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {    registry.addEndpoint("/log")            .addInterceptors(new HttpHandshakeInterceptor())            .setHandshakeHandler(new DefaultHandshakeHandler() {                @Override                protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map&<String, Object&> attributes) {                    return new StompPrincipal(UUID.randomUUID().toString());                }            })            .withSockJS();}

保存SessionId和WebSocketSession对应关系

这一步是为了方便管理,比如主动断开连接,需要实现
configureWebSocketTransport接口,但是这里的SessionId并不是服务端生成的会话ID,而是这个WebSocket的会话ID,每个WebSocket连接都是不同的。

这里主要考虑到如果前端传过来的文件不存在,那么服务端要能主动断开连接。

@Overridepublic void configureWebSocketTransport(WebSocketTransportRegistration registration) {    registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {        @Override        public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {            return new WebSocketHandlerDecorator(webSocketHandler) {                @Override                public void afterConnectionEstablished(WebSocketSession session) throws Exception {                    log.info("日志监控WebSocket连接,sessionId={}", session.getId());                    mWebSocketManager.add(session);                    super.afterConnectionEstablished(session);                }                @Override                public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {                    mWebSocketManager.remove(session.getId());                    super.afterConnectionClosed(session, closeStatus);                }            };        }    });}

监听订阅

接着前端通过Stomp的API来订阅一个消息,那么我们怎么接收订阅的事件呢?就是通过 @EventListener注解来接收SessionSubscribeEvent事件。

而前端订阅时就需要传入要监控的日志路径。这时候我们就能拿到这个WebSocket要监听的日志路径了。

@EventListenerpublic void handlerSessionSubscribeEvent(SessionSubscribeEvent sessionSubscribeEvent) {    ....}

开启tail进程

接着我们要为每个WebSocket都开启一个线程,用来执行tail命令。

@Componentpublic class TailLog {    public static final int MAX_LOG = 3;    private List&<LogMonitorExecute&> mLogMonitorExecutes = new CopyOnWriteArrayList&<&>();    /**     * Log线程池     */    private ExecutorService mExecutors = Executors.newFixedThreadPool(MAX_LOG);    public void addMonitor(LogMonitorObject object) {        LogMonitorExecute logMonitorExecute = new LogMonitorExecute(object);        mExecutors.execute(logMonitorExecute);        mLogMonitorExecutes.add(logMonitorExecute);    }    public void stopMonitor(String sessionId) {        if (sessionId == null) {            return;        }        for (LogMonitorExecute logMonitorExecute : mLogMonitorExecutes) {            if (sessionId.equals(logMonitorExecute.getLogMonitorObject().getSessionId())) {                logMonitorExecute.stop();                mLogMonitorExecutes.remove(logMonitorExecute);            }        }    }    public boolean isArriveMaxLog() {        return mLogMonitorExecutes.size() == MAX_LOG;    }}

最终执行者,其中的stop()方法是在WebSocket断开连接时执行的。那么需要事先保存好sessionId和LogMonitorExecute的对应关系。当文件有新变化时,发送给对应的WebSocket。

public class LogMonitorExecute implements Runnable {    private static final Logger log = LoggerFactory.getLogger(LogMonitorExecute.class.getName());    /**     * 监控的对象     */    private LogMonitorObject mLogMonitorObject;    private volatile boolean isStop = false;    /**     * tail 进程对象     */    private Process mProcess;    public LogMonitorExecute(LogMonitorObject logMonitorObject) {        mLogMonitorObject = logMonitorObject;    }    public LogMonitorObject getLogMonitorObject() {        return mLogMonitorObject;    }    @Override    public void run() {        try {            String path = Paths.get(mLogMonitorObject.getPath(), mLogMonitorObject.getName()).toString();            log.info("{}对{}开始进行日志监控", mLogMonitorObject.getSessionId(), path);            mProcess = Runtime.getRuntime().exec(new String&[]{"bash", "-c", "tail -f " + path});            InputStream inputStream = mProcess.getInputStream();            BufferedReader mBufferedReader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));            String buffer = null;            while (!Thread.currentThread().isInterrupted() &&&& !isStop) {                buffer = mBufferedReader.readLine();                if (mLogMonitorObject.getTailLogListener() != null) {                    mLogMonitorObject.getTailLogListener().onNewLine(mLogMonitorObject.getName(), mLogMonitorObject.getPath(), buffer);                    continue;                }                break;            }            mBufferedReader.close();        } catch (Exception e) {            e.printStackTrace();        }        log.info("{}退出对{}的监控", mLogMonitorObject.getSessionId(), mLogMonitorObject.getPath() + "/" + mLogMonitorObject.getName());    }    public void stop() {        mProcess.destroy();        isStop = true;    }}

注意这里,要发送给指定的WebSocket,而不是订阅了这个路径的WebSocket,因为使用SimpMessagingTemplate在发送数据时,他可以给所有订阅了此路径的WebSocket,那么就导致如果一个浏览器开了2个监控,而且监控的都是同一个日志文件,那么每个监控都会收到两条同样的消息。

所以要使用convertAndSendToUser方法而不是convertAndSend,这也就是为什么前面会通过setHandshakeHandler设置握手处理器为每个WebSocket连接取一个name的原因。

前端

&<!DOCTYPE html&>&<html lang="en"&>&<head&>    &<meta charset="UTF-8"&>    &<title&>日志监控&</title&>    &<style&>        body {            background: #000000;            color: #ffffff;        }        .log-list {            color: #ffffff;            font-size: 13px;            padding: 25px;        }    &</style&>&</head&>&<body&>&<p class="container"&>    &<p class="log-list"&>    &</p&>&</p&>&<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"&>&</script&>&<script src="/lib/stomp/stomp.min.js?x83982"&>&</script&>&<script src="https://lib.sinaapp.com/js/jquery/2.0.2/jquery-2.0.2.min.js"&>&</script&>&<script&>    var socket = new SockJS('/socket-log?a=a');    stompClient = Stomp.over(socket);    stompClient.connect({}, function (frame) {        stompClient.subscribe('/user/topic/path'+getQueryVariable("path"), function (greeting) {            console.log("a" + greeting)            let item = $("&<p class='log-line'&>&</p&>");            item.text(greeting.body)            $(".log-list").append(item);            $("html, body").animate({scrollTop: $(document).height()}, 0);        });    });    function getQueryVariable(variable) {        var query = window.location.search.substring(1);        var vars = query.split("&&");        for (var i = 0; i &< vars.length; i++) {            var pair = vars&[i].split("=");            if (pair&[0] == variable) {                return encodeURIComponent(pair&[1]);            }        }        return (false);    }&</script&>&</body&>&</html&>

效果

下面是启动、关闭Tomcat的日志。

不通过SimpMessagingTemplate如何发送数据

如果不使用SimpMessagingTemplate,那么首先我们要拿到对应的WebSocketSession,它有个sendMessage方法用来发送数据,但是类型是WebSocketMessage,Spring Boot有几个默认的实现,比如TextMessage用来发送文本信息。

但是如果使用了Stomp,那么单纯地使用他发送是不行的,数据虽然能过去,但是格式不对,Stomp解析不了,所以我们要按照Stomp的格式发送。

但是经过查找,未能找到相关的资料,所以自己看了一下他的源码,其中设计到了StompEncoder这个类,看名字就知道他是Stomp编码的工具。Stomp协议分为三个部分,命令、头、消息体,命令有如下几个:

CONNECTSENDSUBSCRIBEUNSUBSCRIBEBEGINCOMMITABORTACKNACKDISCONNECT

紧跟着命令下一行是头,是键值对形式存在的,最后是消息体,末尾以空字符结尾。

下面是发送的必要格式,否则StompEncoder也无法编码,将抛出异常,至于这个为什么这么写,详细就得看
StompEncoderde.writeHeaders方法了,里面有几个验证,这种写完全是被他逼的。

 StompEncoder stompEncoder = new StompEncoder(); byte&[] encode = stompEncoder.encode(createStompMessageHeader(),msg.getBytes()); webSocketSession.sendMessage(new TextMessage(encode));  private HashMap&<String, Object&> createStompMessageHeader() {     HashMap&<String, Object&> hashMap = new HashMap&<&>();     hashMap.put("subscription", createList("sub-0"));     hashMap.put("content-type", createList("text/plain"));     HashMap&<String, Object&> stringObjectHashMap = new HashMap&<&>();     stringObjectHashMap.put("simpMessageType", SimpMessageType.MESSAGE);     stringObjectHashMap.put("stompCommand", StompCommand.MESSAGE);     stringObjectHashMap.put("subscription", "sub-0");     stringObjectHashMap.put("nativeHeaders", hashMap);     return stringObjectHashMap;} private List&<String&> createList(String value) {    List&<String&> list = new ArrayList&<&>();    list.add(value);    return list;}

tail -f 为什么会失效

这是偶尔间的一个发现,当执行tail -f命令后,我们通过vim、gedit等工具编辑并保存这个文件,会发现tail -f并不会输出新的行,反而通过echo test&>&>xx.txt是正常的。

那这里的蹊跷又在哪?

其实,tail -f不管在文件移动、改名都会进行追踪,因为他跟踪的是文件描述符,引入维基百科的一句话:

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

tail -f执行后会产生一个进程,可以在/proc/pid/fd路径下查看他所打开的文件描述符,下面来看一个GIF。

在这个操作中,首先在终端1中创建一个1.txt,然后进行tail -f跟踪,接着在终端2中追加一行数据,可以看到终端1中是可以打印出来的。

然后再看神奇的一幕,在终端2进行mv改名,接着向被改名后的文件追加新的一行,你会发现,终端1居然还是会打印的。

如果查看一下这个进程的文件描述符,就不为奇了,在下面的命令中,显示了3号描述符追踪的是
/home/HouXinLin/test/tail/2.txt。

hxl@hxl-PC:/home/HouXinLin/test/tail$ ps -ef |grep 1.txthxl       1368 29021  0 09:02 pts/0    00:00:00 grep 1.txthxl      20298 29672  0 09:00 pts/6    00:00:00 tail -f 1.txthxl@hxl-PC:/home/HouXinLin/test/tail$ ls -l /proc/20298/fd总用量 0lrwx------ 1 hxl hxl 64 3月  16 09:02 0 -&> /dev/pts/6lrwx------ 1 hxl hxl 64 3月  16 09:02 1 -&> /dev/pts/6lrwx------ 1 hxl hxl 64 3月  16 09:02 2 -&> /dev/pts/6lr-x------ 1 hxl hxl 64 3月  16 09:02 3 -&> /home/HouXinLin/test/tail/2.txtlr-x------ 1 hxl hxl 64 3月  16 09:02 4 -&> anon_inode:inotifyhxl@hxl-PC:/home/HouXinLin/test/tail$ 

但是如果我们通过vim、等工具编辑这个文件后,那么这个文件描述符中会被记录为被删除,即使这个文件确实是存在的,此时在向2.txt文件中追加就会失效。

hxl@hxl-PC:/home/HouXinLin/test/tail$ vim 2.txt hxl@hxl-PC:/home/HouXinLin/test/tail$ ls -l /proc/20298/fd总用量 0lrwx------ 1 hxl hxl 64 3月  16 09:02 0 -&> /dev/pts/6lrwx------ 1 hxl hxl 64 3月  16 09:02 1 -&> /dev/pts/6lrwx------ 1 hxl hxl 64 3月  16 09:02 2 -&> /dev/pts/6lr-x------ 1 hxl hxl 64 3月  16 09:02 3 -&> /home/HouXinLin/test/tail/2.txt~ (deleted)lr-x------ 1 hxl hxl 64 3月  16 09:02 4 -&> anon_inode:inotifyhxl@hxl-PC:/home/HouXinLin/test/tail$ 

“linux查看实时日志命令(查看日志的三种命令分享)” 的相关文章

android圆形进度条组件(android设置按钮为圆形)

进度条有两种,如图横向进度条转圈进度条学的就这两种,转圈的一般是不定的,比如加载某些东西,加载完了,就把圈圈隐藏就行,横向的就可以表示大概是百分之多少。让用户心...

targus电脑包价格(Targus商务电脑包体验分享)

Targus的品牌相信不用过多介绍了,为了笔记本而诞生的背包,旗下产品线从100+到1000+都有分布,这款背包就是Targus旗下的高端型号——你猜?!呃……...

oppo售后线刷刷机工具教程(oppo手机线刷教程图解)

今天,是手机没有升级的第108天。在此小编想问大家一个问题——您的手机有被刷成“砖”的体验吗?最近有小伴咨询类似这样的问题:“诶,网上这个系统好炫酷噢,我可以刷...

kindle重启死机怎么办(见效最好的维修方式)

电子书kindle出现固定画面,充电,关机都不不能解决问题!自己动手完全可以解决!主要解决方法是断电复位。本机没有复位键,只能拆开机子,断掉电池电复位。拆机步骤...

乐视超级无线音箱怎么连接蓝牙(连接无线音箱蓝牙教程)

上周的天极网新媒体网友见面会上,天极联合乐视发起了乐视smart蓝牙音箱的众测活动。乐视smart蓝牙音箱还有另一个更贴切的名字,即磁吸炫彩音箱。顾名思义,这款...

100对鸽子养殖利润,肉鸽养殖前景分析

农村致富项目很多,其中养鸽子就是一个很好的出路。那么目前养给咱真的赚钱吗?那些说自己养鸽子的赚了钱的是真的吗?小编就从以下几个方面给大家介绍下一些情况,让大家自...

2023年各搜索市场份额排名(抢先看搜索市场排名情况)

什么是外贸建站?独立站的定义与分类相信一些刚转战外贸的新手还不知道外贸建站是什么?外贸建站在外贸行业中很多人也称之为独立站,为什么独立站会在外贸行业中成为一个热...

二维码扫描工具哪个好用(手机二维码扫描器推荐)

办公过程中,小工具可以帮助我们解决很多问题,如抠图、图片设计、文件转换等,今天给大家整理了非常实用的一批在线工具,有需要的小伙伴赶紧收藏起来吧。一、Design...

vivos7参数配置(vivos7详细参数配置测评)

前言:仅仅时隔4个月,vivo就又推出了其S系列的新机vivoS7,不过这个系列一直主打的都是线下市场和喜欢自拍的用户以及明星粉丝,所以快速的更迭也有助于保持产...

htc哪个是双卡(htcu11双卡双待双4g)

2017年05月,HTC正式发布了新一代HTCU11,目前这款手机正在京东众盛手机专营店火热销售中,价格仅为4999元,京东配送,喜欢的朋友不妨关注一下。HTC...