此文已由作者杨望暑授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

背景

在服务端查看log会经常使用到tail -f命令实时跟踪文件变化.
那么问题来了, 如果自己写一个同样功能的, 该何处写起呢?
如果你用过ELK里的beats/filebeat的话, 应该知道filebeat做的事情就是监控日志变化, 并把最新数据,按照自定义配置处理后, 发送给ElasticSearch/kafka/...
对, 本文就是想介绍如何自己实现一个简易版filebeat, 只要日志内容发生变化(append new line), 能触发一个消息, 实现对这一行数据的预处理, 打印, 接入kafka等动作, 还有一个功能是, 当这个工具重启后, 依然能从上次读取的位置开始读.

工具

Golang
IDEA

大致流程

具体实现

从流程图中可以看出, 我们需要解决下面几个问题

  1. 记录上一次程序关闭前,文件读取位置,下次程序启动时候加载这个位置信息.

  2. 文件定位并按行读取, 并发布读取的行

  3. 监测文件内容变化,并发出通知

记录上次读取位置

这个问题关键应该是什么时候记录上次读取的offset.

  1. 读取并发布后记录
    如果发布后,做记录前,程序挂了,那么重启程序后,这行数据会重新被读一次.

  2. 读取后马上记录,记录成功后,才对外发布.
    这样会产生另一个问题, 发布前程序挂了, 重启后, 那条未必发送的消息,外部是拿不到了.

如果没理解错, elastic的filebeat选的就是第一种,且没做相应的异常处理, 他是设置一个channel池, 接收并异步写入位置信息, 如果写入失败, 则打印一条error日志就继续走了

logp.Err("Writing of registry returned error: %v. Continuing...", err)

文件定位并按行读取, 并发布读取的行

要读取一个文件, 首先要有一个reader

func (tail *Tailf) openReader() {
    tail.file, _ = os.Open(tail.FileName)
    tail.reader = bufio.NewReader(tail.file)
}

对于从文件位置(offset)=0处开始读一行, 这没什么问题, 直接用下面这个方法就可以了.

func (tail *Tailf) readLine() (string, error) {
    line, err := tail.reader.ReadString('\n')    if err != nil {        return line, err
    }
    line = strings.TrimRight(line, "\n")    return line, err
}

但是, 对于文件内容增加了, 但是还没到一行,也就是没出现\n 却出现了EOF(end of file), 那这个情况下, 我们是要等待的,offset必须保持在这一行的行头.

func (tail *Tailf) getOffset() (offset int64, err error) {
    offset, err = tail.file.Seek(0, os.SEEK_CUR)
    offset -= int64(tail.reader.Buffered())    return}func (tail *Tailf) beginWatch() {
    tail.openReader()    var offset int64
    for {       //取上一次读取位置(行头)
        offset, _ = tail.getOffset()
        line, err := tail.readLine()        if err == nil {
            tail.publishLine(line)
        } else if err == io.EOF {            //读到了EOF, offset设置回到行头
            tail.seekTo(Seek{offset: offset, whence: 0})            //block and wait for changes
            tail.waitChangeEvent()
        } else {
            fmt.Println(err)            return
        }
    }
}func (tail *Tailf) seekTo(pos Seek) error {
    tail.file.Seek(pos.offset, pos.whence)    //一旦改变了offset, 这个reader必须reset一下才能生效
    tail.reader.Reset(tail.file)    return nil}// 这里是发布一个消息, 因为是demo,所以只是简单的往channel里一扔func (tail *Tailf) publishLine(line string) {
    tail.Lines <- line
}

下面说说waitChangeEvent

如何监视文件内容变化,并通知

监测文件内容增加的方式大体有2种

  1. 监测文件最后修改时间以及文件大小的变化,俗称poll--轮询

  2. 利用linux的inotify命令实现监测,他会在文件发生状态改变后触发事件

这里采用第一种方式, filebeat也用的第一种.
我们自己怎么实现呢?

//currReadPos: 文件末尾的offset,也就是当前文件大小func (w *PollWatcher) ChangeEvent(currReadPos int64) (*ChangeEvent, error) {

    watchingFile, err := os.Stat(w.FileName)    if err != nil {        return nil, err
    }
    changes := NewChangeEvent()    //当前的大小
    w.FileSize = currReadPos    //之前的修改时间
    previousModTime := watchingFile.ModTime()    //轮询
    go func() {
        previousSize := w.FileSize        for {
            time.Sleep(POLL_DURATION)            //这里省略很多代码, 假设文件是存在的,且没被重命名,删除之类的情况, 文件是像日志文件一样不断append的 
            file, _ := os.Stat(w.FileName)        // ... 省略一大段代码
            if previousSize > 0 && previousSize < w.FileSize {                //文件肥了
                changes.NotifyModified()
                previousSize = w.FileSize                continue
            }             previousSize = w.FileSize            // 处理 原本没内容, 但是加入了内容, 所以要用修改时间
            modTime := file.ModTime()            if modTime != previousModTime {
                previousModTime = modTime
                changes.NotifyModified()
            }
        }
    }()    return changes, nil}

这里的changes.NotifyModified方法只是往下面实例里Modified Channel 放入 ce.Modified <- true

type ChangeEvent struct {
    Modified  chan bool
    Truncated chan bool
    Deleted   chan bool}

也正是这个动作, 在主线程中, 就能收到文件被修改的通知, 从而继续出发readLine动作

// 上面有个beginWatch方法代码,结合这个代码来看func (tail *Tailf) waitChangeEvent() error {    // ... 省略初始化动作
    select {    //只测试文件内容增加
    case <-tail.changes.Modified:
        fmt.Println(">> find Modified")        return nil
    // ... 省略其他
    }
}

有了这个一连串的代码后, 我们就能在main里监视文件变化了

func main() {
    t, _ := tailf.NewTailf("/Users/yws/Desktop/test.log")    for line := range t.Lines {    //这里会block住,有新行到来,就会输出新行
        fmt.Println(line)
    }
}

扩展点

这个扩展点, 和filebeat一样.

  1. 在读取时候, 不一定是按行读取,可以读多行,json解析等

  2. 发布时候, 本文例子是直接写console, 其实可以接kafka, redis, 数据库等

  3. .... 想不出来了

总结

虽然是一个很简单的功能, 现代主流服务端编程语言基本都能实现, 但为什么用go来实现呢?
一大堆优点和缺点就不列了..这不是软文.
谈谈go初学者的看法

  1. 代码很简洁, 虽然不支持很多高级语言特性, 但看起来依然那么爽, 除了那些过渡包装的struct以及怪异的取名.

  2. 写并发(goroutine)是那么的简单,那么的优雅,但也很容易被我这样的菜鸟滥用, 这语言debug目前有点肉痛

  3. goroutine通信也是那么的简单, channel设计的很棒, 用着很爽

  4. 不爽的地方, 多返回值的问题, 写惯了java的xinstance.method(yInstance.method()), 当yInstance.method()是多返回值的时候,必须拆分成2行或更多, 每次编译器报错时候就想砸键盘.

参考资料

  1. https://github.com/elastic/beats filebeat只是其中一个feature

  2. https://github.com/hpcloud/tail 写到一半发现原来别人也干过一样的事了, 代码基本大同小异, 有兴趣的可以看他的代码, 写的更完善.

网易云免费体验馆,0成本体验20+款云产品!

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 网易七鱼 Android 高性能日志写入方案
【推荐】 【专家坐堂】四种并发编程模型简介
【推荐】 消息中间件客户端消费控制实践

如何用GO实现一个tail -f功能以及相应的思维发散的更多相关文章

  1. python10min系列之面试题解析:python实现tail -f功能

    同步发布在github上,跪求star 这篇文章最初是因为reboot的群里,有人去面试,笔试题有这个题,不知道怎么做,什么思路,就发群里大家讨论 我想了一下,简单说一下我的想法吧,当然,也有很好用的 ...

  2. python实现tail -f 功能

    这篇文章最初是因为reboot的群里,有人去面试,笔试题有这个题,不知道怎么做,什么思路,就发群里大家讨论 我想了一下,简单说一下我的想法吧,当然,也有很好用的pyinotify模块专门监听文件变化, ...

  3. python实现tail -f功能

    这篇文章最初是因为reboot的群里,有人去面试,笔试题有这个题,不知道怎么做,什么思路,就发群里大家讨论 我想了一下,简单说一下我的想法吧,当然,也有很好用的pyinotify模块专门监听文件变化, ...

  4. Python 10min系列之面试题解析丨Python实现tail -f功能

    关于这道题,简单说一下我的想法吧.当然,也有很好用的 pyinotify 模块专门监听文件变化,不过我更想介绍的,是解决的思路. 毕竟作为面试官,还是想看到一下解决问题的思路,而且我觉得这一题的难点不 ...

  5. Notepad++ 中使用tail -f功能

    想要notepad++中有tail -f的功能吗? 可以如下配置 Settings > Preferences > MISC 在 File Status Auto-Detection下 “ ...

  6. 自动化测试(二)如何用python写一个用户登陆功能

    需求信息: 写一个判断登录的程序: 输入: username password 最大错误次数是3次,输入3次都没有登录成功,提示错误次数达到上限 需要判断输入是否为空,什么也不输入,输入一个空格.n个 ...

  7. Pytho实现tail -f

    实现Python版的tail -f功能 tail -f 的功能非常好用.我们用Python也可以实现这样的功能.实现的原理是通过Python版本的inotify获得文件的更新消息,从而读取更新的行.p ...

  8. 实现类似tail -f file功能

    python版本py3 tail -f file是打印最后10行,然后跟踪文件追加的内容打印出来. python3 以为本方式打开的话,不能回退(f.seek(-1,1)),所有以'rb'方式打开文件 ...

  9. tail -f 实时跟踪一个日志文件的输出内容

    tail -f  实时跟踪一个日志文件的输出内容 http://hittyt.iteye.com/blog/1927026 https://blog.csdn.net/mengxianhua/arti ...

随机推荐

  1. Linux Shell高级技巧

    Linux Shell高级技巧(一) http://www.cnblogs.com/stephen-liu74/archive/2011/12/22/2271167.html一.将输入信息转换为大写字 ...

  2. 怎样查询锁表的SQL

    通过以下的语句查询出锁表的SQL: select l.session_id sid, s.serial#,        l.locked_mode,        l.oracle_username ...

  3. ActiveMQ(二) 转

    package pfs.y2017.m11.mq.activemq.demo02; import java.util.concurrent.atomic.AtomicInteger; import j ...

  4. v-on指令

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义反复

    从图片中我们能够看到,这里在语义上有一定的反复,当然这是谷歌的原始设计.这个问题在博客上进行共享从表面上来看着实没有什么太大的意义,只是因为Android4.3在锁屏功能上比起老版本号做了非常大的修改 ...

  6. HDU 6138 Fleet of the Eternal Throne 后缀数组 + 二分

    Fleet of the Eternal Throne Problem Description > The Eternal Fleet was built many centuries ago ...

  7. springCloud和docker笔记(1)——微服务架构概述

    1.微服务设计原则 1)单一职责原则:只关注整个系统中单独.有界限的一部分(SOLID原则之一) 2)服务自治原则:具备独立的业务能力和运行环境,可独立开发.测试.构建.部署 3)轻量级通信机制:体量 ...

  8. SEO搜索引擎基础原理

  9. https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2

    https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2

  10. 静态代理、动态代理和cglib代理

    转:https://www.cnblogs.com/cenyu/p/6289209.html 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处 ...