This is Part II of a two-part post on Function Tracing in Go.
In Part I, we investigated building functions to allow us to log a function’s enter and exit. In this post, we will explore formalizing this code a little so we can wrap it in a neat library. We will also explore extending the tracer with a few options.
If you are too bored to read this, and just want to see the implementation for where this leads to, take a look at sabhiram/go-tracey on github.
Library? Isn’t that for books or something?
If you are not familiar with libraries in Go, take a gander here. It does a very good job of walking you through building a simple library with Go.
Jumping right into code, lets move our enter() and exit() functions into a separate file (in its own folder, lets call it “tracey”).
We will create a library by declaring a package tracey and moving the previous enter and exit functions here:
tracey/tracey.go
Now we can add a file to use the above package (pay attention to the path, I just add this to the parent dir for convenience).
foo.go
This will now produce the following when run with go run foo.go:
So far so good. Clearly writing out defer tracey.Exit(tracey.Enter()) is a bit of a pain, so next we will look at how to make this a bit nicer.
Look at all these exports!
So Go has some weird rules about what it exports (from structs, packages and what not). For more details check out this link.
To clean up our code, we can expose a single function New() which returns the Enter() and Exit() functions to the caller. This simplifies our code to the following:
tracey/tracey.go
This changes the usage in foo.go to something like this:
Running go run foo.go will produce the same output as before (really, I promise).
Options, options, options
Lets add some configuration options to the mix. Here is a struct which we will define some config parameters:
We will also need to modify New to accept a pointer to one such structure (or nil):
Since opts is a pointer, lets do some error handling up-front:
In the above code, we instantiate a local copy of the Options (which is default initialized), and then assign it to the value pointed to by opts (as long as it’s not nil). We then deal with setting the default value for the indent spacing. Note that since options is in the scope that contains the _enter() and _exit() functions - they will have access to its members.
Honor thy options!
Next up, we modify the _enter() and _exit() function to reflect the nesting of function calls. Also pay heed to options.currentDepth - we will use this to keep track of how many nested functions have been called.
We will also add the following helper functions within the scope of New:
_spacify() to return a string with the current depth worth of spaces
_incrementDepth() and _decrementDepth() to update currentDepth
Putting this all together we get the following for tracey/tracey.go:
Since we changed the signature to the New() function, lets update foo.go (and make it a little more compelling while we are at it):
This will produce (with go run foo.go):
If we wanted to change the options passed into tracey.New, all we would need to do is:
Which results in:
Where to go from here?
Here are some issues with the implementation so far:
Functions inside anonymous functions get assigned “func.ID” as their name where “ID” is n for the n-th anonymous function in a file. So perhaps the _enter() should accept an optional string to print.
It is not currently possible to pass _enter() a list of interfaces like we can with fmt.Printf()
We cannot (yet) customize the enter and exit messages
We cannot (yet) customize if the tracing is enabled or disabled
We cannot (yet) use a custom logger dump the trace messages to
Wrapping up
Whew, that was a fun journey. If you found this interesting and want to dig deeper, or use tracey like functionality in your go project, take a look at sabhiram/go-tracey.
The go-tracey library implements the above missing pieces and then some. There are comprehensive examples and unit-tests to validate all parts of the library’s functionality. Feedback welcome!