logger

package module
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: Dec 7, 2021 License: MIT Imports: 12 Imported by: 4

README

logger-go

Lob's logger for Go.

Usage

Include the logger-go package in your import.

import (
    "github.com/lob/logger-go"
)

Logs will always include:

  • logging level
  • hostname
  • release
    • Defined by the environment variable RELEASE
    • This is set by Convox in staging/production environments
  • timestamp
  • nanoseconds

Logs be written to stdout by default. The package ships with a global logger so you can easily emit logs without having to instantiate a logger. This default logger will always write to stdout.

logger.Info("Hello, world!", logger.Data{"fun": "things"})
// Outputs: {"level":"info","host":"Kyle.local","release":"test12345","data":{"fun":"things"},"nanoseconds":1532024420744842400,"timestamp":"2018-07-19T11:20:20-07:00","message":"Hello, world!"}

Alternatively, you can instantiate your own logger. This is useful if you want to attach additional data or top-level information to your logger, which will force all logs emitted by that logger to include that info.

l1 := logger.New("myservicename").ID("test")
l2 := l1.Data(logger.Data{"test": "data"})

l1.Info("hi")
// Outputs {"level":"info","host":"HOSTNAME","release":"RELEASE","id":"test","nanoseconds":1531945897647586415,"timestamp":"2018-07-18T13:31:37-07:00","message":"hi"}
l2.Info("hi")
// Outputs {"level":"info","host":"HOSTNAME","release":"RELEASE","id":"test","data":{"test":"data"},"nanoseconds":1531945897647593709,"timestamp":"2018-07-18T13:31:37-07:00","message":"hi"}

// If Data or Root are empty, they will not show up in the logs.
l1 = l1.Data(logger.Data{})
l1.Info("hi")
// Outputs {"level":"info","host":"HOSTNAME","release":"RELEASE","id":"test","nanoseconds":1531945897647586415,"timestamp":"2018-07-18T13:31:37-07:00","message":"hi"}

// To log errors, use Err. If it's a normal error, a runtime stack trace is logged. This provides limited context, so it's recommended to use pkg/errors instead (see below).
err := fmt.Errorf("foo")
l1.Err(err).Error("unknown error")
// {"level":"error","host":"HOSTNAME","release":"RELEASE","id":"test","error":{"message":"foo","stack":"goroutine 1 [running]:\ngithub.com/lob/logger-go.Logger.log(0x111b0c0, 0xc420010440, 0x0, 0x0, 0x0, 0xc4200b8200, 0x19, 0x1f4, 0xc420010450, 0x1, ...)\n\t/go/src/github.com/lob/logger-go/logger.go:153 +0x5d2\ngithub.com/lob/logger-go.Logger.Error(0x111b0c0, 0xc420010440, 0x0, 0x0, 0x0, 0xc4200b8200, 0x19, 0x1f4, 0xc420010450, 0x1, ...)\n\t/go/src/github.com/lob/logger-go/logger.go:101 +0xce\nmain.main()\n\t/go/src/github.com/lob/logger-go/main.go:27 +0x5db\n"},"nanoseconds":1531945897647586415,"timestamp":"2018-07-18T13:31:37-07:00","message":"unknown error"}

// If the error is wrapped with pkg/errors, a better stack trace is logged. See https://godoc.org/github.com/pkg/errors#hdr-Retrieving_the_stack_trace_of_an_error_or_wrapper for more info.
err = errors.New("bar")
l1.Err(err).Error("unknown error")
// {"level":"error","host":"HOSTNAME","release":"RELEASE","id":"test","error":{"message":"bar","stack":"\nmain.main\n\t/go/src/github.com/lob/logger-go/main.go:26\nruntime.main\n\t/.goenv/versions/1.10.3/src/runtime/proc.go:198\nruntime.goexit\n\t/.goenv/versions/1.10.3/src/runtime/asm_amd64.s:2361"},"nanoseconds":1531945897647586415,"timestamp":"2018-07-18T13:31:37-07:00","message":"unknown error"}

If you want the logger to use a specific writer to pipe logs to anywhere other than stdout, please use the NewWithWriter method. Make sure the argument you are passing in implements the writer interface.

type CustomWriter struct{}
func (cw *CustomWriter) Write(b []byte) (int, error) {
	// your custom write logic here
}

loggerWithWriter := logger.NewWithWriter(CustomWriter{})

The logger supports five levels of logging.

Info
logger.Info("Hello, world!")
Error
logger.Error("Hello, world!")
Warn
logger.Warn("Hello, world!")
Debug
logger.Debug("Hello, world!")
Fatal
logger.Fatal("Hello, world!")

We currently do not support trace-level logging, since zerolog, the underlying logging library, does not support trace (or custom level logging).

Echo Middleware

This package also comes with middleware to be used with the Echo web framework. To use it, you can just register it with e.Use():

e := echo.New()

e.Use(logger.Middleware("myservicename"))

There are also some scenarios where you don't wany an error to be logged and registered with Echo. An example is for broken pipe errors. When this happens, the client closed the connection, so even though it manifests as a network error, it's not actionable for us, so we would rather ignore it. To do that, you can use logger.MiddlewareWithConfig() and logger.MiddlewareConfig.

e := echo.New()

e.Use(logger.MiddlewareWithConfig("myservicename", logger.MiddlewareConfig{
	IsIgnorableError: func(err error) bool {
		e := errors.Cause(err)

		if netErr, ok := e.(*net.OpError); ok {
			if osErr, ok := netErr.Err.(*os.SyscallError); ok {
				return osErr.Err.Error() == syscall.EPIPE.Error() || osErr.Err.Error() == syscall.ECONNRESET.Error()
			}
		}

		return false
	},
}))

With this middleware, not only will it create a handled request log line for every request, but it will also attach a request-specific logger to the Echo context. To use it, you can pull it out with logger.FromEchoContext():

e.GET("/", func(ctx echo.Context) error {
        log := logger.FromEchoContext(ctx, "myservicename")

        log.Info("in handler")

        // ...

        return nil
})

You should always try to use this logger instead of creating your own so that it contains contextual information about the request.

Errors and Stack Traces

In Go, the idiomatic error type doesn't contain any stack information by default. Since there's no mechanism to extract a stack, it's common to use runtime.Stack to generate one. The problem with that is since the stack is usually created at the point of error handling and not the point of error creation, the stack most likely won't have the origination point of the error.

Because of this, the community has created a way to maintain stack information as the error gets bubbled up while still adhering to the idiomatic error interface. This is with the github.com/pkg/errors package. It allows developers to add context and stack frames to errors that are generated throughout a codebase. By leveraging this, you can produce a much better stack trace.

To demonstate the difference between these two mechanisms, here is an example:

func main() {
	log := logger.New("myservicename")

	nativeErr := nativeFunction1()
	pkgErr := pkgFunction1()

	log.Err(nativeErr).Error("native error")
	log.Err(pkgErr).Error("pkg error")
}

func nativeFunction1() error {
	return nativeFunction2() // line 21
}

func nativeFunction2() error {
	// returns a native error
	return fmt.Errorf("foo") // line 26
}

func pkgFunction1() error {
	return pkgFunction2() // line 30
}

func pkgFunction2() error {
	// returns a pkg/errors error
	return errors.New("foo") // line 35
}

This sample code produces these stack traces:

goroutine 1 [running]:
github.com/lob/logger-go.Logger.log(0x111a8c0, 0xc420010440, 0x0, 0x0, 0x0, 0xc4200b8200, 0x19, 0x1f4, 0xc420010450, 0x1, ...)
        /Users/robinjoseph/go/src/github.com/lob/logger-go/logger.go:154 +0x5ad
github.com/lob/logger-go.Logger.Error(0x111a8c0, 0xc420010440, 0x0, 0x0, 0x0, 0xc4200b8200, 0x19, 0x1f4, 0xc420010450, 0x1, ...)
        /Users/robinjoseph/go/src/github.com/lob/logger-go/logger.go:101 +0xce
main.main()
        /Users/robinjoseph/go/src/github.com/lob/logger-go/ex/main.go:16 +0x1bd


main.pkgFunction2
        /Users/robinjoseph/go/src/github.com/lob/logger-go/ex/main.go:35
main.pkgFunction1
        /Users/robinjoseph/go/src/github.com/lob/logger-go/ex/main.go:30
main.main
        /Users/robinjoseph/go/src/github.com/lob/logger-go/ex/main.go:14
runtime.main
        /Users/robinjoseph/.goenv/versions/1.10.3/src/runtime/proc.go:198
runtime.goexit
        /Users/robinjoseph/.goenv/versions/1.10.3/src/runtime/asm_amd64.s:2361

As you can see from the runtime stack (the former), it contains neither function names nor line numbers to indicate the original cause of the error. But with the pkg/errors stack trace (the latter), it's very clear and contains all the necessary information needed for debugging.

This logging package will attempt to extract the pkg/errors stack trace if that information exists, but otherwise, it will provide the runtime stack. It's because of this that we strongly recommend adding pkg/errors to wrap all errors in your codebase. This will aid in the general debuggability of your applications.

For more information on how to wrap errors, like errors.WithStack(), check out the pkg/errors docs.

Development

# Install necessary dependencies for development
make setup

# Run tests and generate test coverage report
make test

# Run linter
make lint

# Remove temporary files generated by the build command and the build directory
make clean

Cutting a New Release

After new commits have been added, a new git tag should also be created so that tools like go mod and dep can utilize it for more semantic versioning.

If there is a breaking change introduced in this new version, then it should be a major version bump. If there are no breaking changes and only new features, then it should be a minor version bump. And if there are no breaking changes, no new features, and only bug fixes, then it should be a patch version.

After determining what the next version should be, make sure you're on an up-to-date master branch and run make release. The v in the version tag is important for tools to recognize it.

$ git checkout master
$ git fetch
$ git rebase
$ make release tag=v1.0.0 # replace 1.0.0 with the next version

Help

The following command generates a list of all make commands.

make help

FAQ

Tweaking code coverage

See this blog post about coverage in Go.

Tweaking the linter

See the gometalinter documentation.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Debug

func Debug(message string, fields ...Data)

Debug writes a debug-level log with a message and any additional data provided.

func Error

func Error(message string, fields ...Data)

Error writes an error-level log with a message and any additional data provided.

func Fatal

func Fatal(message string, fields ...Data)

Fatal writes a fatal-level log with a message and any additional data provided. This will also call `os.Exit(1)`.`

func Info

func Info(message string, fields ...Data)

Info writes a info-level log with a message and any additional data provided.

func Middleware

func Middleware(serviceName string) func(next echo.HandlerFunc) echo.HandlerFunc

Middleware attaches a Logger instance with a request ID onto the context. It also logs every request along with metadata about the request. To customize the middleware, use MiddlewareWithConfig.

func MiddlewareWithConfig

func MiddlewareWithConfig(serviceName string, opts MiddlewareConfig) func(next echo.HandlerFunc) echo.HandlerFunc

MiddlewareWithConfig attaches a Logger instance with a request ID onto the context. It also logs every request along with metadata about the request. Pass in a MiddlewareConfig to customize the behavior of the middleware.

func Warn

func Warn(message string, fields ...Data)

Warn writes a warn-level log with a message and any additional data provided.

Types

type Data

type Data map[string]interface{}

Data is a type alias so that it's much more concise to add additional data to log lines.

type Logger

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

Logger holds the zerolog logger and metadata.

func FromContext

func FromContext(ctx context.Context) Logger

FromContext returns the Logger that is attached to ctx. If there is no Logger, a new Logger instance is returned.

func FromEchoContext

func FromEchoContext(c echo.Context, serviceName string) Logger

FromEchoContext returns a Logger from the given echo.Context. If there is no attached logger, then it will return a new Logger instance with the service's name populated in the correct fields.

func New

func New(serviceName string, options ...Option) Logger

New prepares and creates a new Logger instance.

func NewWithWriter added in v1.1.0

func NewWithWriter(serviceName string, w io.Writer, options ...Option) Logger

NewWithWriter prepares and creates a new Logger instance with a specified writer.

func (Logger) Data

func (log Logger) Data(data Data) Logger

Data returns a new logger with the new data appended to the old list of data.

func (Logger) Debug

func (log Logger) Debug(message string, fields ...Data)

Debug outputs a debug-level log with a message and any additional data provided.

func (Logger) Err

func (log Logger) Err(err error) Logger

Err returns a new Logger with the error set to err.

func (Logger) Error

func (log Logger) Error(message string, fields ...Data)

Error outputs an error-level log with a message and any additional data provided.

func (Logger) Fatal

func (log Logger) Fatal(message string, fields ...Data)

Fatal outputs a fatal-level log with a message and any additional data provided. This will also call os.Exit(1)

func (Logger) ID

func (log Logger) ID(id string) Logger

ID returns a new Logger with the ID set to id.

func (Logger) Info

func (log Logger) Info(message string, fields ...Data)

Info outputs an info-level log with a message and any additional data provided.

func (Logger) Root

func (log Logger) Root(root Data) Logger

Root returns a new logger with the root info appended to the old list of root info. This root info will be displayed at the top level of the log.

func (Logger) Warn

func (log Logger) Warn(message string, fields ...Data)

Warn outputs a warn-level log with a message and any additional data provided.

func (Logger) WithContext

func (log Logger) WithContext(ctx context.Context) context.Context

WithContext returns a copy of ctx with log attached to it.

type MiddlewareConfig

type MiddlewareConfig struct {
	IsIgnorableError func(error) bool
}

MiddlewareConfig can be used to configure the Echo Middleware.

type Option added in v1.3.0

type Option func(*zerolog.Context)

func WithField added in v1.3.0

func WithField(key, value string) Option

Jump to

Keyboard shortcuts

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