github.com/gobwas/gtrace@v0.4.3/README.md (about) 1 # gtrace 2 3 [![CI][ci-badge]][ci-url] 4 5 Command line tool **gtrace** generates boilerplate code for Go components tracing (aka _instrumentation_). 6 7 ## Usage 8 9 As a developer of some module (whenever its library or application module) you should define _trace points_ (or _hooks_) which user of your code can then initialize with some function (aka _probes_) during runtime. 10 11 ### TL;DR 12 13 **gtrace** suggests you to use structures (tagged with `//gtrace:gen`) holding all _hooks_ related to your component and generates helper functions around them so that you can merge such structures and call the _hooks_ without any checks for `nil`. It also can generate context aware helpers to call _hooks_ associated with context. 14 15 Example of generated code [is here](examples/pinger/main_gtrace.go). 16 17 ### Basic 18 19 Lets assume that we have some package called `lib` and some `lib.Client` structure which holds `net.Conn` internally and pings it every time before making some request when user calls `Client.Do()`. 20 For the sake of simplicity lets not cover how dial, ping or any other thing happens. 21 22 ```go 23 type Client struct { 24 conn net.Conn 25 } 26 27 func (c *Client) Do(ctx context.Context) error { 28 if err := c.ping(ctx); err != nil { 29 return err 30 } 31 // Some client logic. 32 } 33 34 func (c *Client) ping(ctx context.Context) error { 35 return doPing(ctx, c.conn) 36 } 37 ``` 38 39 What if we need to write some logs right before and after ping happens? There are several ways to do it, but with **gtrace** we start by defining _trace points_ in our package: 40 41 ```go 42 package lib 43 44 type ClientTrace struct { 45 OnPing func() func(error) 46 } 47 48 type Client struct { 49 Trace ClientTrace 50 ... 51 } 52 ``` 53 54 That is, we export _hook functions_ for every code point that might be interesting for the _user_ of our package. The `ClientTrace` structure contains definitions of all _trace points_ for the `Client`. For this example it has only one point. It defines pair of _ping start_ and _ping done_ callbacks. A user of our package can use it like so: 55 56 ```go 57 c := lib.Client{ 58 Trace: ClientTrace{ 59 OnPing: func() { 60 log.Println("ping start") 61 return func(err error) { 62 log.Printf("ping done; err=%v", err) 63 } 64 }, 65 }, 66 } 67 ``` 68 69 How the `Client` should call that _hooks_? Well, thats the one of the reason of **gtrace** exists: it generates few useful (and very annoying to be manually typed) helpers to use this tracing approach. Lets do this: 70 71 ```go 72 package lib 73 74 //go:generate gtrace 75 76 //gtrace:gen 77 type ClientTrace struct { 78 OnPing func() func(error) 79 } 80 ``` 81 82 And after `go generate` we can instrument our pinging facility as this: 83 84 ```go 85 func (c *Client) ping(ctx context.Context) error { 86 done := c.Trace.onPing() // added this line. 87 err := doPing(ctx, c.conn) 88 done(err) // and this line. 89 return err 90 } 91 ``` 92 93 *grace* has generated that `lib.Client.onPing()` non-exported method which checks if appopriate _probe_ function is non-nil (as well as the returned _ping done_ callback). If any of the callbacks is nil it returns noop functions to avoid branching in the `Client.ping()` code. 94 95 ### Composing 96 97 Lets return to the user of our package and cover another feature that **gtrace** generates for us: _trace points composing_. Composing is about merging two structures of the same trace and resulting a third one which calls _hooks_ from both of them. It is useful when user wants to instrument our ping facility with different measure types (to write logs as well as measure call latency): 98 99 ```go 100 var t ClientTrace 101 t = t.Compose(ClientTrace{ 102 OnPing: func() { 103 log.Println("ping start") 104 return func(err error) { 105 log.Printf("ping done; err=%v", err) 106 } 107 }, 108 }) 109 t = t.Compose(ClientTrace{ 110 OnPing: func() { 111 start := time.Now() 112 return func(error) { 113 sendLatency(time.Since(start)) 114 } 115 }, 116 }) 117 c := lib.Client{ 118 Trace: t, 119 } 120 ``` 121 122 ### Context 123 124 _Trace points composing_ gives us additional way to instrument our package: a context based tracing. We can setup `ClientTrace` not for the whole `Client`, but for some particular context (and probably do this on some particular condition). To do this we should ask **gtrace** to generate context aware tracing: 125 126 ```go 127 //gtrace:gen 128 //gtrace:set context 129 type ClientTrace struct { 130 OnPing func() func(error) 131 } 132 ``` 133 134 After `go generate` command signature of `lib.Client.onPing` changed to `onPing(context.Context)`, as well as two additional _exported_ functions added: `lib.WithClientTrace()` and `lib.ContextClientTrace()`. The former is to associate some `ClientTrace` with some context; and the latter is to obtain associated `ClientTrace` from context. So on the `Client` side we should only pass the context to the `onPing()` method: 135 136 ```go 137 func (c *Client) ping(ctx context.Context) error { 138 done := c.Trace.onPing(ctx) // this line has changed. 139 err := doPing(ctx, c.conn) 140 done(err) 141 return err 142 } 143 ``` 144 145 And on the user side we can do this: 146 147 ```go 148 c := lib.Client{ 149 Trace: t, // Note that both traces are used. 150 } 151 // Send 100 requests with every 5th being instrumented additionally. 152 for i := 0; i < 100; i++ { 153 ctx := context.Background() 154 if i % 5 == 0 { 155 ctx = lib.WithClientTrace(ctx, ClientTrace{ 156 ... 157 }) 158 } 159 if err := c.Do(ctx); err != nil { 160 // handle error. 161 } 162 } 163 ``` 164 165 ### Shortcuts 166 167 Thats it for basic tracing. But usually _trace points_ define _hooks_ with number of arguments way bigger than one or two. In that case we can declare a struct holding all _hook's_ arguments instead: 168 169 ```go 170 type ClientTrace struct { 171 OnPing func(ClientTracePingStart) func(ClientTracePingDone) 172 } 173 ``` 174 175 This makes _hooks_ more readable and extensible. But it also makes calling such _hooks_ a bit more verbose: 176 177 ```go 178 func (c *Client) ping(ctx context.Context) error { 179 done := c.Trace.onPing(ClientTracePingStart{ 180 Foo: 1, 181 Bar: 2, 182 Baz: 3, 183 }) 184 err := doPing(ctx, c.conn) 185 done(ClientTracePingDone{ 186 Foo: 1, 187 Bar: 2, 188 Baz: 3, 189 Err: err, 190 }) 191 return err 192 } 193 ``` 194 195 **gtrace** can generate functions called **shortcuts** to call the _hook_ in more "flat" way: 196 197 ```go 198 //gtrace:gen 199 //gtrace:set shortcut 200 type ClientTrace struct { 201 OnPing func(ClientTracePingStart) func(ClientTracePingDone) 202 } 203 ``` 204 205 After `go generate` we able to call _hooks_ like this: 206 207 ```go 208 func (c *Client) ping(ctx context.Context) error { 209 done := clientTraceOnPing(c.Trace, 1, 2, 3) 210 err := doPing(ctx, c.conn) 211 done(1, 2, 3, err) 212 return err 213 } 214 ``` 215 216 ### Build Tags 217 218 **gtrace** can generate zero-cost tracing helpers when tracing of your app is optional. That is, your client code will remain the same -- composing traces with needed callbacks, calling non-exported versions of _hooks_ (or _shortcuts_) etc. But after compilation calling the tracing helpers would take no CPU time. 219 220 To do that, you can pass the `-tag` flag to `gtrace` binary, which will result generation of two `_gtrace` files -- one which will be used when compiling with `-tags gtrace`, and one with stubs. 221 222 > NOTE: **gtrace** also respects build constraints for GOOS and GOARCH. 223 224 ### Examples 225 226 For more details feel free to read the `examples` package of this repo as well as delve into `test/test_grace.go`. 227 228 [ci-badge]: https://github.com/gobwas/gtrace/workflows/CI/badge.svg 229 [ci-url]: https://github.com/gobwas/gtrace/actions?query=workflow%3ACI