We at kintone.com are using Go for variety of tasks.
In early days when we started using Go, there were only few in-house rules to create commands. As a result, we faced several difficulties:
- Some servers did not record access logs.
- Some programs did not record command execution logs.
- No common spec to handle SIGINT or SIGTERM.
- Some programs did not reopen log files, making log rotation difficult.
- Inconsistent log formats.
It would have been better if they handled signals nicely and recorded logs consistently. More specifically, we think a command well-behaved if it:
- Stops gracefully after all running jobs completed upon SIGINT/SIGTERM.
- Restarts gracefully (or reloads configs) on SIGHUP.
- Re-opens log files on a signal, say SIGUSR1.
- Records logs in a consistent, machine-readable format.
- Records accesses to HTTP servers.
- Records results of command executions.
- Provides an option to change verbosity level of logging.
- Records logs with ID to track activities of a goroutine.
Looks good? The last item is difficult to understand for people who are not familiar with Go, so let me explain. Go runs jobs concurrently in so-called goroutines. Goroutines are not OS processes nor threads, and do not have identifiers of themselves. Hence we need to add our own ID.
To get straight to the point, such well-behaved commands can be created by using
github.com/cybozu-go/cmd
(hereinafter called cmd
).
How cmd
works
cmd
is built around context
package that appears new
in Go 1.7. context
is an excellent solution for problems like:
- Stop processing on a signal or a timeout whichever comes first.
- Propagate request-scope values between function calls.
By using context
properly, we can implement graceful stop and ID for goroutines.
The first thing cmd
provides is a kind of barrier synchronizer that can signal its
goroutines to interrupt processing at any time.
cmd.Environment
is it whose
Cancel(err error)
method cancels the internal context
to signal goroutines started
by its Go(func(ctx context.Context) error)
method.
import ( "context" "github.com/cybozu-go/cmd" "github.com/cybozu-go/log" ) func main() { env := cmd.NewEnvironment(context.Background()) // Go calls a function inside a goroutine. // If the function returns non-nil error, Cancel(err) // is called automatically. env.Go(func(ctx context.Context) error { for { select { case <-ctx.Done(): // gracefully stop when context is canceled. return nil default: } doSomething() } }) // more Go() calls... //env.Go()... env.Stop() // wait for all goroutines to return. err := env.Wait() if err != nil { log.ErrorExit(err) } }
cmd
offers a global cmd.Environment
and installs a signal handler
that calls Cancel
of the global environment when it gets SIGINT or SIGTERM.
By depending on contexts inherited from the global environment, you can
manage all essential goroutines in your programs to stop them gracefully.
Of course, cmd
already provides such implementations for network servers.
For instance, cmd.HTTPServer
is a wrapper for the standard http.Server
that adds graceful stop, access log, and unique request IDs.
func main() { // The only thing you need to do is to wrap http.Sever // inside cmd.HTTPServer. s := &cmd.HTTPServer{ Server: &http.Server{ Handler: http.FileServer(http.Dir("/path/to/files")), }, } // ListenAndServe is overridden to return soon after started // a goroutine with Go(). err := s.ListenAndServe() if err != nil { log.ErrorExit(err) } // Wait returns when SIGINT or SIGTERM is got and all ongoing // HTTP requests complete. err = cmd.Wait() // IsSignaled returns true if err comes from the signal handler. if err != nil && !cmd.IsSignaled(err) { log.ErrorExit(err) } }
Please visit the tutorial to
learn more about cmd
. There are plenty more useful things. You can read
about the tricks used in cmd.HTTPServer
in another article.
How log
works
We have not mention about logging. It is provided by another package
github.com/cybozu-go/log
(hereinafter called log
).
log
provides so-called structured logging in which a log consists of key-value pairs.
Users can add their own key-value pairs to make logs better explain the situation:
log.Error("message", map[string]interface{}{ "field1": []int{1, 2, 3}, "field2": time.Now(), })
Structured logs are often recorded in machine-readable formats. log
provides three
formatters: syslog-like "plain", logfmt, and
JSON Lines.
With cmd
, programs automatically implement command-line flags to specify:
- log filename,
- verbosity level of logging, and
- log format.
Commands built on cmd
We already adopted cmd
for all our open source Go products:
github.com/cybozu-go/transocks
This is a transparent proxifier similar to redsocks. It supports IPv4 and IPv6 and can connect to SOCK5 proxy or HTTP proxy (CONNECT).
-
usocksd/socks
is a general purpose SOCKS4/4a/5 server library.usocksd
is built on it as a handy SOCKS server. -
This is a simple monitoring tool that watches something and kicks actions. Monitoring rules can be added/removed by files, REST API, and CLI.
-
This replaces
apt-mirror
andapt-cacher-ng
for better consistency and reliability. Please read an article for details.
Summary and conclusion
github.com/cybozu-go/cmd
provides tools to make
well-behaved Go programs. Well-behaved here means programs can stop/restart gracefully
and record useful logs.
All our open-source products already using cmd
successfully. You can try it out now.