tailf 组件

查看log日志,会经常使用到tail -f命令实时跟踪文件变化。也可以用Go语言的代码来实现同样的功能,这样就可以直接用到项目中去了。这里不用重复造轮子,有一个第三方的库已经实现了这个功能:

import "github.com/hpcloud/tail"

HP团队出的tail库,常用于日志收集。这里主要就是看看如何使用。

示例代码

package main

import (
    "os"
    "fmt"
    "github.com/hpcloud/tail"
    "time"
)

func main () {
    filename := "tailf_test.txt"  // 指定查看哪个文件
    tails, err := tail.TailFile(filename, tail.Config{
        // 下面的2行配置,相当于命令 tail -F 的效果
        // 追踪文件并保持重试,即该文件被删除或改名后,如果再次创建相同的文件名,会继续追踪。
        ReOpen: true,
        Follow: true,

        Location: &tail.SeekInfo{Offset: 0, Whence: os.SEEK_END},  // 从哪个位置开始读,这里的设置是从文件结尾
        MustExist: false,  // 如果文件不存在,会失败。这是设为fales,允许文件不存在,等文件一创建就会开始追踪
        Poll: true,  // 设为true,检查文件的变化。false是通过inotify来检查
    })
    if err != nil {
        fmt.Fprintf(os.Stderr, "tail file: %s , ERROR: %v\n", tails.Filename, err)
    }
    for {
        msg, ok := <-tails.Lines
        if ! ok {
            fmt.Fprintf(os.Stderr, "文件不存在,尝试重新打开文件: %s", tails.Filename)
            time.Sleep(time.Millisecond * 1000)
            continue
        }
        fmt.Println("msg:", msg.Text)
        fmt.Println("time:", msg.Time)
        // windows系统里的换行是\r\n,输出的时候可能只去掉了\n,导致字符串会以\r结尾
        // 如果以\r结尾,后面还有字符串的话,就会从头开始把之前的内容覆盖掉
        fmt.Printf("%q\n", msg.Text)  // 输出有问题的原因是这样的,这里可以看出来
        // 可以手动把 msg.Text 最后的 \r 去掉,即使没有,也不影响
        fmt.Printf("msg: %s time: %s\n", strings.TrimRight(msg.Text, "\r"), msg.Time)
    }
}

这里最后踩了个小坑,应该是windows系统才会有的问题。

配置文件库

解析配置文件使用的第三方库是属于beego框架里的一个模块。

beego 框架

beego是一个快速开发Go应用的HTTP框架,他可以用来快速开发API、Web及后端服务等各种应用,是一个 RESTful的框架。
安装:

go get github.com/astaxie/beego

beego是基于八大独立的模块构建的,是一个高度解耦的框架:

  • cache : 做缓存
  • config : 解析各种格式的配置文件
  • context
  • httplibs
  • logs : 记录操作信息
  • orm
  • session
  • toolbox

接下来只是单独把某个模块拿来使用,不学习框架的使用。安装的话只能完整的全部装上了。

使用示例

基本用法:
配置文件如下:

[server]
host = "1.1.1.1"
port = 23

示例代码:

package main

import (
    "github.com/astaxie/beego/config"
    "fmt"
    "os"
)

func main() {
    conf, err := config.NewConfig("ini", "test.conf")  // 配置文件格式和文件路径
    if err != nil {
        fmt.Fprintf(os.Stderr, "New Confit ERROR: %v\n", err)
        return
    }
    port, err := conf.Int("server::port")  // 获取数值型数据可能会返回错误
    if err != nil {
        fmt.Fprintf(os.Stderr, "get conf ERROR: %v\n", err)
        return
    }
    fmt.Println("port:", port)
    host := conf.String("server::host")  // 获取字符串数据,不会返回错误,读不到会返回空字符串
    fmt.Println("host:", host)
    ip := conf.String("server::ip")  // 读不到就会返回空
    fmt.Println("ip:", ip)
    ip2 := conf.DefaultString("server::ip", "1.1.1.2")  // 代码层面指定的默认值
    fmt.Println("ip2", ip2)
}

默认值
上面的默认值是在代码层面实现的。但是ini配置本身是支持默认值的,定义的时候可以写在最外面,也可以写在[default] 里面,效果都是一样的:

key1 = vale1

[default]
key2 = value2

[server]
host = "1.1.1.1"
port = 23
key1 = v1

示例代码:

package main

import (
    "github.com/astaxie/beego/config"
    "fmt"
    "os"
)

func main() {
    conf, err := config.NewConfig("ini", "test.conf")  // 配置文件格式和文件路径
    if err != nil {
        fmt.Fprintf(os.Stderr, "New Confit ERROR: %v\n", err)
        return
    }
    server, err := conf.GetSection("server")  // 可以获取到所有的参数,返回map
    if err != nil {
        fmt.Fprintf(os.Stderr, "get section ERROR: %v\n", err)
    }
    fmt.Println(server)
    df, err := conf.GetSection("default")  // default里的以及最外层的参数都会解析存到这个map里
    if err != nil {
        fmt.Fprintf(os.Stderr, "get section ERROR: %v\n", err)
    }
    fmt.Println(df)

    key1a, key1b := conf.String("key1"), conf.String("default::key1")  //  前面是否加上 default:: 都是一样的
    key2 := conf.String("key2")
    fmt.Println(key1a, key1b, key2)
    // 解析获取默认值可能就是这么做的吧,网上没有找到相关的示例
    k1 := conf.DefaultString("server::key1", conf.String("key1"))
    k2 := conf.DefaultString("server::key2", conf.String("key2"))
    fmt.Println(k1, k2)
}

日志库

这个还是beego框架里的一个组件

基本使用

先把log输出到终端:

package main

import (
    "github.com/astaxie/beego/logs"
)

func main() {
    logs.SetLogger(logs.AdapterConsole)  // 设置日志输出到哪里, 参数是个常数。这里的效果是输出到终端
    logs.SetLevel(logs.LevelInfo)  // 设置日志等级
    logs.Debug("DEBUG msg")  // 这条等级不够,不会显示
    logs.Info("INFO msg")
}

这里使用了console引擎,就是输出到终端,底层是到os.Stdout。

输出到文件

要把log输出到文件,只需要设置一个新的file引擎:

package main

import (
    "github.com/astaxie/beego/logs"
)

func main() {
    logs.SetLogger(logs.AdapterFile, `{"filename":"test.log","level":6}`)  // logs.LevelInfo = 6
    logs.SetLogger(logs.AdapterConsole, `{"level":4,"◦color":true}`)  // logs.LevelWarning = 4
    logs.SetLevel(logs.LevelInfo)
    logs.Info("INFO msg")
    logs.Warn("WARN msg")
}

SetLogger接收2个参数。
第一个参数就是引擎名,这里用到了2个 "console" 和 "file" 。上面的代码里写的是常量名。
第二个参数是可选参数(上个例子没有用,这里都用上了),用来表示配置信息,所有配置写在一个json字符串里。

更多引擎和参数

这里可参考官网的引擎配置设置:
https://beego.me/docs/module/logs.md

这里只列出2个
console 主要参数说明:

  • level 输出的日志级别
  • color 是否开启打印日志彩色打印(需环境支持彩色输出)

file 主要参数说明:

  • filename 保存的文件名
  • maxlines 每个文件保存的最大行数,默认值 1000000
  • maxsize 每个文件保存的最大尺寸,默认值是 1 << 28, //256 MB
  • daily 是否按照每天 logrotate,默认是 true
  • maxdays 文件最多保存多少天,默认保存 7 天
  • rotate 是否开启 logrotate,默认是 true
  • level 日志保存的时候的级别,默认是 Trace 级别
  • perm 日志文件权限

所有的引擎有这些,左边是常量,右边是对应的名字:

const (
    AdapterConsole   = "console"
    AdapterFile      = "file"
    AdapterMultiFile = "multifile"
    AdapterMail      = "smtp"
    AdapterConn      = "conn"
    AdapterEs        = "es"
    AdapterJianLiao  = "jianliao"
    AdapterSlack     = "slack"
    AdapterAliLS     = "alils"
)

注册引擎名

所有的引擎,都会自动进行注册。具体就是写在引擎的代码的init方法里:

// beego/logs/console.go
func init() {
    Register(AdapterConsole, NewConsole)
}

不过很多太多方法都是小写的,导致接口没有暴露出来用不了。只有 console 和 conn 引擎可以注册。

package main

import (
    "github.com/astaxie/beego/logs"
    "encoding/json"
    "fmt"
    "os"
)

func main() {
    logs.Register("new", logs.NewConsole)
    config := make(map[string]interface{})  // 先定义一个map来存放配置参数
    config["level"] = 4  // LevelWarning = 4
    config["color"] = true  // map的value可能是字符串、×××或bool等不同的类型,所以定义的时候类型是空接口
    configStr, err := json.Marshal(config)
    if err != nil {
        fmt.Fprintf(os.Stderr, "json Marshal ERROR %v\n", err)
        return
    }
    logs.SetLogger("new", string(configStr))  // 用map来设置参数,到这里再转成json字符串
    logs.SetLogger(logs.AdapterConsole)  // 再设置一个默认的console引擎
    logs.Info("INFO msg")  // 满足1个logger
    logs.Error("ERROR msg")  // 两个logger都会输出这条
}

使用map来设置配置信息
配置信息是要传递json字符串进去的,但是人工拼接josn也是很不友好的。所以先把配置写在map里,然后再序列化成json传递给函数。并且map的value是空接口,因为配置可能是字符串、整数或布尔等不同类型。
无法注册file引擎
注册不了新的file引擎貌似没啥用。源码没有把对应的方法暴露出来应该就没有办法了,就是 func newFileWriter() Logger{} 这个函数,函数名是小写的。只能去改源码,还不方便把上面的函数名改掉,因为在别处还有调用这个方法。所以最方便的修改方法是给原来的小写的函数名定义一个大写的别名:

func newFileWriter() Logger {
    // 省略函数体部分
}

var NewFileWriter func() Logger = newFileWriter

如果代码中有这种需要开放接口,又不想修改每一个被应用的位置,可以用上面的例子来把接口变成可导出的。

多文件的正确用法
没有提供注册file引擎的接口,是因为输出到多个文件还提供了另外一个multifile的引擎。之前只把注意力集中到了file引擎上,没有发现还有其他的可用的引擎。
虽然multifile底层也是通过调用file来实现的,是在file的基础上又做了一些封装。所以如果需要输出到多个文件,应该使用提供的multifile来实现。