In order to get an idea of usage of our client library, it’s common for client libraries to define a custom User-Agent
header to identify what versions of the library are making network requests. While the header can be defined as a constant string with the version getting changed on each release, this is error-prone because it requires manual changes to keep the version up-to-date. For example, for our the initial implementation of
User-Agent
headers in planetscale-go
, we defined a libraryVersion
constant, which was then interpolated into a userAgent
constant. As shown below.
const (
libraryVersion = "v0.67.0"
userAgent = "planetscale-go/" + libraryVersion
)
// Variadic option for configuring the `User-Agent` header.
func WithUserAgent(userAgent string) ClientOption {
return func(c *Client) error {
c.UserAgent = fmt.Sprintf("%s %s", userAgent, c.UserAgent)
return nil
}
}
func (c *Client) newRequest(...) {
// ...
req.Header.Set("User-Agent", c.UserAgent)
// ...
}
Needless to say, after the initial implementation, nobody (including myself) remembered to update that libraryVersion
constant. And it went uncaught for two years until we needed to look at these requests for something and we noticed that
the User-Agent
header was still stuck at v0.67.0
. In reality, the latest version was v0.110.0
, so a lot of changes
had occurred since then.
Noticing this, I realized that I needed to correct my mistake and improve upon this. After chatting with a coworker, he mentioned
that Go implemented first-class support for embedding version information within binaries, so that sounded promising.
After some searching, I found out that Go 1.18 added the debug.BuildInfo
struct, which
populates a lot of information about the Go program, such as the version, checksum, and even Git commit SHAs. Within
that struct, there is also the Deps
field, which contains similar information about the dependencies of the program,
including planetscale-go
.
With this new knowledge, I changed how the default User-Agent
was constructed. Instead of a constant string, we could
just find the planetscale-go
dependency and check the version from there. We wrap this in a sync.OnceValue
because
calling debug.ReadBuildInfo()
is expensive.
var defaultUserAgent = sync.OnceValue(func() string {
libraryVersion := "unknown"
buildInfo, ok := debug.ReadBuildInfo()
if ok {
for _, dep := range buildInfo.Deps {
if dep.Path == "github.com/planetscale/planetscale-go" {
libraryVersion = dep.Version
break
}
}
}
return "planetscale-go/" + libraryVersion
})
This gives us a more accurate User-Agent
header, taking out manual changes in order to make sure it is
up-to-date. If you define a specific version of a module like planetscale-go@v0.111.0
, that version will be defined in the header.
Note, we have to use the Deps
module in this case because planetscale-go
does not have a binary and is never built,
it is imported for usage in other Go programs, such
as the PlanetScale CLI. If we were to use the Main
field from debug.BuildInfo
, that
would give us information about the Go program that is using planetscale-go
, not planetscale-go
itself. You can see
the full pull request here.
If you wanted to use this approach in your own code, if it is a binary, you can check the Version
from within debug.BuildInfo.Main
if your program is being compiled into a binary. Otherwise, you can use snippet above, but ensuring that you are comparing dep.Path
to your Go module’s path. It’s as simple as that. Happy coding!