logger

package
v1.65.3 Latest Latest
Warning

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

Go to latest
Published: Apr 28, 2025 License: MIT Imports: 6 Imported by: 0

README

This is a very small and simple interface wrapper over the zerolog logger,
including the reference implementation of the wrapper. It is production ready,
and used since 2021 privately, until open sourced in 2024. Open sourced version
took some improvements since then.

Goals of making such a wrapper around the zerolog logger:

    1) Ability to make an implementation which will allow logging to extra destinations,
        e.g. to queues or to various logging collection systems, without interfering with the
        user codebase.

            Clarification: In fact, you don't need a wrapper for that, moreover, this wrapper
            won't help with that (if we're talking about the zerolog underneath). That's because
            the buffer with serialized data is private to zerolog, and inaccessible (and imho
            that was a mistake of them). However, you can do so, by intercepting the output
            of zerolog logger. The framing is natural, because it call single Write() for each
            logline.

            Tip: If you make own implementation, you can also use Msg(string) instead of Send()
            internally in Send(), and so pass that string as argument to the zerolog hook.
            Well, that'd be weird though.

            Resume: currently, you can only control whether to do send or not, from the Send()
            hook; the send operation itself is detached in a writer, and you can't link the two,
            but to fork and fix the original zerolog.

    2) Technical ability to add filters, including those activated and managed at runtime, or set
        via a config files, without interfering into the source code.

    3) Ability to set logging ratelims for individual codes/tags or groups of codes/tags,
        without interfering into the main user codebase.
        
        That is, the fine-tuning of what to log and how, can be delegated to operations or
        maintenance team.

    4) Ability to switch off some points of logging, without removing them from the source code.
        Like a filter, but hardcoded.

    5) Ability to transparently add metrics reporting - by levels and by codes/tags.

    6) Ability to build a histogram in Grafana of what is logging, and so determine
        any parasitic logging, see what can be decreased or throttled or levels corrected.

    7) A histogram in Grafana can also be used as a natural live registry of what is logged,
        so an operator can quickly figure out what codes to look after in the logging system.
        That is, all codes/tags and titles would be accounted for in there, which is very convenient
        and important to have.

    8) In case of erroneous flooding of the logging system, the histogram will help quickly
        diagnose what is the root cause of such a flooding, and assist in quickest possible
        remedy.

    9) Ability to add alerts, both in logging and in metrics systems, for specific codes/tags,
        their groups or sets. It is very important functionality, it allows to track situations of
        repeated restarts of sub-systems, too often refreshes, or the lack of thereof, and a lot of
        another kinds of anomalies, by accounting for growth or fall of specific trends of logging
        rates in general, as well as by specific sub-systems, points, or groups of codes.

    10) Ability to make graphs in grafana of specific codes or groups of them, and visualize
        the key states of the service, such as start, restart, etc. For more complex services,
        it will allow to visualize its current states as state diagrams.

        You can also see when the service panics, without the need to look after it in logs.

    11) Ability to use Msgtag struct for production-level readiness sensor, which can be hard-coded
        for specific logging codes, and keep an account what subsystems are running already.

    12) Ability to use logging codes for a production heartbeat sensor systems, analogously to
        the readiness sensor systems.

    13) If you use this interface wrapper, you can always make your own implementation easily,
        and employ different logger underneath.

    14) Ability to use reported metrics for ML systems and other pattern finding algorithms,
        for determining the health and runtime state and modes of operation of services.

    15) Added the inactication functionality, which allows to efficiently deactivate some LPs,
        and via an ActicationHook activate them on demand. The hook is executed on setting the
        Msgtag(), because only then it makes sense.

        To make sure inactivated LPs incur minimum CPU overhead on useless attachment of data,
        please use this chain idiom:

            logger.InfoEvent().Title().Msgtag(msgtag).AllOtherData()...                   -- (1)
            logger.InfoEvent().Title().Inactive().Msgtag(msgtag).AllOtherData()...        -- (2)
        
        That's because the ActivationHook is called in Msgtag() specifically.

        Some rules of thumb, if the the ActivationHook is set, and is about to reactivate it:

            If the logger was inactivated, then any data set before the Msgtag() will be bypassed,
            and data set after it will be set.

            If the event was inactivated, then any data set before the Msgtag() will be be bypassed,
            and data set after it will be set.

            If the logger was active, then any data set on event before the Inactive() will be set,
            and not bypassed.

--

Some technical details: why wasn't standard hooks mechanism of zerolog wasn't enough?

    Well. We can use them. However, you need to have some logging attributes in them as scalar objects,
    and the point of zerolog is rather to do immediate serialization. So, to get those, you'd need to
    deserialize it back, which is absurd. With this wrapper, we are free to put and keep any attributes
    we want as scalars, and then use in our own sending hook.

    Currently, in the reference implementation, the msgtag array, the title, level, and subsystem are
    all available as scalars.

--

Common idioms:

    Init the logger:

            "github.com/rs/zerolog/log"
            "github.com/rusriver/nutz/logger"

            logger = logger.New(&log.Logger, func(s *logger.Settings) {
                s.PanicOnMisuse = false
                s.OnSendHook = func(e *logger.Event) (doSend bool) {
                    // ...
                    return true
                }
            })

    Or, init v2:
            
            "github.com/rs/zerolog"
            "github.com/rusriver/nutz/logger"

            zerologger := zerolog.New(os.Stderr).With().Timestamp().Logger()

            logger = logger.New(zerologger, func(s *logger.Settings) {
                s.PanicOnMisuse = false
                s.OnSendHook = func(e *logger.Event) (doSend bool) {
                    // ...
                    return true
                }
            })

    Make a sub-logger:

            logger = logger.SubLoggerInitChain().
                Str("executable", "my service").
                Str("version", "1.234.0").
                Caller().
                ILogger()

    Add callbacks to already existing logger:

            nutzLogger := logger.GetNutzLogger()
            nutzLogger.Settings.ActivationHook = func(e *logger.Event) (inactivate bool) {
                inactivate = false
                return
            }
            nutzLogger.Settings.OnSendHook = func(e *logger.Event) (doSend bool) {
                return true //
            }

    Send a logline:

            logger.InfoEvent().Title("informational test event").Msgtag(nil, "123", "456").Send()
            logger.InfoEvent().Title("informational test event").Inactive().Msgtag(nil, "123", "456").Send()

    Intercept the serialized events with a io.Writer tee:


            type LogsTee struct{}

            var _ io.Writer = (*LogsTee)(nil)

            func (t *LogsTee) Write(p []byte) (n int, err error) {
                os.Stderr.Write(p)

                // clone the p - as we must not modify it, see the https://pkg.go.dev/io#Writer
                p2 := append(p[:0:0], p...)

                s2 := strings.TrimSpace(string(p2))
                fmt.Println("<--", s2, "-->")

                return
            }

            tee := &LogsTee{}

            zerologger := zerolog.New(tee).With().Timestamp().Logger()

            logger = logger.New(zerologger, func(s *logger.Settings) {})


    You can also consider using the https://pkg.go.dev/io#MultiWriter, but that's a little bit different.




--

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StacktraceReadableOneliner added in v1.27.0

func StacktraceReadableOneliner(stacktrace []byte) string

Make a one-line representation of Go stracktrace, optimized to be human-readable in one-line form. It's useful when reading stacktraces logged into logging systems, where it's represented as a one-liner, with line breaks represented as \n, et al. Typical usage idiom:

e.Str("stacktrace", logger.StacktraceReadableOneliner(debug.Stack()))

Types

type Event

type Event struct {
	ParentLogger *Logger
	IsInactive   bool   // per event
	TheLevel     string // per event
	TheMsgtag    Msgtag // per event
	TheTitle     string // per event
	// contains filtered or unexported fields
}

func (*Event) Array

func (e *Event) Array(k string, v *zerolog.Array) IEvent

func (*Event) Bytes

func (e *Event) Bytes(k string, bb []byte) IEvent

func (*Event) Caller

func (e *Event) Caller(skip ...int) IEvent

func (*Event) Dict

func (e *Event) Dict(k string, v *zerolog.Event) IEvent

func (*Event) Float64 added in v1.34.0

func (e *Event) Float64(k string, v float64) IEvent

func (*Event) ILogger

func (e *Event) ILogger() ILogger

func (*Event) IfActive added in v1.20.0

func (e *Event) IfActive(f func(ev IEvent)) IEvent

func (*Event) Inactive added in v1.17.0

func (e *Event) Inactive() IEvent

func (*Event) Int

func (e *Event) Int(k string, v int) IEvent

func (*Event) Msgtag

func (e *Event) Msgtag(msgtag *Msgtag, ss ...string) IEvent

func (*Event) RawJSON

func (e *Event) RawJSON(k string, rj []byte) IEvent

func (*Event) RequestId

func (e *Event) RequestId(s string) IEvent

func (*Event) Send

func (e *Event) Send()

func (*Event) SendMsg

func (e *Event) SendMsg(s string)

func (*Event) SendMsgf

func (e *Event) SendMsgf(s string, vv ...any)

func (*Event) SendTitle

func (e *Event) SendTitle(s string)

func (*Event) Str

func (e *Event) Str(k, v string) IEvent

func (*Event) Strf added in v1.23.0

func (e *Event) Strf(k, s string, vv ...any) IEvent

func (*Event) Strs

func (e *Event) Strs(k string, vv []string) IEvent

func (*Event) SubSystem

func (e *Event) SubSystem(s string) IEvent

func (*Event) Time

func (e *Event) Time(k string, t time.Time) IEvent

func (*Event) Title

func (e *Event) Title(s string) IEvent

type IEvent

type IEvent interface {
	// Inactivation applies to real events only, all data set ops will be bypassed.
	// However, if applied to sub-logger init chain, nothing is bypassed.
	Inactive() IEvent

	// This is useful to do some work only if the event is activated, e.g. do some data prep
	// or JSON marshalling, and waste no time otherwise.
	//
	//	     logger.InfoEvent().Inactive().Msgtag().IfActive(func(ev IEvent) {
	//				// do some heavy work, then
	//				ev.RawJSON(b)
	//			}).Title("my event").Send()
	IfActive(func(IEvent)) IEvent

	Caller(skip ...int) IEvent
	Str(string, string) IEvent
	Strf(string, string, ...any) IEvent
	Strs(string, []string) IEvent
	Time(string, time.Time) IEvent
	Int(string, int) IEvent
	Float64(string, float64) IEvent
	Array(k string, v *zerolog.Array) (e IEvent)
	Dict(k string, v *zerolog.Event) (e IEvent)

	// Must not be used on sub-logger init chains.
	SendMsgf(string, ...any)

	// Must not be used on sub-logger init chains.
	SendMsg(string)

	// It's like Msg(), but sets the value to "title" field, and also
	// can be used to report to metrics. Must be low-cardinality string.
	// DO NOT put in it any variable strings, e.g. Sprintf()-formatted or
	// containing requestId or any other Id or counters!
	// Can't be applied for sub-logger chains, only for events.
	// In typical idiom, must be used BEFORE the Msgtag() - it's specififcally
	// designed to not bypass in inactive events.
	Title(string) IEvent

	// The same as Title().Send()
	// Must not be used on sub-logger init chains.
	SendTitle(string)

	// Some first strings are reported to metrics, be careful to NOT put in them
	// high-cardinality IDs. Doesn't apply to sub-logger init chains, must be set
	// per each event individually.
	// If the ActicationHook is set, it is called, and it can reactivate the event.
	Msgtag(msgtag *Msgtag, ss ...string) IEvent

	// Also must be reported to metrics.
	SubSystem(string) IEvent
	RequestId(string) IEvent
	RawJSON(string, []byte) IEvent
	Bytes(string, []byte) IEvent

	// Must not be used on sub-logger init chains.
	Send()

	// Only works if you created a chain with With()
	ILogger() ILogger
}

type ILogger

type ILogger interface {
	TraceEvent() IEvent
	DebugEvent() IEvent
	ErrorEvent() IEvent
	ErrEvent(error) IEvent
	WarnEvent() IEvent
	InfoEvent() IEvent

	// Can be used if you intend to call ILogger() on it later.
	// By default, all new sub-loggers are active, even if parent was inactivated.
	SubLoggerInitChain() IEvent

	GetZeroLoggerPtr() *zerolog.Logger

	// This is useful to access Settings later after init.
	GetNutzLogger() *Logger

	SetLevel(zerolog.Level) ILogger

	// Attach any user object to the event object, to be used later
	// inside the callback hooks. Intended to pass user-level runtime data to the
	// hook(s). Doesn't add anything to the logline serializations.
	// Normally, the UO must be a pointer, and at the receiving side in the hook
	// you do something along the lines:
	//
	//	myObject, _ := e.ParentLogger.UserObject.(*MyObject)
	//	if myObject !=  nil {
	//		...
	//	}
	AddUO(uo any) ILogger
}

type Logger

type Logger struct {
	Settings     *Settings
	IsInactive   bool   // per whole logger instance
	TheSubsystem string // per logger
	UserObject   any
	// contains filtered or unexported fields
}

func New

func New(zl *zerolog.Logger, optFuncs ...func(s *Settings)) (logger *Logger)

An initial zerologger must be provided by yours explicitly. Typical idiom:

import(
	"github.com/rs/zerolog/log"
)
	...
	my.Logger = logger.New(&log.Logger, func(s *logger.Settings) {
		s.PanicOnMisuse = false
	})

func (*Logger) AddUO added in v1.18.0

func (l *Logger) AddUO(uo any) ILogger

func (*Logger) DebugEvent added in v1.17.0

func (l *Logger) DebugEvent() (e IEvent)

func (*Logger) ErrEvent added in v1.17.0

func (l *Logger) ErrEvent(err error) (e IEvent)

func (*Logger) ErrorEvent added in v1.17.0

func (l *Logger) ErrorEvent() (e IEvent)

func (*Logger) GetNutzLogger added in v1.16.0

func (l *Logger) GetNutzLogger() *Logger

func (*Logger) GetZeroLoggerPtr

func (l *Logger) GetZeroLoggerPtr() *zerolog.Logger

func (*Logger) InfoEvent added in v1.17.0

func (l *Logger) InfoEvent() (e IEvent)

func (*Logger) SetLevel

func (l *Logger) SetLevel(level zerolog.Level) ILogger

func (*Logger) SubLoggerInitChain

func (l *Logger) SubLoggerInitChain() IEvent

func (*Logger) TraceEvent added in v1.17.0

func (l *Logger) TraceEvent() (e IEvent)

func (*Logger) WarnEvent added in v1.17.0

func (l *Logger) WarnEvent() (e IEvent)

type Msgtag

type Msgtag []string

Few first strings are reported to metrics, be careful to NOT put in them high-cardinality IDs.

func NormalizeTheMsgtag

func NormalizeTheMsgtag(msgtag *Msgtag, n int) *Msgtag

func (*Msgtag) String

func (msgtag *Msgtag) String() string

func (*Msgtag) With

func (msgtag *Msgtag) With(ss ...string) *Msgtag

type Settings

type Settings struct {
	PanicOnMisuse  bool
	MsgtagKey      string
	OnSendHook     func(e *Event) (doSend bool)
	ActivationHook func(e *Event) (inactivate bool) // called on Msgtag()
}

Jump to

Keyboard shortcuts

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