Logging


An important aspect of writing microservices is having a good logging strategy. This is a pretty complex topic and goa makes as little assumption as possible about how the service implements it. However there needs to be some integration so that for example errors that are not caught by user code end up being logged properly.

The Logger Adapter

goa defines a minimal interface that it expects the logger to implement. This interface is LogAdapter and is defined as follows:

 type LogAdapter interface {
 	// Info logs an informational message.
 	Info(msg string, keyvals ...interface{})
 	// Error logs an error.
 	Error(msg string, keyvals ...interface{})
 	// New appends to the logger context and returns the updated logger adapter.
 	New(keyvals ...interface{}) LogAdapter
 }

The Info and Error methods implement structured logging by making it possible to pass in optional key/value pairs with each message. This results in structured log entries for loggers that support it. The adapters should take care of properly logging the context even if the actual logger does not support it similarly to how the standard logger adapter that is part of goa does it.

Middlewares can take advantage of the New method to instantiate adapters with additional logging context that gets logged with each log entry. Such additional context may contain a unique request id for example as done by the LogRequest middleware.

The log adapter itself is stored in the request context and can be retrieved using the ContextLogger function. goa also exposes the WithLogContext function which calls New on the adapter and creates a new context containing the resulting log adapter.

Usage in Services

Services should setup their logger like they need to - independently of goa. goa comes with a number of LogAdapter implementations for common logging packages including the standard logger, log15, logrus and the go-kit logger.

Once instantiated the service should inject its logger in goa using one of these adapters. Taking log15 as an example, this could look like the following:

// Create logger
logger := log.New("module", "app/server")
// Configure it
logger.SetHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat()))
// Inject it
service.WithLogger(goalog15.New(logger))

goa then takes care of setting up the logger context before the action handler gets invoked. The context may also be additionally configured by middleware such as the LogRequest middleware.

The code may take advantage of the logging context by using the accessor functions provided by each adapter package. Keeping with the log15 example:

logger := goalog15.Logger(ctx) // logger is a log15.Logger
logger.Warn("whoops", "value", 15)

The service code should not have to funnel all logging calls through goa. For example the database layer should probably not have to use the goa package at all. The idea to avoid the coupling is to define functions for each log method that combine the above:

// define logInfo, logWarn, and logError globally:
func logInfo(ctx context.Context, msg string, keyvals...interface{}) {
	goalog15.Logger(ctx).Info(msg, keyvals...)
}

// which can be used as in:
logInfo(ctx, "whoops", "value", 15)

The service should define the logging functions that make sense to it this way which allows it to take advantage of the context setup by goa without creating a strong coupling.

Usage in Middleware

Middlewares have access to the logger via the context and the ContextLogger function. They may use the returned LogAdapter to add to the logger context or write logs.

Alternatively middlewares may take advantage of the WithLogContext function to add to the logger context and use the LogInfo and LogError goa package functions to write logs.