slog

package module
v0.0.0-...-4d8b9e4 Latest Latest
Warning

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

Go to latest
Published: Jan 6, 2017 License: MIT Imports: 11 Imported by: 1

README

slog

Structured, Leveled Logging with Context

GoDoc license Build Status (Linux) Build status (Windows) GoReportCard

Another logging package? Really?

Yes. slog is another logging package. It's probably worth listing some of the more mature logging packages out there that may suit your purpose:

Please note that this package is not under active development. It has some good ideas, but there are just too many other good logging packages that have wide support and active development.

Structured

Package slog does not provide the use of Printf-like methods for formatting messages. Instead it encourages the use of key-value pairs for logging properties associated with a log message. So, instead of of

 log.Printf("[error] cannot open file %s: %s", filename, err.Error())

which would look like:

 [error] cannot open file /etc/hosts: file not found

Logging key-value pairs looks like:

 slog.Error(ctx, "cannot open file",
     slog.WithValue("filename", filename),
     slog.WithError(err))

which results in a log message like:

 error msg="cannot open file" filename=/etc/hosts error="file not found"

This idea has gained traction as a best practice, and the result is both readable by humans, and can be parsed quite easily.

Leveled

Like many other logging packages, slog requires the calling program to assign a level to each message logged. The log levels available in the slog package are:

  • Debug for messages that are of interest to software developers when they are debugging the application. A debug message might involve quite low-level information, such as entering and leaving a function.
  • Info for messages that indicate an event of interest, but is not an error condition. An example might be logging an info message when a new user signs up for a service. This might be useful for counting, but it is not a cause for concern for the dev-ops team.
  • Warn for messages that indicate a condition that is not necessarily a cause for concern by the dev-ops team in its own right, but could be a cause for concern if it were to happen on a regular basis. An example of a warning message might be an attempt to login with an invalid username/password combination. Not a problem if it happens occasionally, but the dev-ops team might be interested if it were happening on a very regular basis from one particular IP address.
  • Error for messages that indicate a condition that may require immediate attention from the dev-ops team. Error messages indicate some sort of failure that the application program may not be able to recover from without human intervention.

The guidelines for the levels above are quite general. There is room for interpretation, and the level chosen for any particular message can depend on the application, and the agreed standards of the development and operations teams for that application.

It might be worth noting that there is a growing opinion that fewer levels might be better than many levels. Dave Cheney, for example, promotes an argument that warning messages should be eliminated.

With Context

Package slog makes heavy use of the golang.org/x/net/context package. If your application does not use this context package, then you will probably want to look at one of the other logging packages, as slog will not deliver much benefit to you. If you are unfamiliar with the golang.org/x/net/context package there is an excellent article on the Go Blog.

The golang.org/x/net/context package is useful when writing servers that handle requests. Common examples are HTTP servers, RPC servers and batch processors. As each request is processed, multiple goroutines may be started to assist with the processing of the request. By convention each function involved in the processing of the request receives as its first parameter a ctx variable of type context.Context. The context makes it easy to pass values associated with the request, and slog makes use of this by adding log properties to the request.

func Login(ctx context.Context, username, password string) (*User, error) {
    // create a new context with log properties
    ctx = slog.NewContext(ctx,
        slog.Property{"operation", "Login"},
        slog.Property{"username", username})

    // ... pass request onto database access functions ...
    user, err := db.FindUserByUsername(username)
    if err != nil {
        // will log `error msg="cannot find user" operation="Login" username="fnurk"`
        return nil, slog.Error(ctx, "cannot find user", slog.WithError(err))
    }

    // ... more processing ...

    return user, nil
 }

In the above example, the Login function attaches some properties to the context, so if at a later time an error condition is logged, the properties in the context are logged with the message.

When to log an error message

When using slog to log error messages, there are a few simple rules of thumb for logging error messages.

  • If a function with a context calls a function without a context, then log any error and return the message logged as the error.

    func FuncWithContext(ctx context.Context, int someArg) error {
        // calling a function that does not accept a context, could
        // be some external library
        if err := DoThatOneThing(someArg); err != nil {
            // log a message and return that message as the error
            return slog.Error(ctx, "cannot do that one thing",
                slog.WithError(err))
        }
    
        // ... do more processing ...
        return nil
    }
    
  • If a function with a context calls another function with a context, then there is no need log to log an error if the only processing to be performed is to pass the error back to the caller.

    func FuncWithContext(ctx context.Context, int someArg) error {
        // calling another function with context: that function
        // will log an error if it encounters it and return
        if err := DoOneThingWithContext(ctx, someArg); err != nil {
            // don't log a message: the DoOneThingWithContext function
            // has already logged and all we are doing is passing the
            // error back to our caller
            return nil, err
        }
    
        // ... do more processing ...
        return nil
    }
    
  • If a function with a context calls another function with a context and receives an error response, then if there is some non-trivial error handling then there may be scope for additional logging.

    func FuncWithContext(ctx context.Context, int someArg) error {
        // calling another function with context: that function
        // will log an error if it encounters it and return
        if err := DoOneThingWithContext(ctx, someArg); err != nil {
            slog.Info(ctx, "cleaning up")
            DoSomeCleanup(ctx, someArg)
            return nil, err
        }
    
        // ... do more processing ...
        return nil
    }
    

Messages are errors

As seen in the above examples, the slog.Error, slog.Warn, slog.Info and slog.Debug functions all return a non-nil *slog.Message. This non-nil pointer implements the error interface, and can be returned as an error value.

Messages can have a status code

In the common case of a HTTP server, it may be useful to pass back a suggested HTTP status code when logging an error:

if user, err := FindUser(username); err != nil {
	return slog.Error(ctx, "cannot find user", slog.WithError(err))
} else if user == nil {
	// log message and include a hint at a suitable HTTP status code
	return slog.Warn(ctx, "user not found",
		slog.WithStatusCode(http.StatusNotFound))
}

// ... continue processing user ...

The HTTP middleware can then make use of the status code later if necessary

// statusCodeFromError chooses a HTTP status code based on an error.
func statusCodeFromError(err error) int {
	// default to internal error
	statusCode := http.StatusInternalServerError

	type statusCoder interface {
		StatusCode() int
	}

	if errWithStatusCode, ok := err.(statusCoder); ok {
		if sc := errWithStatusCode.StatusCode(); sc > 0 {
			statusCode = sc
		}
	}

	return statusCode
}

Messages can have an error code

There are times when it may be useful to pass back a code to inform the requesting party that a specific error condition has occurred.

// optimistic locking exception has occurred
return slog.Info(ctx, "optimistic locking error",
	slog.WithCode("OptimisticLockingError"))

TODO: we have played around with an errors-like package with functions StatusCode(error) int and Code(error) string, but haven't got around to publishing it yet. It keeps changing with every project we use it on.

Documentation

Overview

Package slog provides structured, context-aware logging. It is intended for use by applications that make use of the golang.org/x/net/context package. (See https://blog.golang.org/context for more information).

The idea is that the application can build up information on the current request, transaction, or operation and store it in the current context. When an event occurs that needs to be logged, the built-up context is included in the log message.

file, err := os.Open(filename)
if err != nil {
    // message logged will include any logging context stored in ctx
    slog.Error(ctx, "cannot open file",
        slog.WithValue("filename", filename),
        slog.WithError(err))
}

// Output: (assuming userip has been set in the context)
// 2009-11-10T12:34:56.789 error msg="cannot open file" filename=/etc/hosts error="file does not exist" userip=123.231.111.222

Out of the box, this package logs to stdout in logfmt format. (https://brandur.org/logfmt). Other formats are planned (including pretty TTY output), and a handler mechanism exists to integrate with external logging providers.

See the examples for more details. A more comprehensive guide is available at https://github.com/spkg/slog

Example
package main

import (
	"errors"

	log "github.com/spkg/slog"
	"golang.org/x/net/context"
)

func main() {
	// everything logged needs a context
	ctx := context.Background()

	log.Info(ctx, "program started")
	if err := doFirstThing(ctx, 5, 4); err != nil {
		// ... error processing here ...
	}

	// YYYY-MM-DDTHH:MM:SS.FFFFFF info msg="program started"
	// YYYY-MM-DDTHH:MM:SS.FFFFFF error msg="cannot do third thing" error="error message goes here" a=5 b=4
}

// doFirstThing illustrates setting a new logging context.
// Any message logged with the new context will have values for
// a and b.
func doFirstThing(ctx context.Context, a, b int) error {
	// add some logging to the context
	ctx = log.NewContext(ctx,
		log.Property{Key: "a", Value: a},
		log.Property{Key: "b", Value: b})

	// ... perform some more processing and then

	return doSecondThing(ctx)
}

func doSecondThing(ctx context.Context) error {
	if err := doThirdThing(); err != nil {
		// log the message at the first point where the context is available
		return log.Error(ctx, "cannot do third thing", log.WithError(err))
	}
	return nil
}

func doThirdThing() error {
	// this function has no context so it does not log,
	// it just returns an error message
	return errors.New("error message goes here")
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// Default is the default logger.
	Default = New()
)
View Source
var (
	MinLevel = LevelInfo
)

MinLevel is the minimum level that will be logged. The calling program can change this value at any time.

Functions

func AddHandler

func AddHandler(h Handler)

AddHandler appends the handler to the list of handlers for the default logger.

Example
package main

import (
	"log"
	"net/http"

	"github.com/spkg/slog"
	"golang.org/x/net/context"
)

type ExternalHandler struct {
	// ... details for accessing external handler ...
}

func (h *ExternalHandler) Handle(msgs []*slog.Message) {
	// ... send to msgs to external logging handler ...
}

func main() {
	slog.AddHandler(&ExternalHandler{})
}

func main(ctx context.Context) {
	// Creates a HTTP server whose error log will write to the
	// default slog.Logger. Any panics that are recovered will
	// have the details logged via slog.
	httpServer := &http.Server{
		Addr:     ":8080",
		ErrorLog: log.New(slog.NewWriter(ctx), "http", 0),
	}

	slog.Info(ctx, "web server started", slog.WithValue("addr", httpServer.Addr))
	if err := httpServer.ListenAndServe(); err != nil {
		slog.Error(ctx, "web server failed", slog.WithError(err))
	}

	// 2009-11-10T12:34:56.789 info msg="web server started" addr=:8080
	// 2009-11-10T12:35:57:987 error msg="http: panic serving 123.1:2.3:36145 runtime error: invalid memory address or nil pointer dereference"
}

func main(ctx context.Context, n1, n2 int) error {
	if err := doSomethingWith(n1, n2); err != nil {
		return slog.Error(ctx, "cannot doSomething",
			slog.WithValue("n1", n1),
			slog.WithValue("n2", n2),
			slog.WithError(err))
	}

	// .. more processing and then ...

	return nil
}

func main(ctx context.Context, n1, n2 int) error {
	if err := doSomethingWith(n1, n2); err != nil {
		return slog.Error(ctx, "doSomethingWith failed",
			slog.WithValue("n1", n1),
			slog.WithValue("n2", n2))
	}

	// ... more processing and then ...

	return nil
}

func main(ctx context.Context) error {
	if err := doSomething(); err != nil {
		return slog.Error(ctx, "doSomething failed",
			slog.WithError(err))
	}

	// ... more processing and then ...

	return nil
}

func main(ctx context.Context, n1, n2 int) error {
	ctx = slog.NewContext(ctx,
		slog.Property{Key: "n1", Value: n1},
		slog.Property{Key: "n2", Value: n2})

	if err := doSomethingWith(n1, n2); err != nil {
		return slog.Error(ctx, "doSomethingWith failed",
			slog.WithError(err))
	}

	slog.Debug(ctx, "did something with")

	// ... more processing and then ...

	return nil
}

func doSomethingWith(n1 int, n2 int) error {
	return nil
}

func doSomething() error {
	return nil
}

func doAnotherThing() error {
	return nil
}

func doOneMoreThing() error {
	return nil
}
Output:

func NewContext

func NewContext(ctx context.Context, properties ...Property) context.Context

NewContext returns a new context that has one or more properties associated with it for logging purposes. The properties will be included in any log message logged with this context.

func NewWriter

func NewWriter(ctx context.Context) io.Writer

NewWriter creates a new writer that can be used to integrate with the standard log package. The main use case for this is to log messages generated from the standard library, in particular the net/http package. See the example for more information.

func SetMinLevel

func SetMinLevel(level Level)

SetMinLevel sets the minimum log level for the default logger. By default the minimum log level is LevelInfo.

func SetOutput

func SetOutput(w io.Writer)

SetOutput sets the output writer for the default logger.

Types

type Handler

type Handler interface {
	Handle(msgs []*Message)
}

Handler is an interface for message handlers. A message handler is added to a Logger to perform arbitrary process of log messages.

type Level

type Level int

Level indicates the level of a log message.

const (
	LevelDebug   Level = iota // Debugging only
	LevelInfo                 // Informational
	LevelWarning              // Warning
	LevelError                // Error condition
)

Values for Level.

func (Level) MarshalText

func (lvl Level) MarshalText() ([]byte, error)

MarshalText implements the encoding.TextMarshaler interface.

func (Level) String

func (lvl Level) String() string

String implements the String interface.

func (*Level) UnmarshalText

func (lvl *Level) UnmarshalText(text []byte) error

UnmarshalText implements the encoding.TextUnmarshaler interface.

type Logger

type Logger interface {
	Debug(ctx context.Context, text string, opts ...Option) *Message
	Info(ctx context.Context, text string, opts ...Option) *Message
	Warn(ctx context.Context, text string, opts ...Option) *Message
	Error(ctx context.Context, text string, opts ...Option) *Message

	NewWriter(ctx context.Context) io.Writer
	SetOutput(w io.Writer)
	SetMinLevel(level Level)
	AddHandler(h Handler)
}

Logger is an interface for logging.

func New

func New() Logger

New returns a new Logger with default settings. Writes to stdout, and does not print debug messages.

type Message

type Message struct {
	Timestamp  time.Time
	Level      Level
	Text       string
	Err        error
	Properties []Property
	Context    []Property
	// contains filtered or unexported fields
}

Message contains all of the log message information. Note that *Message implements the error interface.

func Debug

func Debug(ctx context.Context, text string, opts ...Option) *Message

Debug logs a debug level message to the default logger. Returns a non-nil *Message, which can be used as an error value.

func Error

func Error(ctx context.Context, text string, opts ...Option) *Message

Error logs an error level message to the default logger. Returns a non-nil *Message, which can be used as an error value.

func Info

func Info(ctx context.Context, text string, opts ...Option) *Message

Info logs an informational level message to the default logger. Returns a non-nil *Message, which can be used as an error value.

func Warn

func Warn(ctx context.Context, text string, opts ...Option) *Message

Warn logs a warning level message to the default logger. Returns a non-nil *Message, which can be used as an error value.

func (*Message) Code

func (m *Message) Code() string

Code returns the code associated with the message. Implements the Coder interface.

func (*Message) Error

func (m *Message) Error() string

Error implements the error interface

func (*Message) Logfmt

func (m *Message) Logfmt() string

Logfmt writes the contents of the message to the buffer in logfmt format. See https://brandur.org/logfmt for a description of logfmt. Returns number of bytes written to w.

func (*Message) SetCode

func (m *Message) SetCode(code string)

SetCode sets the Code property.

func (*Message) SetStatus

func (m *Message) SetStatus(status int)

SetStatus sets the status code associated with the message.

func (*Message) Status

func (m *Message) Status() int

Status returns any status code associated with the message. This is intended to be a HTTP status code, but the application can use any numbering scheme.

type Option

type Option func(*Message)

An Option is a function option that can be applied when logging a message. See the example for how they are used. Options is based on Dave Cheney's article "Functional options for friendly APIs" (http://goo.gl/l2KaW3) that can be applied to a Message.

func WithCode

func WithCode(code string) Option

WithCode associates an arbitrary code with the Message that is logged. This is useful for associating a pre-agreed error code with the message.

func WithError

func WithError(err error) Option

WithError sets the error associated with the log message.

func WithStatusCode

func WithStatusCode(status int) Option

WithStatusCode sets a status code associated with the log message. This is useful for associating a HTTP status code, but the status can be any number that makes sense for the application.

func WithValue

func WithValue(name string, value interface{}) Option

WithValue sets a property with a name and a value.

type Property

type Property struct {
	Key   string
	Value interface{}
}

Property is a key value pair associated with a Message.

Directories

Path Synopsis
Package logfmt provides some helper functions to write logfmt messages.
Package logfmt provides some helper functions to write logfmt messages.

Jump to

Keyboard shortcuts

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