Skip to main content

Logging

Arrower recommends using slog.Logger in your application.
It comes with its own implementation of slog.Handler, to add interesting extra functionalities.

Introduction

logger := alog.New()
logger := alog.NewDevelopment(pgx)
logger := alog.Test(t)
EnvironmentConstructorKey Features
productionNew
  • Defaults to level INFO
  • Writes to Stderr
  • Formats in JSON
developmentNewDevelopment
  • Defaults to level DEBUG
  • Writes to Stderr
  • Sends logs to local loki & postgres, if available
testingTest
  • Defaults to level DEBUG
  • Writes to buffer
  • Semantic assertions for the log output

Available Handlers

A logger can take one or multiple handlers that it writes to simultaneously. You can bring and use your own handler(s). Any slog Handlers will work. This gives you more control over your logging needs, compared to one of the default loggers from above.

logger := alog.New(
alog.WithHandler(h0),
alog.WithHandler(h1),
alog.WithLevel(slog.LevelDebug),
)
info

The level of a given handler is ignored and the level of the logger is used for all handlers instead.

NameDescription
slog.NewTextHandlerThe standard libraries handler
slog.NewJSONHandlerThe standard libraries handler
slog.DiscardHandlerThe standard libraries handler
alog.NewLokiHandlerUse this for local development only!
Sends all logs to a loki instance
alog.NewPostgresHandlerUse this for local development only!
Sends all logs to postgres

Runtime Configuration

To conveniently debug issues, the logger supports changing some properties at runtime.

Change The Log Level

To change the log level of a logger at runtime while it is in use:

alog.Unwrap(logger).SetLevel(slog.LevelDebug)

The log level will be changed for all handlers, independent of their specific configuration. Changing the log level at runtime doesn't require the creation of a new logger and your existing dependencies stay valid.

Settings

To influence the logger, even if multiple instances run on different machines, settings can be used to dynamically change behaviour at runtime.
Using the e.g. the postgres settings store, changes in settings are reflected immediately for subsequent requests.

If used/enabled, it impacts the performance of the logger!

// SettingLogLevel // sets the log level across loggers
// SettingLogUsers // shows all logs for the given users, independent of the log level

// dependency setup
settings := setting.NewInMemorySettings()

logger := alog.New(
alog.WithLevel(slog.LevelInfo),
alog.WithSettings(settings),
)

// change setting at run time
settings.Save(ctx, alog.SettingLogUsers, setting.NewValue([]string{userID}))

// http request path
ctx := context.WithValue(ctx, auth.CtxUserID, userID)
logger.DebugContext(ctx, "debug") // appears, although the logger's level is INFO

Writing Log Messages

The Go community has struggled for some time to find good logger interfaces. Check out Dave Cheney's post Let's talk about logging where he makes a compelling argument to only log two things:

  • Things that developers care about when they are developing or debugging software. => DEBUG
  • Things that users care about when using your software. => INFO

slog Logger Interface

Arrower returns always an slog.Logger for logging. So you can use the known API, and all the available methods that Go is offering.

One important consideration though: It is recommended to give the context to the methods, so use Log(), LogAttrs(), or InfoCtx() over Info(). The context carries information to correlate the logs with traces.

alog Logger Interface

Arrower recommends you the use the slog.Logger interface. You probably don't want to bind your code to our logger interface.

That said, the project itself uses a more restricted subset of the slog interface, that:

  1. encourages the use of methods taking context.Context, so that tracing information can be correlated
  2. encourages the use of the levels DEBUG and INFO, without preventing the others
type Logger interface {
Log(ctx context.Context, level slog.Level, msg string, args ...any)
LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr)
DebugCtx(ctx context.Context, msg string, args ...any)
InfoCtx(ctx context.Context, msg string, args ...any)
}

Log Level

Arrower works with the standard slog levels. That also means you can define your own log levels. Arrower uses the following two log levels internally, leaving you some space to define your own in between, if desired.

const (
// LevelInfo is used to see what is going on inside Arrower.
LevelInfo = slog.Level(-8)
// LevelDebug is used by Arrower developers, if you really want to know what is going on.
LevelDebug = slog.Level(-12)
)

Request Specific Attributes

Sometimes it is necessary to log request-specific information, but the logger is usually injected as a dependency, so this is difficult. You could put a logger into the context and hand it down to the place where it is used, or you add the attributes as request scoped data only.

All attributes added to the context are logged automatically by the Arrower logger, provided you use a context aware method.

ctx = alog.AddAttr(ctx, slog.String("my", "attr"))
ctx = alog.AddAttrs(ctx, slog.String("my", "attr"), slog.String("other", "attr"))

Testing

To ensure what got logged during testing time is easy. alog comes with a set of semantic assertions and fails your test cases.

func TestMyService(t *testing.T) {
logger := alog.Test(t)

// myService(logger)
// myService.MethodUnderTest()

logger.NotEmpty() // assert the logger is not empty
t.Log(logger.String()) // print all log lines
}

Correlate With Tracing

Your web applications might span multiple machines. To make it easy to trace down a "request", arrower adds the traceID and spanID automatically to each output, if present in the context.

Improve Docs

Screenshot how logs have IDs

In return each log is also recorded as an event in the span, to make it easier to debug potentially issues.

Improve Docs

Screenshot how traces have logs with all attributes

For more on tracing, see traces.