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