├── .github └── workflows │ ├── ci.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── interface.go ├── options.go ├── pdebug.go ├── pdebug_off.go ├── pdebug_off_test.go └── pdebug_on_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: [ '1.15', '1.14' ] 10 | name: Go ${{ matrix.go }} test 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | - name: Install Go stable version 15 | if: matrix.go != 'tip' 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ${{ matrix.go }} 19 | - name: Install Go tip 20 | if: matrix.go == 'tip' 21 | run: | 22 | git clone --depth=1 https://go.googlesource.com/go $HOME/gotip 23 | cd $HOME/gotip/src 24 | ./make.bash 25 | echo "::set-env name=GOROOT::$HOME/gotip" 26 | echo "::add-path::$HOME/gotip/bin" 27 | echo "::add-path::$(go env GOPATH)/bin" 28 | - name: Test 29 | run: go test -tags debug -v -race ./... 30 | - name: Test (With Build Tag) 31 | run: go test -v -race ./... 32 | - name: Upload code coverage to codecov 33 | if: matrix.go == '1.15' 34 | uses: codecov/codecov-action@v1 35 | with: 36 | file: ./coverage.out 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | # Run once without build tag 10 | - uses: golangci/golangci-lint-action@v2 11 | with: 12 | version: v1.34.1 13 | # Run once with build tag 14 | - uses: golangci/golangci-lint-action@v2 15 | with: 16 | version: v1.34.1 17 | args: --build-tags=debug0 18 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - path: /*_example_test.go 4 | linters: 5 | - errcheck 6 | - forbidigo 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 lestrrat 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github.com/lestrrat-go/pdebug ![](https://github.com/lestrrat-go/pdebug/workflows/CI/badge.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/pdebug.svg)](https://pkg.go.dev/github.com/lestrrat-go/pdebug) [![codecov.io](http://codecov.io/github/lestrrat-go/pdebug/coverage.svg?branch=master)](http://codecov.io/github/lestrrat-go/pdebug?branch=master) 2 | 3 | Utilities for my print debugging fun. YMMV 4 | 5 | # Synopsis 6 | 7 | ![optimized](https://pbs.twimg.com/media/CbiqhzLUUAIN_7o.png) 8 | 9 | # Description 10 | 11 | Building with `pdebug` declares a constant, `pdebug.Enabled` which you 12 | can use to easily compile in/out depending on the presence of a build tag. 13 | 14 | In the following example, the clause within `pdebug.Enabled` is compiled out 15 | because it is a constant boolean. 16 | 17 | ```go 18 | import "github.com/lestrrat-go/pdebug/v3" 19 | 20 | func Foo() { 21 | // will only be available if you compile with `-tags debug` 22 | if pdebug.Enabled { 23 | pdebug.Printf("Starting Foo()!") 24 | } 25 | } 26 | ``` 27 | 28 | To enable the prints, simply compile with the `debug` tag. 29 | 30 | # Markers 31 | 32 | When you want to print debug a chain of function calls, you can use the 33 | `Marker` functions: 34 | 35 | ```go 36 | func Foo() { 37 | if pdebug.Enabled { 38 | g := pdebug.FuncMarker() 39 | defer g.End() 40 | } 41 | 42 | pdebug.Printf("Inside Foo()!") 43 | } 44 | ``` 45 | 46 | This will cause all of the `Printf` calls to automatically indent 47 | the output so it's visually easier to see where a certain trace log 48 | is being generated. 49 | 50 | By default it will print something like: 51 | 52 | ``` 53 | |DEBUG| 123456789.0000 START github.com/lestrrat-go/pdebug.Foo 54 | |DEBUG| 123456789.0000 Inside Foo()! 55 | |DEBUG| 123456789.0000 END github.com/lestrrat-go/pdebug.Foo (elapsed=1.23s) 56 | ``` 57 | 58 | If you want to automatically show the error value you are returning 59 | (but only if there is an error), you can use the `BindError` method: 60 | 61 | ```go 62 | import "github.com/lestrrat-go/pdebug/v3" 63 | 64 | func Foo() (err error) { 65 | if pdebug.Enabled { 66 | g := pdebug.FuncMarker().BindError(&err) 67 | defer g.End() 68 | } 69 | 70 | pdebug.Printf("Inside Foo()!") 71 | 72 | return errors.New("boo") 73 | } 74 | ``` 75 | 76 | This will print something like: 77 | 78 | ``` 79 | |DEBUG| 123456789.0000 START github.com/lestrrat-go/pdebug.Foo 80 | |DEBUG| 123456789.0000 Inside Foo()! 81 | |DEBUG| 123456789.0000 END github.com/lestrrat-go/pdebug.Foo (elapsed=1.23s, rror=boo) 82 | ``` 83 | 84 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lestrrat-go/pdebug/v3 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10= 4 | github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 9 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | package pdebug 2 | 3 | import "time" 4 | 5 | type MarkerGuard interface { 6 | BindError(*error) MarkerGuard 7 | End() 8 | } 9 | 10 | type Clock interface { 11 | Now() time.Time 12 | } 13 | 14 | type ClockFunc func() time.Time 15 | 16 | func (cf ClockFunc) Now() time.Time { 17 | return cf() 18 | } 19 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package pdebug 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/lestrrat-go/option" 7 | ) 8 | 9 | type Option = option.Interface 10 | 11 | type identClock struct{} 12 | type identWriter struct{} 13 | 14 | func WithClock(v Clock) Option { 15 | return option.New(identClock{}, v) 16 | } 17 | 18 | func WithWriter(v io.Writer) Option { 19 | return option.New(identWriter{}, v) 20 | } 21 | -------------------------------------------------------------------------------- /pdebug.go: -------------------------------------------------------------------------------- 1 | // +build debug0 or debug 2 | 3 | package pdebug 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "os" 11 | "runtime" 12 | "strconv" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | const Enabled = true 18 | const indentPerLevel = 2 19 | 20 | func init() { 21 | Trace = true 22 | } 23 | 24 | type state struct { 25 | clock Clock 26 | indent int 27 | mu sync.RWMutex 28 | out io.Writer 29 | prefix []byte 30 | } 31 | 32 | type mGuard struct { 33 | errptr *error 34 | indent int 35 | msgFormat string 36 | msgArgs []interface{} 37 | start time.Time 38 | } 39 | 40 | var Trace bool 41 | var st = &state{ 42 | clock: ClockFunc(time.Now), 43 | out: os.Stderr, 44 | prefix: []byte("|DEBUG|"), 45 | } 46 | 47 | func Configure(options ...Option) { 48 | var clock Clock 49 | var output io.Writer 50 | for _, option := range options { 51 | switch option.Ident() { 52 | case identClock{}: 53 | clock = option.Value().(Clock) 54 | case identWriter{}: 55 | output = option.Value().(io.Writer) 56 | } 57 | } 58 | 59 | st.mu.Lock() 60 | st.clock = clock 61 | st.out = output 62 | st.mu.Unlock() 63 | } 64 | 65 | func FuncMarker() MarkerGuard { 66 | pc, _, _, ok := runtime.Caller(1) 67 | if !ok { 68 | panic("pdebug.FuncMarker could not determine the name of caller function") 69 | } 70 | f := runtime.FuncForPC(pc) 71 | return Marker(f.Name()) 72 | } 73 | 74 | var mGuardPool = sync.Pool{ 75 | New: allocMGuard, 76 | } 77 | 78 | func allocMGuard() interface{} { 79 | return &mGuard{} 80 | } 81 | 82 | func getMGuard() *mGuard { 83 | return mGuardPool.Get().(*mGuard) 84 | } 85 | 86 | func releaseMGuard(mg *mGuard) { 87 | mg.indent = 0 88 | mg.msgFormat = "" 89 | mg.msgArgs = nil 90 | mGuardPool.Put(mg) 91 | } 92 | 93 | func Marker(format string, args ...interface{}) MarkerGuard { 94 | if !Trace { 95 | return nil 96 | } 97 | 98 | var clock Clock 99 | var indent int 100 | var prefix []byte 101 | st.mu.RLock() 102 | clock = st.clock 103 | prefix = st.prefix 104 | indent = st.indent 105 | st.mu.RUnlock() 106 | 107 | mg := getMGuard() 108 | mg.indent = indent 109 | mg.msgFormat = format 110 | mg.msgArgs = args 111 | 112 | if clock := st.clock; clock != nil { 113 | mg.start = clock.Now() 114 | } 115 | 116 | var buf []byte 117 | formatMarkerMessage(&buf, "START "+mg.msgFormat, mg.msgArgs, prefix, nil, clock, mg.indent) 118 | 119 | st.mu.Lock() 120 | _, _ = st.out.Write(buf) 121 | st.indent += indentPerLevel 122 | st.mu.Unlock() 123 | return mg 124 | } 125 | 126 | func (mg *mGuard) BindError(err *error) MarkerGuard { 127 | if !Trace { 128 | return nil 129 | } 130 | 131 | mg.errptr = err 132 | return mg 133 | } 134 | 135 | func (mg *mGuard) End() { 136 | if !Trace { 137 | return 138 | } 139 | 140 | var clock Clock 141 | var prefix []byte 142 | st.mu.Lock() 143 | st.indent -= indentPerLevel 144 | if st.indent < 0 { 145 | st.indent = 0 146 | } 147 | clock = st.clock 148 | prefix = st.prefix 149 | st.mu.Unlock() 150 | 151 | var postfix []byte 152 | 153 | if clock != nil || mg.errptr != nil { 154 | postfix = append(postfix, '(') 155 | if clock != nil { 156 | postfix = append(postfix, []byte("elapsed=")...) 157 | postfix = append(postfix, []byte(clock.Now().Sub(mg.start).String())...) 158 | } 159 | 160 | if errptr := mg.errptr; errptr != nil && *errptr != nil { 161 | if clock != nil { 162 | postfix = append(postfix, ", "...) 163 | } 164 | postfix = append(postfix, "error="...) 165 | postfix = append(postfix, []byte((*errptr).Error())...) 166 | } 167 | postfix = append(postfix, ')') 168 | } 169 | 170 | var buf []byte 171 | formatMarkerMessage(&buf, "END "+mg.msgFormat, mg.msgArgs, prefix, postfix, clock, mg.indent) 172 | 173 | st.mu.Lock() 174 | _, _ = st.out.Write(buf) 175 | st.mu.Unlock() 176 | 177 | releaseMGuard(mg) 178 | } 179 | 180 | func formatMarkerMessage(buf *[]byte, format string, args []interface{}, prefix, postfix []byte, clock Clock, indent int) { 181 | // foo\nbar\n should be written as preamble foo\npreamble bar\n 182 | var scratch bytes.Buffer 183 | fmt.Fprintf(&scratch, format, args...) 184 | 185 | scanner := bufio.NewScanner(&scratch) 186 | for scanner.Scan() { 187 | appendPreamble(buf, prefix, clock, indent) 188 | line := scanner.Bytes() 189 | *buf = append(*buf, line...) 190 | *buf = append(*buf, postfix...) 191 | *buf = append(*buf, '\n') 192 | } 193 | } 194 | 195 | func appendPreamble(buf *[]byte, prefix []byte, clock Clock, indent int) { 196 | *buf = append(*buf, prefix...) 197 | 198 | *buf = append(*buf, ' ') 199 | 200 | if clock != nil { 201 | *buf = append(*buf, 202 | []byte(strconv.FormatFloat(float64(clock.Now().UnixNano())/1000000.0, 'f', 5, 64))..., 203 | ) 204 | *buf = append(*buf, ' ') 205 | } 206 | for i := 0; i < indent; i++ { 207 | *buf = append(*buf, ' ') 208 | } 209 | } 210 | 211 | func Printf(format string, args ...interface{}) { 212 | if !Trace { 213 | return 214 | } 215 | 216 | var buf []byte 217 | var clock Clock 218 | var prefix []byte 219 | var indent int 220 | st.mu.RLock() 221 | clock = st.clock 222 | prefix = st.prefix 223 | indent = st.indent 224 | st.mu.RUnlock() 225 | 226 | formatMarkerMessage(&buf, format, args, prefix, nil, clock, indent) 227 | 228 | st.mu.Lock() 229 | _, _ = st.out.Write(buf) 230 | st.mu.Unlock() 231 | } 232 | -------------------------------------------------------------------------------- /pdebug_off.go: -------------------------------------------------------------------------------- 1 | // +build !debug0,!debug 2 | 3 | package pdebug 4 | 5 | const Enabled = false 6 | const Trace = false 7 | 8 | type nullMGuard struct {} 9 | 10 | func (g nullMGuard) BindError(_ *error) MarkerGuard { return g } 11 | func (_ nullMGuard) End() {} 12 | 13 | func FuncMarker() MarkerGuard { return nullMGuard{} } 14 | func Marker(_ string, _ ...interface{}) MarkerGuard { return nullMGuard{} } 15 | func Printf(_ string, _ ...interface{}) {} 16 | -------------------------------------------------------------------------------- /pdebug_off_test.go: -------------------------------------------------------------------------------- 1 | // +build !debug0,!debug 2 | 3 | package pdebug_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/lestrrat-go/pdebug/v3" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestInterface(t *testing.T) { 13 | // If we fail to provide this API, this test should fail to compile 14 | _ = pdebug.Marker 15 | _ = pdebug.FuncMarker 16 | _ = pdebug.Printf 17 | } 18 | 19 | func TestDisabled(t *testing.T) { 20 | if !assert.False(t, pdebug.Enabled) { 21 | return 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pdebug_on_test.go: -------------------------------------------------------------------------------- 1 | // +build debug0 or debug 2 | 3 | package pdebug_test 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/lestrrat-go/pdebug/v3" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestMarker(t *testing.T) { 17 | fn := func(t *testing.T, wg *sync.WaitGroup) { 18 | t.Helper() 19 | if wg != nil { 20 | defer wg.Done() 21 | } 22 | var err error 23 | g1 := pdebug.FuncMarker().BindError(&err) 24 | defer g1.End() 25 | 26 | pdebug.Printf("Hello, World test 1") 27 | 28 | g2 := pdebug.Marker("Test") 29 | defer g2.End() 30 | 31 | pdebug.Printf("Hello, World test 2") 32 | err = errors.New("test 1 error") 33 | } 34 | 35 | var buf bytes.Buffer 36 | var now = time.Unix(0, 0) 37 | pdebug.Configure( 38 | pdebug.WithClock(pdebug.ClockFunc(func() time.Time { return now })), 39 | pdebug.WithWriter(&buf), 40 | ) 41 | 42 | t.Run("Output format", func(t *testing.T) { 43 | fn(t, nil) 44 | t.Logf("%s", buf.String()) 45 | if pdebug.Enabled && pdebug.Trace { 46 | const expected = `|DEBUG| 0.00000 START github.com/lestrrat-go/pdebug/v3_test.TestMarker.func1 47 | |DEBUG| 0.00000 Hello, World test 1 48 | |DEBUG| 0.00000 START Test 49 | |DEBUG| 0.00000 Hello, World test 2 50 | |DEBUG| 0.00000 END Test(elapsed=0s) 51 | |DEBUG| 0.00000 END github.com/lestrrat-go/pdebug/v3_test.TestMarker.func1(elapsed=0s, error=test 1 error) 52 | ` 53 | if !assert.Equal(t, expected, buf.String()) { 54 | return 55 | } 56 | } 57 | }) 58 | t.Run("Race condition", func(t *testing.T) { 59 | // Make sure race conditions don't exist by calling multiple goroutines 60 | const gcount = 10 61 | 62 | var wg sync.WaitGroup 63 | wg.Add(gcount) 64 | for i := 0; i < gcount; i++ { 65 | go fn(t, &wg) 66 | } 67 | wg.Wait() 68 | }) 69 | 70 | } 71 | --------------------------------------------------------------------------------