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!