slog

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 10, 2019 License: MIT Imports: 13 Imported by: 179

README

slog

GitHub Release GoDoc Coveralls CI Status

slog is a minimal structured logging library for Go.

Install

go get cdr.dev/slog

Features

Example

Many more examples available at godoc.

log := sloghuman.Make(os.Stdout)

log.Info(context.Background(), "my message here",
    slog.F("field_name", "something or the other"),
    slog.F("some_map", slog.M(
        slog.F("nested_fields", "wowow"),
    )),
    slog.Error(
        xerrors.Errorf("wrap1: %w",
            xerrors.Errorf("wrap2: %w",
                io.EOF,
            ),
        ),
    ),
)

Example output screenshot

Why?

The logging library of choice at Coder has been Uber's zap for several years now.

It's a fantastic library for performance but the API and developer experience is not great.

Here is a list of reasons how we improved on zap with slog.

  1. slog has a minimal API surface

    • Compare slog to zap and zapcore.
    • The sprawling API makes zap hard to understand, use and extend.
  2. slog has a concise semi typed API

    • We found zap's fully typed API cumbersome. It does offer a sugared API but it's too easy to pass an invalid fields list since there is no static type checking. Furthermore, it's harder to read as there is no syntax grouping for each key value pair.
    • We wanted an API that only accepted the equivalent of zap.Any for every field. This is slog.F.
  3. sloghuman uses a very human readable format

    • It colors distinct parts of each line to make it easier to scan logs. Even the JSON that represents the fields in each log is syntax highlighted so that is very easy to scan. See the screenshot above.
      • zap lacks appropriate colors for different levels and fields
    • slog automatically prints one multiline field after the log to make errors and such much easier to read.
      • zap logs multiline fields and errors stack traces as JSON strings which made them unreadable in a terminal.
    • When logging to JSON, slog automatically converts a golang.org/x/xerrors chain into an array with fields for the location and wrapping messages.
  4. Full context.Context support

    • slog lets you set fields in a context.Context such that any log with the context prints those fields.
    • We wanted to be able to pull up all relevant logs for a given trace, user or request. With zap, we were plugging these fields in for every relevant log or passing around a logger with the fields set. This became very verbose.
  5. Simple and easy to extend

    • A new backend only has to implement the simple Sink interface.
    • The Logger type provides a nice API around Sink but also implements Sink to allow for composition.
    • zap is hard and confusing to extend. There are too many structures and configuration options.
  6. Structured logging of Go structures with json.Marshal

    • All values will be logged with json.Marshal unless they implement fmt.Stringer or error.
    • One may implement slog.Value to override the representation completely.
    • With zap, We found ourselves often implementing zap's ObjectMarshaler to log Go structures. This was verbose and most of the time we ended up only implementing fmt.Stringer and using zap.Stringer instead.
  7. slog takes inspiration from Go's stdlib and implements slog.Helper which works just like t.Helper

    • It marks the calling function as a helper and skips it when reporting location info.
    • We had many helper functions for logging but we wanted the line reported to be of the parent function. zap has an API for this but it's verbose and requires passing the logger around explicitly.
  8. Tight integration with stdlib's testing package

    • You can configure slogtest to exit on any ERROR logs and it has a global stateless API that takes a testing.TB so you do not need to create a logger first.
    • Test assertion helpers are provided in slogtest/assert.
    • zap has zaptest but the API surface is large and doesn't integrate well. It does not support any of the features described above.

Documentation

Overview

Package slog implements minimal structured logging.

See https://cdr.dev/slog for overview docs and a comparison with existing libraries.

The examples are the best way to understand how to use this library effectively.

The Logger type implements a high level API around the Sink interface. Logger implements Sink as well to allow composition.

Implementations of the Sink interface are available in the sloggers subdirectory.

Example
package main

import (
	"context"
	"io"
	"os"

	"golang.org/x/xerrors"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	log := sloghuman.Make(os.Stdout)

	log.Info(context.Background(), "my message here",
		slog.F("field_name", "something or the other"),
		slog.F("some_map", slog.M(
			slog.F("nested_fields", "wowow"),
		)),
		slog.Error(
			xerrors.Errorf("wrap1: %w",
				xerrors.Errorf("wrap2: %w",
					io.EOF,
				),
			),
		),
	)

	// 2019-12-09 05:04:53.398 [INFO]	<example.go:16>	my message here	{"field_name": "something or the other", "some_map": {"nested_fields": "wowow"}} ...
	//  "error": wrap1:
	//      main.main
	//          /Users/nhooyr/src/cdr/scratch/example.go:22
	//    - wrap2:
	//      main.main
	//          /Users/nhooyr/src/cdr/scratch/example.go:23
	//    - EOF
}
Output:

Example (Multiple)
package main

import (
	"context"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
	"cdr.dev/slog/sloggers/slogstackdriver"
)

func main() {
	ctx := context.Background()
	l := sloghuman.Make(os.Stdout)

	f, err := os.OpenFile("stackdriver", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
	if err != nil {
		l.Fatal(ctx, "failed to open stackdriver log file", slog.Error(err))
	}

	l = slog.Make(l, slogstackdriver.Make(f))

	l.Info(ctx, "log to stdout and stackdriver")

	// 2019-12-07 20:59:55.790 [INFO]	<example_test.go:46>	log to stdout and stackdriver
}
Output:

Example (Testing)
package main

import (
	"testing"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/slogtest"
)

func main() {
	// Provided by the testing package in tests.
	var t testing.TB

	slogtest.Info(t, "my message here",
		slog.F("field_name", "something or the other"),
	)

	// t.go:55: 2019-12-05 21:20:31.218 [INFO]	<examples_test.go:42>	my message here	{"field_name": "something or the other"}
}
Output:

Example (Tracing)
package main

import (
	"context"
	"os"

	"go.opencensus.io/trace"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	log := sloghuman.Make(os.Stdout)

	ctx, _ := trace.StartSpan(context.Background(), "spanName")

	log.Info(ctx, "my msg", slog.F("hello", "hi"))

	// 2019-12-09 21:59:48.110 [INFO]	<example_test.go:62>	my msg	{"trace": "f143d018d00de835688453d8dc55c9fd", "span": "f214167bf550afc3", "hello": "hi"}
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ForceJSON

func ForceJSON(v interface{}) interface{}

ForceJSON ensures the value is logged via json.Marshal even if it implements fmt.Stringer or error.

Example
package main

import (
	"context"
	"fmt"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

type stringer struct {
	X int `json:"x"`
}

func (s *stringer) String() string {
	return fmt.Sprintf("string method: %v", s.X)
}

func (s *stringer) SlogValue() interface{} {
	return slog.ForceJSON(s)
}

func main() {
	l := sloghuman.Make(os.Stdout)

	l.Info(context.Background(), "hello", slog.F("stringer", &stringer{X: 3}))

	// 2019-12-06 23:33:40.263 [INFO]	<example_force_json_test.go:27>	hello	{"stringer": {"x": 3}}
}
Output:

func Helper

func Helper()

Helper marks the calling function as a helper and skips it for source location information. It's the slog equivalent of testing.TB.Helper().

Example
package main

import (
	"context"
	"net/http"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func httpLogHelper(ctx context.Context, status int) {
	slog.Helper()

	l.Info(ctx, "sending HTTP response",
		slog.F("status", status),
	)
}

var l = sloghuman.Make(os.Stdout)

func main() {
	ctx := context.Background()
	httpLogHelper(ctx, http.StatusBadGateway)

	// 2019-12-07 21:18:42.567 [INFO]	<example_helper_test.go:24>	sending HTTP response	{"status": 502}
}
Output:

func Stdlib

func Stdlib(ctx context.Context, l Logger) *log.Logger

Stdlib creates a standard library logger from the given logger.

All logs will be logged at the Info level and the given ctx will be passed to the logger's Info method, thereby logging all fields and tracing info in the context.

You can redirect the stdlib default logger with log.SetOutput to the Writer on the logger returned by this function. See the example.

Example
package main

import (
	"context"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	ctx := slog.With(context.Background(), slog.F("field", 1))
	l := slog.Stdlib(ctx, sloghuman.Make(os.Stdout))

	l.Print("msg")

	// 2019-12-07 20:54:23.986 [INFO]	(stdlib)	<example_test.go:29>	msg	{"field": 1}
}
Output:

func With

func With(ctx context.Context, fields ...Field) context.Context

With returns a context that contains the given fields.

Any logs written with the provided context will have the given logs prepended.

It will append to any fields already in ctx.

Example
package main

import (
	"context"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	ctx := slog.With(context.Background(), slog.F("field", 1))

	l := sloghuman.Make(os.Stdout)
	l.Info(ctx, "msg")

	// 2019-12-07 20:54:23.986 [INFO]	<example_test.go:20>	msg	{"field": 1}
}
Output:

Types

type Field

type Field struct {
	Name  string
	Value interface{}
}

Field represents a log field.

func Error

func Error(err error) Field

Error is the standard key used for logging a Go error value.

func F

func F(name string, value interface{}) Field

F is a convenience constructor for Field.

type Level

type Level int

Level represents a log level.

const (
	LevelDebug Level = iota
	LevelInfo
	LevelWarn
	LevelError
	LevelCritical
	LevelFatal
)

The supported log levels.

The default level is Info.

func (Level) String

func (l Level) String() string

String implements fmt.Stringer.

type Logger

type Logger struct {
	// contains filtered or unexported fields
}

Logger wraps Sink with a nice API to log entries.

Logger is safe for concurrent use.

func Make

func Make(sinks ...Sink) Logger

Make creates a logger that writes logs to the passed sinks at LevelInfo.

func (Logger) Critical

func (l Logger) Critical(ctx context.Context, msg string, fields ...Field)

Critical logs the msg and fields at LevelCritical.

It will then Sync().

func (Logger) Debug

func (l Logger) Debug(ctx context.Context, msg string, fields ...Field)

Debug logs the msg and fields at LevelDebug.

func (Logger) Error

func (l Logger) Error(ctx context.Context, msg string, fields ...Field)

Error logs the msg and fields at LevelError.

It will then Sync().

func (Logger) Fatal

func (l Logger) Fatal(ctx context.Context, msg string, fields ...Field)

Fatal logs the msg and fields at LevelFatal.

It will then Sync() and os.Exit(1).

func (Logger) Info

func (l Logger) Info(ctx context.Context, msg string, fields ...Field)

Info logs the msg and fields at LevelInfo.

func (Logger) Leveled added in v0.5.0

func (l Logger) Leveled(level Level) Logger

Leveled returns a Logger that only logs entries equal to or above the given level.

Example
package main

import (
	"context"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	ctx := context.Background()

	l := sloghuman.Make(os.Stdout)
	l.Debug(ctx, "testing1")
	l.Info(ctx, "received request")

	l = l.Leveled(slog.LevelDebug)

	l.Debug(ctx, "testing2")

	// 2019-12-07 21:26:20.945 [INFO]	<example_test.go:95>	received request
	// 2019-12-07 21:26:20.945 [DEBUG]	<example_test.go:99>	testing2
}
Output:

func (Logger) LogEntry added in v0.5.0

func (l Logger) LogEntry(ctx context.Context, e SinkEntry)

LogEntry logs the given entry with the context to the underlying sinks.

It extends the entry with the set fields and names.

func (Logger) Named

func (l Logger) Named(name string) Logger

Named appends the name to the set names on the logger.

Example
package main

import (
	"context"
	"net"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

func main() {
	ctx := context.Background()

	l := sloghuman.Make(os.Stdout)
	l = l.Named("http")
	l.Info(ctx, "received request", slog.F("remote address", net.IPv4(127, 0, 0, 1)))

	// 2019-12-07 21:20:56.974 [INFO]	(http)	<example_test.go:85>	received request	{"remote address": "127.0.0.1"}
}
Output:

func (Logger) Sync

func (l Logger) Sync()

Sync calls Sync on all the underlying sinks.

func (Logger) Warn

func (l Logger) Warn(ctx context.Context, msg string, fields ...Field)

Warn logs the msg and fields at LevelWarn.

func (Logger) With

func (l Logger) With(fields ...Field) Logger

With returns a Logger that prepends the given fields on every logged entry.

It will append to any fields already in the Logger.

type Map

type Map []Field

Map represents an ordered map of fields.

func M

func M(fs ...Field) Map

M is a convenience constructor for Map

func (Map) MarshalJSON

func (m Map) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

It is guaranteed to return a nil error. Any error marshalling a field will become the field's value.

type Sink

type Sink interface {
	LogEntry(ctx context.Context, e SinkEntry)
	Sync()
}

Sink is the destination of a Logger.

All sinks must be safe for concurrent use.

type SinkEntry

type SinkEntry struct {
	Time time.Time

	Level   Level
	Message string

	LoggerNames []string

	Func string
	File string
	Line int

	SpanContext trace.SpanContext

	Fields Map
}

SinkEntry represents the structure of a log entry. It is the argument to the sink when logging.

type Value

type Value interface {
	SlogValue() interface{}
}

Value represents a log value.

Implement SlogValue in your own types to override the value encoded when logging.

Example
package main

import (
	"context"
	"os"

	"cdr.dev/slog"
	"cdr.dev/slog/sloggers/sloghuman"
)

type vals struct {
	first  int
	second int
}

func (s *vals) SlogValue() interface{} {
	return slog.M(
		slog.F("total", s.first+s.second),
		slog.F("first", s.first),
		slog.F("second", s.second),
	)
}

func main() {
	l := sloghuman.Make(os.Stdout)
	l.Info(context.Background(), "hello", slog.F("val", &vals{
		first:  3,
		second: 6,
	}))

	// 2019-12-07 21:06:14.636 [INFO]	<example_value_test.go:26>	hello	{"val": {"total": 9, "first": 3, "second": 6}}
}
Output:

Directories

Path Synopsis
internal
assert
Package assert contains helpers for test assertions.
Package assert contains helpers for test assertions.
entryhuman
Package entryhuman contains the code to format slog.SinkEntry for humans.
Package entryhuman contains the code to format slog.SinkEntry for humans.
syncwriter
Package syncwriter implements a concurrency safe io.Writer wrapper.
Package syncwriter implements a concurrency safe io.Writer wrapper.
sloggers
sloghuman
Package sloghuman contains the slogger that writes logs in a human readable format.
Package sloghuman contains the slogger that writes logs in a human readable format.
slogjson
Package slogjson contains the slogger that writes logs in JSON.
Package slogjson contains the slogger that writes logs in JSON.
slogstackdriver
Package slogstackdriver contains the slogger for google cloud's stackdriver.
Package slogstackdriver contains the slogger for google cloud's stackdriver.
slogtest
Package slogtest contains the slogger for use with Go's testing package.
Package slogtest contains the slogger for use with Go's testing package.
slogtest/assert
Package assert is a helper package for test assertions.
Package assert is a helper package for test assertions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL