利用代码生成自动注入Trace函数
要实现向目标代码中的函数/方法自动注入Trace函数,我们首先要做的就是将上面Trace函数相关的代码打包到一个module中以方便其他module导入。下面我们就先来看看将Trace函数放入一个独立的module中的步骤。
将Trace函数放入一个独立的module中
我们创建一个名为instrument_trace的目录,进入这个目录后,通过go mod init命令创建一个名为github.com/bigwhite/instrument_trace的module:
$mkdir instrument_trace
$cd instrument_trace
$go mod init github.com/bigwhite/instrument_trace
go: creating new go.mod: module github.com/bigwhite/instrument_trace
接下来,我们将最新版的trace.go放入到该目录下,将包名改为trace,并仅保留Trace函数、Trace使用的函数以及包级变量,其他函数一律删除掉。这样,一个独立的trace包就提取完毕了。
作为trace包的作者,我们有义务告诉大家如何使用trace包。 在Go中,通常我们会用一个example_test.go文件来编写使用trace包的演示代码,下面就是我们为trace包提供的example_test.go文件:
// instrument_trace/example_test.go
package trace_test
import (
trace "github.com/bigwhite/instrument_trace"
)
func a() {
defer trace.Trace()()
b()
}
func b() {
defer trace.Trace()()
c()
}
func c() {
defer trace.Trace()()
d()
}
func d() {
defer trace.Trace()()
}
func ExampleTrace() {
a()
// Output:
// g[00001]: ->github.com/bigwhite/instrument_trace_test.a
// g[00001]: ->github.com/bigwhite/instrument_trace_test.b
// g[00001]: ->github.com/bigwhite/instrument_trace_test.c
// g[00001]: ->github.com/bigwhite/instrument_trace_test.d
// g[00001]: <-github.com/bigwhite/instrument_trace_test.d
// g[00001]: <-github.com/bigwhite/instrument_trace_test.c
// g[00001]: <-github.com/bigwhite/instrument_trace_test.b
// g[00001]: <-github.com/bigwhite/instrument_trace_test.a
}
在example_test.go文件中,我们用ExampleXXX形式的函数表示一个示例,go test命令会扫描example_test.go中的以Example为前缀的函数并执行这些函数。
每个ExampleXXX函数需要包含预期的输出,就像上面ExampleTrace函数尾部那样,我们在一大段注释中提供这个函数执行后的预期输出,预期输出的内容从// Output:的下一行开始。go test会将ExampleTrace的输出与预期输出对比,如果不一致,会报测试错误。从这一点,我们可以看出example_test.go也是trace包单元测试的一部分。
现在Trace函数已经被放入到独立的包中了,接下来我们就来看看如何将它自动注入到要跟踪的函数中去。
自动注入Trace函数
现在,我们在instrument_trace module下面增加一个命令行工具,这个工具可以以一个Go源文件为单位,自动向这个Go源文件中的所有函数注入Trace函数。
我们再根据05讲中介绍的带有可执行文件的Go项目布局,在instrument_trace module中增加cmd/instrument目录,这个工具的main包就放在这个目录下,而真正实现自动注入Trace函数的代码呢,被我们放在了instrumenter目录下。
下面是变化后的instrument_trace module的目录结构:
$tree ./instrument_trace -F
./instrument_trace
├── Makefile
├── cmd/
│ └── instrument/
│ └── main.go # instrument命令行工具的main包
├── example_test.go
├── go.mod
├── go.sum
├── instrumenter/ # 自动注入逻辑的相关结构
│ ├── ast/
│ │ └── ast.go
│ └── instrumenter.go
└── trace.go
我们先来看一下cmd/instrument/main.go源码,然后自上而下沿着main函数的调用逻辑逐一看一下这个功能的实现。下面是main.go的源码:
// instrument_trace/cmd/instrument/main.go
... ...
var (
wrote bool
)
func init() {
flag.BoolVar(&wrote, "w", false, "write result to (source) file instead of stdout")
}
func usage() {
fmt.Println("instrument [-w] xxx.go")
flag.PrintDefaults()
}
func main() {
fmt.Println(os.Args)
flag.Usage = usage
flag.Parse() // 解析命令行参数
if len(os.Args) < 2 { // 对命令行参数个数进行校验
usage()
return
}
var file string
if len(os.Args) == 3 {
file = os.Args[2]
}
if len(os.Args) == 2 {
file = os.Args[1]
}
if filepath.Ext(file) != ".go" { // 对源文件扩展名进行校验
usage()
return
}
var ins instrumenter.Instrumenter // 声明instrumenter.Instrumenter接口类型变量
// 创建以ast方式实现Instrumenter接口的ast.instrumenter实例
ins = ast.New("github.com/bigwhite/instrument_trace", "trace", "Trace")
newSrc, err := ins.Instrument(file) // 向Go源文件所有函数注入Trace函数
if err != nil {
panic(err)
}
if newSrc == nil {
// add nothing to the source file. no change
fmt.Printf("no trace added for %s\n", file)
return
}
if !wrote {
fmt.Println(string(newSrc)) // 将生成的新代码内容输出到stdout上
return
}
// 将生成的新代码内容写回原Go源文件
if err = ioutil.WriteFile(file, newSrc, 0666); err != nil {
fmt.Printf("write %s error: %v\n", file, err)
return
}
fmt.Printf("instrument trace for %s ok\n", file)
}
作为命令行工具,instrument使用标准库的flag包实现对命令行参数(这里是-w)的解析,通过os.Args获取待注入的Go源文件路径。在完成对命令行参数个数与值的校验后,instrument程序声明了一个instrumenter.Instrumenter接口类型变量ins,然后创建了一个实现了Instrumenter接口类型的ast.instrumenter类型的实例,并赋值给变量ins。
instrumenter.Instrumenter接口类型的声明放在了instrumenter/instrumenter.go中: