unwindlogger

package module
v0.0.0-...-9823034 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2023 License: GPL-3.0 Imports: 9 Imported by: 0

README

unwindlogger

Reference implementation of a logger, that can unwind and log previously skipped messages, after encountering an error

API heavily inspired by https://github.com/sirupsen/logrus

Motivation

In prod, we don't need 99% of the logs.

So, we log at WARN or ERROR level. To keep costs low, drive our alerts/monitors etc.

However, when we need logs, we want all the logs. Eg: what business processes fired, what happened in the call before this error, etc. Typically, these are logged at INFO level, so we can't see what happened.

So, wouldn't it be nice if, when an error was encountered, we could see the INFO logs for the rest of the request?

Including from before we knew an error would occur?

That's what this logger intends to do

How it works

Whenever a message with a context is logged, if it is below the immediate log level, it is deferred.

If an error does not occur, the ignorable logs are not logged, and dropped when the tracking stops

logger := unwindlogger.
    NewLogger().
    WithImmediateLevel(unwindlogger.WARN).
    WithDeferredLevel(unwindlogger.INFO)

ctx := context.Background()
doSomething := func(ctx context.Context) {
    logger.WithContext(ctx).WithField("hello", "world").Info("info log!")
}

ctx = logger.StartTracking(ctx)
doSomething(ctx)
logger.WithContext(ctx).Warn("warn log!")
logger.EndTracking(ctx)
// Will only output: {"level":"WARN","msg":"warn log!","time":"..."}
// As the tracking was ended without an error

If an error does occur, the deferred logs are flushed, providing more context

logger := unwindlogger.
    NewLogger().
    WithOut(os.Stdout).
    WithImmediateLevel(unwindlogger.WARN).
    WithDeferredLevel(unwindlogger.INFO)

ctx := context.Background()
doSomething := func(ctx context.Context) {
    logger.WithContext(ctx).WithField("hello", "world").Info("info log!")
}

ctx = logger.StartTracking(ctx)
doSomething(ctx)
logger.WithContext(ctx).Warn("warn log!")
logger.EndTrackingWithError(ctx, fmt.Errorf("oh no"))
// Will output both: {"level":"INFO","msg":"info log!","time":"..."} and {"level":"WARN","msg":"warn log!","time":"..."}
// As the tracking was ended with an error

Also supports a mode called fullDefer, which defers all logs, and logs those at immediate level, when tracking stops without an error, or logs at defer level, when tracking stops with an error.

This fullDefer mode ensures that logs are always emitted in time order, as deferring some logs and not others can create out of order logs.

Performance

It ain't great, but it's a POC.

$>  go test -bench=BenchmarkLogger -test.benchtime 10s -test.benchmem
goos: linux
goarch: amd64
pkg: github.com/Ben435/unwindlogger
cpu: AMD Ryzen 7 1700 Eight-Core Processor          
BenchmarkLogger_ImmediateLogging-16                     17658039               778.2 ns/op          1236 B/op         21 allocs/op
BenchmarkLogger_DeferredLogging-16                      15719172               963.0 ns/op          1235 B/op         21 allocs/op
BenchmarkLogger_MixedLoggingWithError-16                 8281585                1690 ns/op          2275 B/op         41 allocs/op
BenchmarkLogger_MixedLoggingWithoutError-16             13839494                1031 ns/op          1363 B/op         24 allocs/op
BenchmarkLogger_MixedLoggingWithError10Logs-16            577330               17757 ns/op         27060 B/op        453 allocs/op

Next steps

  • Ergonomics
    • Allow for logging without a context
    • Duplicate the entry similar to how logrus does, to allow for partial chaining
  • Improve performance
    • Steal liberally from better loggers
    • Wouldn't worry too much about improving *WithError performance
    • Minimized overhead for deferred logging when logs are discarded
  • Check its thread safe
    • Seems about right, but erm, haven't really checked
  • Test how bad out-of-order logs are to log aggregators
    • Eg: Datadog, Splunk, etc.
    • If they can handle it, fullDefer is basically only for file outputs and local runtime

Alternate ideas:

  • Just wrap another logger
    • This would just provide the "deferring" logic
  • Encase this in a plugin for other loggers
    • Not sure if the plugins are powerful enough to do this tho
    • Depends on the logger lib

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Entry

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

func NewEntry

func NewEntry(logger *Logger, ctx context.Context) *Entry

func (*Entry) Bytes

func (e *Entry) Bytes() ([]byte, error)

func (*Entry) Debug

func (e *Entry) Debug(msg string)

func (*Entry) Error

func (e *Entry) Error(msg string)

func (*Entry) Info

func (e *Entry) Info(msg string)

func (*Entry) Log

func (e *Entry) Log(level Level, msg string)

func (*Entry) String

func (e *Entry) String() (string, error)

func (*Entry) Warn

func (e *Entry) Warn(msg string)

func (*Entry) WithError

func (e *Entry) WithError(err error) *Entry

func (*Entry) WithField

func (e *Entry) WithField(k string, v interface{}) *Entry

type Level

type Level int
const (
	DEBUG Level = 20
	INFO  Level = 40
	WARN  Level = 60
	ERROR Level = 80
)

func (Level) Format

func (l Level) Format() string

type Logger

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

func NewLogger

func NewLogger() *Logger

func (*Logger) EndTracking

func (l *Logger) EndTracking(ctx context.Context)

func (*Logger) EndTrackingWithError

func (l *Logger) EndTrackingWithError(ctx context.Context, err error)

func (*Logger) StartTracking

func (l *Logger) StartTracking(ctx context.Context) context.Context

func (*Logger) WithContext

func (l *Logger) WithContext(ctx context.Context) *Entry

func (*Logger) WithDeferredLevel

func (l *Logger) WithDeferredLevel(level Level) *Logger

func (*Logger) WithFullDefer

func (l *Logger) WithFullDefer(fullDefer bool) *Logger

func (*Logger) WithImmediateLevel

func (l *Logger) WithImmediateLevel(level Level) *Logger

func (*Logger) WithOut

func (l *Logger) WithOut(out io.Writer) *Logger

Jump to

Keyboard shortcuts

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