前言&引入

一个好的log模块可以帮助我们排错,分析,统计

一般来说log中需要有时间、栈信息(比如说文件名行号等),这些东西一般某些底层log模块已经帮我们做好了。但在业务中还有很多我们需要记录的信息,比如说:在web开发中,如果我们接收到一条request,我们可能需要执行很多操作,最基本的:

  • 请求数据是要记录的
  • response也是要记录的

如果仅仅只有这两条的话我们实际上是可以将消息放到一行来展示,但更复杂的情况是也可能还需要记录某些其他的信息,比如说我们在这次请求中将某个消息放入了消息队列,我们可能需要将这个消息是否放置成功,内容是什么,等等记录下来。如果分行记录的话当出现问题需要排查的话可能会十分麻烦,因为线上的环境一般是并发的,我们无法保证同一个请求中的日志每行都挨在一起,所以我们一般需要一个requestId来区分哪些日志是同一个请求所产生的。所以我们可能需要这样的请求处理函数:

func HandleRequest(requestId string, requestData []byte) (response []byte) {log.Info(requestId, requestData)...log.Info(requestId, "do something: A")...log.Info(requestId, "do something: B")...log.Info(requestId, response)...
}

但这样是不是很麻烦!每次打印日志都需要额外的手动记录requestId,我们需要有个通用的东西统一记录requestId,然后只需要将msg作为参数放置进去就行了。

那么我们可能会想到一个解决办法:每个Request都作为一个结构体,这个结构体包含了一个prefix字段,用来存储像requestId这样的需要预置的前缀,那么这个结构体可能看起来是这样的:

type Request struct {Header []byteBody []byteMethod []byteUrl []byte...prefix string
}func (r *Request) Info(msg []byte) {log.Info(r.prefix, msg)
}func (r *Request) SetPrefix(prefix string) {r.prefix = prefix
}

那么我们前面的请求处理函数可能就像这样:

func HandleRequest(r *Request) (response []byte) {r.Info(requestData)...r.Info("do something: A")...r.Info("do something: B")...r.Info(response)...
}

到这里似乎大功就告成了,但新的问题来了,因为项目中用到了http2.0,一个连接可以处理多个请求,你的老大希望每个连接都要记录日志,且能正确区分不同的连接。这时候你可能想都没想就给连接结构体Conn加上了prefix字段,然后给Conn加上了Info等记录方法,但聪明的你忽然发现自己似乎是在做一些重复的工作,为何不把日志抽离出来?于是就像这样:

// r.go
type PrefixLog struct {prefix string
}func NewPrefixLog(prefix string) (pl *PrefixLog){return $PrefixLog{prefix}
}func (pl *PrefixLog) Info(msg []byte){Log.Info(pl.prefix, msg) // 假设这里行号是30
}type Request struct {Header []byteBody []byteMethod []byteUrl []byte...*PrefixLog
}type Conn struct {requestCount uint32*PrefixLog
}
...

这次基本大功告成!但似乎新的问题又来了,假如为了更方便的排错,我们在日志需要保存log的文件名行号信息的话,上面这种形式就有问题了,因为通过这种方式调用的话所有的日志的文件名和行号都是相同的: file_name: r.go line:30,这该咋办呢?

正文

frp中的log模块相对简单,其封装了beego的log模块,主要逻辑写在utils/log文件中,来分析一下该文件。

全局变量之Log

import ("github.com/fatedier/beego/logs"
)// Log is the under log object
var Log *logs.BeeLogger

这个Log变量是frp中log模块的核心,几乎所有(或者说就是所有)的日志都是由这个Log变量来负责操作的。

初始化之init函数

func init() {Log = logs.NewLogger(200)Log.EnableFuncCallDepth(true)Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)
}

这个init函数则初始化了Log对象,注意Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1)这句,大体上就是:我们的程序可以说是由一个一个的函数组成,这些函数之间相互调用,每调用一个函数就进行了一次入栈操作,当某个函数执行完就执行了出栈操作,而loggerFuncCallDepth则表示要访问的栈的位置。

关于calldepth

那这个东西有啥用呢?我们知道我们打印日志的时候有的时候希望能够在日志中输出执行log的文件以及行号信息,拿go标准库log举个例子:

// main 文件
func a() {...b("hell0") // 假如该行行号为10...
}func wtf(msg string) {...msg = "[WTF!!]: " + msglog.Printf(msg) // 假如该行行号为21...
}func main() {a()
}
// 标准库log中的Printf方法,注意其内部调用了Output方法,且第一个参数为2
func Printf(format string, v ...interface{}) {std.Output(2, fmt.Sprintf(format, v...))
}// 这是真正执行了打印的方法
func (l *Logger) Output(calldepth int, s string) error {...
}

这里函数的调用顺序是main -> a -> wtf -> log.Printf -> Output,可以说这是一个深度为5的栈,calldepth为0表示栈顶,也就是Output对应的栈空间,1则表示log.Printf对应的栈空间,2表示wtf对应的栈空间,3表示wtf......以此类推。因为log模块设置的callpath是2,也就是假如我们设置了Llongfile或者Lshortfile标识符的时候输出的文件名是main,行号为21,假如我们设置callpath为3的话,输出的文件名依然是main但行号则变为了10。

打印函数

这里打印函数就拿Info来说明吧

func Info(format string, v ...interface{}) {Log.Info(format, v...)
}

可以看到Info函数实际上就是调用了Log.Info方法,Log.Info做了很多关于并发控制,格式输出,buffer写入的操作,但其最主要就是做了“将我们要打印的文字输出出来”这个操作。

log文件中唯一的一个结构体: PrefixLogger

type PrefixLogger struct {prefix    stringallPrefix []string
}func (pl *PrefixLogger) AddLogPrefix(prefix string) {if len(prefix) == 0 {return}pl.prefix += "[" + prefix + "] "pl.allPrefix = append(pl.allPrefix, prefix)
}// 同样,这里也仅仅列出PrefixLogger的Info方法
func (pl *PrefixLogger) Info(format string, v ...interface{}) {Log.Info(pl.prefix+format, v...)
}

PrefixLogger实际上就是一个具有前缀功能的很简单的结构体。

转载于:https://www.cnblogs.com/MnCu8261/p/10631854.html

frp源码剖析-frp中的log模块相关推荐

  1. frp源码剖析-frp中的mux模块

    前言 frp几乎所有的连接处理都是构建在mux模块之上的,重要性不必多说,来看一下这是个啥吧 ps: 安装方法 go get "github.com/fatedier/golib/net/m ...

  2. Swoft 源码剖析 - Swoft 中的注解机制

    作者:bromine 链接:https://www.jianshu.com/p/ef7... 來源:简书 著作权归作者所有,本文已获得作者授权转载,并对原文进行了重新的排版. Swoft Github ...

  3. python字符串代码对象_Python源码剖析 - Python中的字符串对象

    1. 前言 我们已经在 [Python中的整数对象] 章节中对定长对象进行了详细的讲解,接下来我们将介绍变长对象,而字符串类型,则是这类对象的典型代表. 这里必须先引入一个概念: Python 中的变 ...

  4. python源码剖析—— python中的列表对象

    1. PyListObject对象 PyListObject 对象可以有效地支持插入,添加,删除等操作,在 Python 的列表中,无一例外地存放的都是 PyObject 的指针.所以实际上,你可以这 ...

  5. python源码剖析—— python中的字节码对象初探

    一.代码对象 每个初学python的人都会认为python是一种解释型语言,这个不能说错.但是python并不是真的对执行的python代码的每一行进行解释,虽然我们有一个所谓的"解释器&q ...

  6. 源码剖析Redis中如何使用跳表的

    前言 阿里云今年春招校招面试题,面试官问Redis在是如何使用跳表的?让很多同学赶到很头疼.今天我们就来讲一讲吧. Sorted Set的结构 redis的数据类型中有序集合(sorted set)使 ...

  7. 从源码剖析SpringBoot中Tomcat的默认最大连接数

    为什么你的websocket只能建立256个连接?推出后,有许多小伙伴问:关键是怎么解决256这个问题.嗯,可能是我的标题起的有点问题,不过如果有认真阅读文章的话,应该会知道,其实256的限制是Chr ...

  8. LevelDB 源码剖析(六)WAL模块:LOG 结构、读写流程、崩溃恢复

    文章目录 日志结构 读写流程 写入 读取 崩溃恢复 当向 LevelDB 写入数据时,只需要将数据写入内存中的 MemTable,而由于内存是易失性存储,因此 LevelDB 需要一个额外的持久化文件 ...

  9. STL源码剖析 5中迭代器型别

    最常使用的5种迭代器的型别 为 value_type.difference_type.pointer.reference.iterator_category. 如果想要自己开发的容器和STL进行适配, ...

最新文章

  1. linux怎样查看内核参数,Linux 实例如何查看和修改 Linux 实例内核参数?
  2. RecycleView Layout 详解
  3. 瞬变电磁法的基本原理与TEM正演技术
  4. java 7.函数-递归_带有谓词的Java中的函数样式-第1部分
  5. android elf 加固_Android常见App加固厂商脱壳方法的整理
  6. .net core 部署在Linux系统上运行的环境搭建
  7. 当他不再爱你的时候(男女生一定要看)
  8. Sqlserver 2008:sp_msforeachdb 坑爹的错误陷阱
  9. 【工具推荐】PDF和其他格式的相关的转换
  10. 如何用计算机截部分屏,电脑如何长屏幕的截图?电脑截取长屏的方法
  11. 开源中国众包平台派活:微信小程序任务
  12. 引用提高 提高 啦啦啦啦啦啦啦啦啦啦了
  13. 奇葩Bug频出,苹果AirPods Pro 2提醒用户换电池
  14. 【笑小枫的按步照搬系列】Git从安装到入门操作,一文搞定
  15. uniapp开发微信小程序腾讯地图功能,生成地点云的sig签名
  16. Github的wiki编写
  17. 入门PAT的一些心得
  18. 【180730】三国记忆卡片小游戏源码
  19. 双目视觉07_立体匹配Census算法
  20. 学习Coq笔记(一):Windows下安装Coq

热门文章

  1. 零基础带你学习MySQL—备份恢复数据库(三)
  2. 一篇文章教你学会如何使用CSS中的雪碧图(CSS Sprite)
  3. 面试了二十多个人,终于定下来一个
  4. 墨条不如墨汁黑是怎么回事?
  5. 小时候有哪些丑事,让你终身难忘?
  6. 很多人创业是为了自由
  7. 创业人永远不要让工作成为自己的负担
  8. 巴菲特投资50年的5个心得
  9. OpenStack 是什么
  10. WIFI与WLAN区别有哪些?