Skip to content

Celerway/chainsaw

Repository files navigation

Chainsaw, circular, structured logging

This is a highly opinionated, structured logging library for Go.

This logging library will keep the last N log messages around. This allows you to dump the logs at trace or debug level if you encounter an error, contextualizing the error.

As an example, say you're doing a database transaction. During this you'll pre-process some data, prepare the transaction and then commit it. If the commit fails, you'll want to know what data was being processed, what the transaction looked like and what the error was. This library will keep the last N log messages around, so they can be dumped when you encounter an error.

Example

    func performTx() {
		logger := chainsaw.NewLogger("database")
		logger.SetBackTraceLevel(chainsaw.ErrorLevel) // will trigger a backtrace on error
		log.Info("Preparing data")
		data, err := prepareData()
		// ...
		log.Info("Committing transaction")
		// ...
		tx, err := commitTx()
		if err != nil {
			log.Error("Failed to commit transaction", err) // will dump the logs
        }
		// ...
}

It also allows you to start a stream of log messages, making it suitable for applications which have sub-applications where you might want to look at different streams of logs. Say in an application which has a bunch of goroutines which have more or less independent logs which you want to present live.

There are two types of streams you can create. One in the form of a channel, which will deliver struct with log messages. The other is a stream of bytes, in the form of an io.Writer.

Note that care should be taken to maintain these streams. If you don't read from the channel, or let the io.Writer write, it will lock up the package, likely taking your application down. Chainsaw tries to detect a dead stream channel and will log a warning and remove the channel from subsequent log messages.

Fields

Chainsaw supports fields. These can either be set on a logger using the SetFieldsmethods or passed when using the fields enabled logging functions and methods (ending in 'w').

A field is a Pair consisting of a key, string and value, interface{}.

Usage

With or without instances

You can choose to instantiate a logger. Doing this will allow you to set a name as well as to add some fields to the logger.

without a logger instance.

    package main
    import "github.com/celerway/chainsaw"

    func main() {
		chainsaw.Infof("Application %f starting up", version)
		err := checkGloop()
		if err != nil {
			chainsaw.Fatal("Out of gloop")
		}
		// with fields:
		chainsaw.Infow("Can't open file", chainsaw.P{"file", file}, chainsaw.P{"err", err})
	}

with a logger instance.

    package main
    import "github.com/celerway/chainsaw"

    func main() {
		logger := chainsaw.MakeLogger("main")
		logger.SetFields(chainsaw.P{"hostname", hostname})
		logger.Error("Error in file:", err)
	}
}

A complete example

package main

import (
	"context"
	"fmt"
	log "github.com/celerway/chainsaw"
	"os"
	"time"
)

func main() {
	log.Info("Application starting up")
	log.Trace("This is a trace message. Doesn't get printed directly")
	logMessages := log.GetMessages(log.TraceLevel)
	for i, mess := range logMessages {
		fmt.Println("Fetched message: ", i, ":", mess.Content)
	}
	log.RemoveWriter(os.Stdout) // stop writing to stdout
	log.Flush()
	log.Info("Doesn't show up on screen.")
	ctx, cancel := context.WithCancel(context.Background())
	stream := log.GetStream(ctx)
	go func() {
		for mess := range stream {
			fmt.Println("From stream: ", mess.Content)
		}
		fmt.Println("Log stream is closed")
	}()
	log.Info("Should reach the stream above")
	cancel()
	time.Sleep(time.Second)
}

Caveats: things to be aware of

The logging is async so messages might take a bit of time before showing up. Control messages are synchronously, however, so a Flush will not return unless all pending messages are processed.

log.go is generated by gen/main.go

chainsaw is not fast - it is about as slow as logrus. Channels in Go aren't really that fast and if you're logging thousands of messages per second you might wanna at a high performance logging framework like zerolog.

On my laptop a regular log invocation takes about 600ns and control messages take 500ns.

Todo:

  • Increase compatibility with other logging libraries.
  • Add support for formatting
  • Documentation, at least when we're somewhat certain that the basic design is sane.
  • Adapt a logging interface. Shame there isn't one in Stdlib.