├── .gitignore ├── LICENSE ├── README.md ├── doc.go ├── log └── log.go ├── trace.go └── trace_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Bardin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## gotrace 2 | ### tracing for go programs 3 | 4 | gotrace annotates function calls in go source files with log statements on entry and exit. 5 | 6 | usage: gotrace [flags] [path ...] 7 | -exclude string 8 | exclude any matching functions, takes precedence over filter 9 | -exported 10 | only annotate exported functions 11 | -filter string 12 | only annotate functions matching the regular expression (default ".") 13 | -formatLength int 14 | limit the formatted length of each argument to 'size' (default 1024) 15 | -package 16 | show package name prefix on function calls 17 | -prefix string 18 | log prefix 19 | -returns 20 | show function return 21 | -timing 22 | print function durations. Implies -returns 23 | -w re-write files in place 24 | 25 | #### Example 26 | 27 | # gotrace operates directly on go source files. 28 | # Insert gotrace logging statements into all *.go files in the current directory 29 | # Make sure all files are saved in version control, as this rewrites them in-place! 30 | 31 | $ gotrace -w -returns ./*.go 32 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Command gotrace annotates function entry and exit points to provide strace-like 4 | tracing of go programs. 5 | 6 | usage: gotrace [flags] [path ...] 7 | -exclude string 8 | exclude any matching functions, takes precedence over filter 9 | -exported 10 | only annotate exported functions 11 | -filter string 12 | only annotate functions matching the regular expression (default ".") 13 | -package 14 | show package name prefix on function calls 15 | -prefix string 16 | log prefix (default "\t") 17 | -returns 18 | show function return 19 | -w re-write files in place 20 | 21 | */ 22 | package main 23 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Custom logger for gotrace 2 | package log 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "reflect" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "time" 16 | "unicode/utf8" 17 | ) 18 | 19 | // counter to mark each call so that entry and exit points can be correlated 20 | var ( 21 | counter uint64 22 | L *log.Logger 23 | setupOnce sync.Once 24 | formatSize int 25 | ) 26 | 27 | // Setup our logger 28 | // return a value so this van be executed in a toplevel var statement 29 | func Setup(output, prefix string, size int) int { 30 | setupOnce.Do(func() { 31 | setup(output, prefix, size) 32 | }) 33 | return 0 34 | } 35 | 36 | func setup(output, prefix string, size int) { 37 | var out io.Writer 38 | switch output { 39 | case "stdout": 40 | out = os.Stdout 41 | default: 42 | out = os.Stderr 43 | } 44 | 45 | L = log.New(out, prefix, log.Lmicroseconds) 46 | formatSize = size 47 | } 48 | 49 | // Make things a little more readable. Format as strings with %q when we can, 50 | // strip down empty slices, and don't print the internals from buffers. 51 | func formatter(i interface{}, size int) (s string) { 52 | // don't show the internal state of buffers 53 | switch i := i.(type) { 54 | case *bufio.Reader: 55 | s = "&bufio.Reader{}" 56 | case *bufio.Writer: 57 | s = "&bufio.Writer{}" 58 | case *bytes.Buffer: 59 | s = fmt.Sprintf("&bytes.Buffer{%q}", i.String()) 60 | case *bytes.Reader: 61 | v := reflect.ValueOf(i) 62 | // TODO: should probably iterate to find the slice in case the name changes 63 | if b, ok := v.FieldByName("s").Interface().([]byte); ok { 64 | if len(b) > size { 65 | b = b[:size] 66 | } 67 | s = fmt.Sprintf("&bytes.Reader{%q}", b) 68 | } 69 | case *strings.Reader: 70 | v := reflect.ValueOf(i) 71 | if f, ok := v.FieldByName("s").Interface().(string); ok { 72 | s = fmt.Sprintf("&strings.Reader{%q}", f) 73 | } 74 | case []byte: 75 | // bytes slices are often empty, so trim them down 76 | b := bytes.TrimLeft(i, "\x00") 77 | if len(b) == 0 { 78 | s = "[]byte{0...}" 79 | } else if utf8.Valid(i) { 80 | s = fmt.Sprintf("[]byte{%q}", i) 81 | } else { 82 | s = fmt.Sprintf("%#v", i) 83 | } 84 | case string: 85 | s = fmt.Sprintf("%q", i) 86 | } 87 | 88 | if s == "" { 89 | s = fmt.Sprintf("%#v", i) 90 | } 91 | 92 | if len(s) > size { 93 | last := s[len(s)-1] 94 | s = s[:size] + "..." + string(last) 95 | } 96 | 97 | return s 98 | } 99 | 100 | // Format N number of arguments for logging, and limit the length of each formatted arg. 101 | func Format(args ...interface{}) string { 102 | parts := make([]string, len(args)) 103 | for i, arg := range args { 104 | parts[i] = formatter(arg, formatSize) 105 | } 106 | return strings.Join(parts, ", ") 107 | } 108 | 109 | func ID() uint64 { 110 | return atomic.AddUint64(&counter, 1) 111 | } 112 | 113 | func Now() time.Time { 114 | return time.Now() 115 | } 116 | 117 | func Since(t time.Time) time.Duration { 118 | return time.Since(t) 119 | } 120 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "go/ast" 8 | "go/format" 9 | "go/parser" 10 | "go/token" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "regexp" 15 | "strings" 16 | "text/template" 17 | ) 18 | 19 | var ( 20 | importName = "__log" 21 | importPath = `"github.com/jbardin/gotrace/log"` 22 | 23 | importStmt = ` 24 | import __log "github.com/jbardin/gotrace/log" 25 | ` 26 | 27 | setup = ` 28 | var _ = __log.Setup("stderr", "%s", %d) 29 | ` 30 | 31 | tmpl = ` 32 | __traceID := __log.ID() 33 | __log.L.Printf("[%d] {{.fname}}(%s){{if .position}} [{{.position}}]{{ end }}\n", __traceID, __log.Format({{.args}})) 34 | {{if .timing}}__start := __log.Now(){{end}} 35 | {{if .return}}defer func() { 36 | {{if .timing}}since := "in " + __log.Since(__start).String(){{else}}since := ""{{end}} 37 | __log.L.Printf("[%d] {{.fname}}{{if .position}} [{{.position}}]{{ end }} returned %s\n", __traceID, since) 38 | }(){{ end }} 39 | ` 40 | 41 | ErrAlreadyImported = fmt.Errorf("%s already imported", importPath) 42 | ) 43 | 44 | var ( 45 | fset *token.FileSet 46 | funcTemplate *template.Template 47 | showReturn bool 48 | exportedOnly bool 49 | prefix string 50 | showPackage bool 51 | writeFiles bool 52 | filterFlag string 53 | excludeFlag string 54 | formatLength int 55 | timing bool 56 | 57 | filter *regexp.Regexp 58 | exclude *regexp.Regexp 59 | ) 60 | 61 | // convert function parameters to a list of names 62 | func paramNames(params *ast.FieldList) []string { 63 | var p []string 64 | for _, f := range params.List { 65 | for _, n := range f.Names { 66 | // we can't use _ as a name, so ignore it 67 | if n.Name != "_" { 68 | p = append(p, n.Name) 69 | } 70 | } 71 | } 72 | return p 73 | } 74 | 75 | func debugCall(fName string, pos token.Pos, args ...string) []byte { 76 | vals := make(map[string]string) 77 | if len(args) > 0 { 78 | vals["args"] = strings.Join(args, ", ") 79 | } else { 80 | vals["args"] = "" 81 | } 82 | 83 | if timing { 84 | vals["timing"] = "true" 85 | } 86 | 87 | vals["fname"] = fName 88 | 89 | if pos.IsValid() { 90 | vals["position"] = fset.Position(pos).String() 91 | } 92 | 93 | if showReturn { 94 | vals["return"] = "true" 95 | } 96 | 97 | var b bytes.Buffer 98 | err := funcTemplate.Execute(&b, vals) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | return b.Bytes() 103 | } 104 | 105 | type edit struct { 106 | pos int 107 | val []byte 108 | } 109 | 110 | type editList struct { 111 | edits []edit 112 | packageName string 113 | } 114 | 115 | func (e *editList) Add(pos int, val []byte) { 116 | e.edits = append(e.edits, edit{pos: pos, val: val}) 117 | } 118 | 119 | func (e *editList) inspect(node ast.Node) bool { 120 | if node == nil { 121 | return false 122 | } 123 | 124 | var pos token.Pos 125 | var funcType *ast.FuncType 126 | var body *ast.BlockStmt 127 | var funcName string 128 | 129 | switch n := node.(type) { 130 | case *ast.FuncDecl: 131 | body = n.Body 132 | if body == nil { 133 | return true 134 | } 135 | funcType = n.Type 136 | funcName = n.Name.Name 137 | 138 | // prepend our receiver type 139 | if n.Recv != nil && len(n.Recv.List) > 0 { 140 | switch t := n.Recv.List[0].Type.(type) { 141 | case *ast.StarExpr: 142 | funcName = t.X.(*ast.Ident).Name + "." + funcName 143 | case *ast.Ident: 144 | funcName = t.Name + "." + funcName 145 | } 146 | } 147 | 148 | case *ast.FuncLit: 149 | body = n.Body 150 | funcType = n.Type 151 | funcName = "func" 152 | pos = n.Pos() 153 | 154 | default: 155 | return true 156 | } 157 | 158 | if exportedOnly && !ast.IsExported(funcName) { 159 | return true 160 | } 161 | 162 | if showPackage { 163 | funcName = e.packageName + "." + funcName 164 | } 165 | 166 | if !filter.MatchString(funcName) { 167 | return true 168 | } 169 | 170 | if exclude != nil && exclude.MatchString(funcName) { 171 | return true 172 | } 173 | 174 | e.Add(int(body.Lbrace), debugCall(funcName, pos, paramNames(funcType.Params)...)) 175 | 176 | return true 177 | } 178 | 179 | func annotateFile(file string) { 180 | orig, err := ioutil.ReadFile(file) 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | 185 | src, err := annotate(file, orig) 186 | if err != nil { 187 | log.Printf("%s: skipping %s", err, file) 188 | return 189 | } 190 | 191 | if !writeFiles { 192 | fmt.Println(string(src)) 193 | return 194 | } 195 | 196 | err = ioutil.WriteFile(file, src, 0) 197 | if err != nil { 198 | log.Fatal(err) 199 | } 200 | } 201 | 202 | func annotate(filename string, orig []byte) ([]byte, error) { 203 | // we need to make sure the source is formmated to insert the new code in 204 | // the expected place 205 | orig, err := format.Source(orig) 206 | if err != nil { 207 | return orig, err 208 | } 209 | 210 | fset = token.NewFileSet() 211 | f, err := parser.ParseFile(fset, filename, orig, parser.ParseComments) 212 | if err != nil { 213 | return nil, err 214 | } 215 | 216 | for _, imp := range f.Imports { 217 | if imp.Name != nil && imp.Name.Name == importName { 218 | return nil, ErrAlreadyImported 219 | } 220 | if imp.Path.Value == importPath { 221 | return nil, ErrAlreadyImported 222 | } 223 | } 224 | 225 | edits := editList{packageName: f.Name.Name} 226 | 227 | // insert our import directly after the package line 228 | edits.Add(int(f.Name.End()), []byte(importStmt)) 229 | 230 | ast.Inspect(f, edits.inspect) 231 | 232 | var buf bytes.Buffer 233 | if err := format.Node(&buf, fset, f); err != nil { 234 | return nil, fmt.Errorf("format.Node: %s", err) 235 | } 236 | 237 | data := buf.Bytes() 238 | 239 | var pos int 240 | var out []byte 241 | for _, e := range edits.edits { 242 | out = append(out, data[pos:e.pos]...) 243 | out = append(out, []byte(e.val)...) 244 | pos = e.pos 245 | } 246 | out = append(out, data[pos:]...) 247 | 248 | // it's easier to append the setup code at the end 249 | out = append(out, []byte(setup)...) 250 | 251 | src, err := format.Source(out) 252 | if err != nil { 253 | return out, fmt.Errorf("format.Source: %s", err) 254 | } 255 | 256 | return src, nil 257 | } 258 | 259 | func init() { 260 | funcTemplate = template.Must(template.New("debug").Parse(tmpl)) 261 | } 262 | 263 | func main() { 264 | flag.BoolVar(&showReturn, "returns", false, "show function return") 265 | flag.BoolVar(&exportedOnly, "exported", false, "only annotate exported functions") 266 | flag.StringVar(&prefix, "prefix", "", "log prefix") 267 | flag.BoolVar(&showPackage, "package", false, "show package name prefix on function calls") 268 | flag.BoolVar(&writeFiles, "w", false, "re-write files in place") 269 | flag.StringVar(&filterFlag, "filter", ".", "only annotate functions matching the regular expression") 270 | flag.StringVar(&excludeFlag, "exclude", "", "exclude any matching functions, takes precedence over filter") 271 | flag.IntVar(&formatLength, "formatLength", 1024, "limit the formatted length of each argumnet to 'size'") 272 | flag.BoolVar(&timing, "timing", false, "print function durations. Implies -returns") 273 | flag.Parse() 274 | 275 | if flag.NArg() < 1 { 276 | flag.PrintDefaults() 277 | os.Exit(1) 278 | } 279 | 280 | setup = fmt.Sprintf(setup, prefix, formatLength) 281 | 282 | var err error 283 | filter, err = regexp.Compile(filterFlag) 284 | if err != nil { 285 | log.Fatal(err) 286 | } 287 | 288 | if excludeFlag != "" { 289 | exclude, err = regexp.Compile(excludeFlag) 290 | if err != nil { 291 | log.Fatal(err) 292 | } 293 | } 294 | 295 | if timing { 296 | showReturn = true 297 | } 298 | 299 | for _, file := range flag.Args() { 300 | annotateFile(file) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /trace_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "regexp" 7 | "testing" 8 | ) 9 | 10 | func init() { 11 | setup = fmt.Sprintf(setup, prefix, formatLength) 12 | 13 | var err error 14 | filter, err = regexp.Compile(filterFlag) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | 20 | var ( 21 | testSrc1 = []byte(`package none 22 | 23 | func testFunc1(a, b string) string { 24 | return a + b 25 | }`) 26 | 27 | testSrc2 = []byte(`package none 28 | 29 | func testFunc2(a, _ string) int { 30 | return 1 31 | }`) 32 | 33 | testSrc3 = []byte(`package none 34 | 35 | func testFunc3(a ...interface{}) bool { 36 | return true 37 | }`) 38 | 39 | testSrc4 = []byte(`package none 40 | func testFunc4(a, b int) {return false}`) 41 | 42 | testSrc5 = []byte(`package none 43 | func testFunc5(a, b int) { 44 | func(a int) { 45 | a = b 46 | }(a) 47 | }`) 48 | ) 49 | 50 | func annotateTest(source []byte, t *testing.T) []byte { 51 | processed, err := annotate("test.go", source) 52 | if err != nil { 53 | fmt.Println(string(processed)) 54 | t.Fatal(err) 55 | } 56 | t.Log(string(processed)) 57 | return processed 58 | } 59 | 60 | func returnsOn() func() { 61 | prev := showReturn 62 | showReturn = true 63 | return func() { 64 | showReturn = prev 65 | } 66 | } 67 | 68 | func timingOn() func() { 69 | prevT := timing 70 | timing = true 71 | prevR := showReturn 72 | showReturn = true 73 | return func() { 74 | timing = prevT 75 | showReturn = prevR 76 | } 77 | } 78 | 79 | // TODO: Use go/types to further check the output, since we don't execute the 80 | // new source. 81 | // I'll add that once go/types is moved into the main repo 82 | 83 | // Check the syntax on a basic function 84 | func TestBasic(t *testing.T) { 85 | annotateTest(testSrc1, t) 86 | } 87 | 88 | // Check that we don't fail on an un-named argument 89 | func TestUnderscore(t *testing.T) { 90 | annotateTest(testSrc2, t) 91 | } 92 | 93 | // We should handle variadic just fine 94 | func TestVariadic(t *testing.T) { 95 | annotateTest(testSrc3, t) 96 | } 97 | 98 | // Make sure we can handle improperly formatted source 99 | func TestUnFmted(t *testing.T) { 100 | annotateTest(testSrc4, t) 101 | } 102 | 103 | // test output with return logging 104 | func TestReturns(t *testing.T) { 105 | defer returnsOn()() 106 | annotateTest(testSrc1, t) 107 | } 108 | 109 | func TestTiming(t *testing.T) { 110 | defer timingOn()() 111 | annotateTest(testSrc4, t) 112 | } 113 | 114 | func TestEmbedded(t *testing.T) { 115 | defer timingOn()() 116 | annotateTest(testSrc5, t) 117 | } 118 | --------------------------------------------------------------------------------