Categorygithub.com/VisborN/xlog
modulepackage
0.5.2
Repository: https://github.com/visborn/xlog.git
Documentation: pkg.go.dev

# README

General

xlog is a library for easily logging into multiple endpoints, such as files, stdout, stderr, syslog, etc at the same time. Configure logger(s) once, and then write to several log endpoints according to defined rules with one function call. Flexible configuration of loggers allows controlling which recorders will be used in different cases.

Overview

LogRecorder objects represent log endpoints and provide write function and other methods to correctly interact with them. Recorders connect to one or more Logger objects which control them and determinate which log messages they should write and when. Basically all what recorder do it is listening signals from connected loggers and write the specific log messages when WRITE signal received.

Logger objects unite several recorders to correspond to some context. You can specify bitmasks for each connected recorder to control which recorders will be used for each severity (log level). Every logger also has a list of default recorders which will be used by default for writing if a custom list is not specified at the write function call.

Usage

🛈 Use godoc to get complete package documentation.

For example, we want to write logs to the stdout and 2 log files. Files should be used as primary endpoints: one for the info messages and one for the errors; stdout gonna be optional endpoint (will use it manually when we need).

First, you should create LogRecorder objects for necessary endpoints. Usually, you will need one recorder per endpoint. Also, we need Logger object to control recorders. If you have several contexts in your app, you probably may need several loggers.

Create and activate recorders:

// ...

stdoutRecorder  := xlog.NewIoDirectRecorder(os.Stdout)
infFileRecorder := xlog.NewIoDirectRecorder(hFileInfo)
errFileRecorder := xlog.NewIoDirectRecorder(hFileError)

go stdoutRecorder.Listen()
go infFileRecorder.Listen()
go errFileRecorder.Listen()

// defer func() { stdoutRecorder.Intrf().ChCtl  <- xlog.SignalStop() }()
// defer func() { intFileRecorder.Intrf().ChCtl <- xlog.SignalStop() }()
// defer func() { errFileRecorder.Intrf().ChCtl <- xlog.SignalStop() }()

OR you can use the SpawnXXX functions (that's the recommended way).

stdoutRecorder  := xlog.SpawnIoDirectRecorder(os.Stdout)
infFileRecorder := xlog.SpawnIoDirectRecorder(hFileInfo)
errFileRecorder := xlog.SpawnIoDirectRecorder(hFileError)

Declare recorder IDs which will be used in the logger to access the recorders.

recStdout := xlog.RecorderID("rec-stdout")
recInfo   := xlog.RecorderID("rec-finfo")
recErr    := xlog.RecorderID("rec-ferr")

Create Logger and connect recorders:

logger := xlog.NewLogger()
_ = logger.RegisterRecorder(recStdout, stdoutRecorder.Intrf())
_ = logger.RegisterRecorder(recInfo,  infFileRecorder.Intrf())
_ = logger.RegisterRecorder(recErr,   errFileRecorder.Intrf())

Logger.RegisterRecorder() registers recorder in the logger and bind it to the given ID. Logger can interact only with registered recorders.

Configure the logger:

_ = logger.DefaultsSet([]RecorderID{recInfo, recErr})
_ = logger.SetSeverityMask(recInfo, xlog.SeverityMinor | xlog.Warning) // Notice | Info | Warning
_ = logger.SetSeverityMask(recErr,  xlog.SeverityMajor) // Warning | Emerg | Alert | Critical | Error

Here we set infFileRecorder and errFileRecorder as default recorders for the logger. These recorders will be used by default unless otherwise specified. So, to write into the stdoutRecorder, we need manually pass recStdout id at the Logger.WriteMsg() call. Also, we set severity masks for the recorders: errFileRecorder will write only "bad" severities ignoring messages with Notice, Info and Debug flags; infFileRecorder opposite will write only Info, Notice and Warning (optional) messages. This allows the recorder to automatically distribute messages to the correct recorders, you need to specify only severity flag for the message.

And finally, initialise it:

if err := logger.Initialise(); err != nil {
    os.Exit(1)
} else {
    defer func() { // at exit
        logger.Close()
    }()
}

To log something just call Logger.Write(). This function receives severity and attribute flags as first parameter and message with arguments as second and further (like fmt.Printf). Logger.Write() always use default recorders of the logger.

logger.Write(xlog.Info, "my message %d", 123)     // will be send to the infFileRecorder
logger.Write(xlog.Error, "something went wrong")  // will be send to the errFileRecorder
logger.Write(xlog.Warning, "imp. notification")   // will be send to both file recorders
logger.Write(xlog.Debug, "some debug info")       // this message will be ignored

If you have a long operation and you want to handle it with a single log message, you can use LogMsg to construct a complex message. In this case, you should use Logger.WriteMsg() instead.

msg := xlog.NewLogMsg().
    SetFlags(xlog.Info).
    Setf("message header\n")
// ... do something
msg.Addf("  op1: OK\n")
// .. do something
msg.Addf("  op2: OK")
msg.UpdateTime()

logger.WriteMsg(nil, msg)

To manually select which recorders (of a specific logger) should be used to handle a message, you should use Logger.WriteMsg() with specified list of recorders IDs as the first argument. Otherwise (nil argument), logger will use default recorders.

// write only to the stdout recorder (registered earlier as recStdout)
logger.WriteMsg([]xlog.RecorderID{recStdout}, xlog.Message("my message %d", 123))

// send Critical message to all recorders
recAll := []xlog.RecorderID{recStdout, recInfo, recErr}
logger.WriteMsg(recAll, xlog.Message("my message").SetFlags(xlog.Critical))

xlog.Message("msg") is equivalent to xlog.NewMsg().Setf("msg")

Some advanced features

Safe initialisation

Logger.Initialise() can receive an optional parameter - list of recorder objects. If it specified the function will use it to call LogRecorder.IsListening() functions to ensure that recorders are ready to receive the signals. Because if some of the recorders are not listening, initialisation call may lock the goroutine. Besides if parameter xlog.cfgAutoStartListening is enabled, the function can call a listener by self and continue without an error.

So, if you don't use SpawnXXX functions, this way is recommended:

l := xlog.NewLogger()
var gRecorders xlog.ListOfRecorders
r1 := xlog.NewIoDirectRecorder(os.Stdout)
r2 := xlog.NewSyslogRecorder("my-prefix")
go r1.Listen()
go r2.Listen()
gRecorders.Add(r1)
gRecorders.Add(r2)

l.RegisterRecorder("REC-1", r1.Intrf())
l.RegisterRecorder("REC-2", r2.Intrf())

if err := l.Initialise(gRecorders); err != nil {
    if err == xlog.ErrNotListening { ... }
}

Handling write errors

Default recorders just skip write signal in case of error to do not lock a caller goroutine. It means that you will not be notified if the error happens. So, if you need to handle msg write errors, you can attach external channel to receive error messages (it's errors from the internal write function only).

First, you need to create a receiver: it must be an async channel chan error with a sufficient buffer size so as not to block the recorder. You should provide the proper usage of this channel(s). If you use a single handler for all your recorders, you have to make sure that it will not be closed or locked while recorders use it.

To set a channel as an error handler for the default recorder you need to send SigSetErrChan control signal. To construct this signal use SignalSetErrChan function. To drop the channel for the logger send SigDropErrChan signal using SignalDropErrChan function.

chErr := make(chan error, 256)
go func() {
    for msg := range chErr {
        if msg == nil { continue } // unreachable
        fmt.Printf("RECORDER ERROR: %s\n", msg.Error())
    }
}

r := xlog.NewIoDirectRecorder(os.Stdout, "my-prefix")
r.Intrf().ChCtl <- xlog.SignalSetErrChan(chErr)
runtime.Gosched()

// ...

r.Intrf().ChCtl <- xlog.SignalDropErrChan()
time.Sleep(time.Second)
close(chErr)

...

Log message formatters & custom flags

You can control the recorder's output by custom format functions. Recorder will call a formatter before the writing to construct a final output string. This functions should implement the interface: type FormatFunc func(*xlog.LogMsg) string. To set format function call LogRecorder.FormatFunc().

For example, to get colored output, you can do this:

r := xlog.NewIoDirectRecorder(os.Stdout).FormatFunc( func(msg *xlog.LogMsg) string {
    // drop attributes, get severity flags only
    sev := msg.GetFlags() &^ xlog.SeverityShadowMask

    // Logger.WriteMsg ensures that several severity flags
    // issue is not possible here; we can use switch here
    switch (sev) {
    case xlog.Emerg:
        return fmt.Sprintf("\x1b[30;41m%s\x1b[0m", msg.GetContent())
    case xlog.Alert:
        return fmt.Sprintf("\x1b[30;41m%s\x1b[0m", msg.GetContent())
    case xlog.Critical:
        return fmt.Sprintf("\x1b[30;41m%s\x1b[0m", msg.GetContent())
    case xlog.Error:
        return fmt.Sprintf("\x1b[31m%s\x1b[0m", msg.GetContent())
    case xlog.Warning:
        return fmt.Sprintf("\x1b[33m%s\x1b[0m", msg.GetContent())
    case xlog.Notice:
        return fmt.Sprintf("\x1b[1m%s\x1b[0m", msg.GetContent())
    default:
        return msg.GetContent()
    }
})

Furthermore, LogMsg has Data field which can be used to pass any kind additional information into the formatter:

type payload struct {
  a string
  b int
}

func formatter(msg *xlog.LogMsg) string {
    var extra string
    if data, ok := msg.Data.(payload); ok {
        extra = fmt.Sprintf(" with %v", data)
    }
    return msg.GetContent() + extra
}

// ...

msg := xlog.NewLogMsg().Setf("message")
msg.Data = payload{"extra", 83485}
logger.WriteMsg(nil, msg)

Besides 10 default flags (8 severities and 2 attributes) custom flags are available. You can declare em like this:

var MySeverity1 xlog.MsgFlagT = xlog.CustomB1
var MySeverity1 xlog.MsgFlagT = xlog.CustomB2

var MyAttribute1 xlog.MsgFlagT = xlog.CustomB3
var MyAttribute2 xlog.MsgFlagT = xlog.CustomB4

With custom flags and message formatters you can realise extra functionality without creating a custom recorder.

Custom recorders

TL;DW
Docs coming soon. For now, you can use rec_direct.go as an example.

# Functions

No description provided by the author
No description provided by the author
Message builds and returns simple message with default severity (0).
No description provided by the author
NewIoDirectRecorder allocates and returns a new I/O direct recorder (recorder which represents endpoint as io.Writer).
NewLogger allocates and returns new logger.
NewLogMsg allocates and returns a new LogMsg.
NewSyslogRecorder allocates and returns a new syslog recorder.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
SpawnIoDirectRecorder creates recorder and starts a listener.
SpawnSyslogRecorder creates recorder and starts a listener.

# Constants

No description provided by the author
0000 0000 0000 0010.
bit-reset (reversed) mask for attribute flags.
No description provided by the author
0000 0000 0000 0100.
0001 0000 0000 0000.
0010 0000 0000 0000.
0100 0000 0000 0000.
1000 0000 0000 0000.
0000 0000 1000 0000.
0000 0000 0000 0001.
0000 0000 0000 1000.
0000 0000 0100 0000.
0000 0000 0010 0000.
Default | Custom.
Custom = CustomB1 | CustomB2.
Default = Major | Minor | Debug.
Major = Emerg | Alert | Critical | Error | Warning.
Minor = Notice | Info.
bit-reset (reversed) mask for severity flags.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
0000 0001 0000 0000.
0000 1000 0000 0000.
0000 0000 0001 0000.

# Variables

If true, Initialise function with passed 'objects' argument will start listeners by self for not-listening recorders.
If true, all Logger methods will be skipped.
ErrNoRecorders returns when user tries to write to the empty logger.
ErrNotInitialised returns when you try to write in the uninitialised logger.
ErrNotListening returns when Logger tries to send a signal to recorder which is not ready to receive signals.
ErrNotWhereToWrite returns when a user tries to write to the logger without configured default recorders with unspecified custom recorders field (nil).
ErrWrongFlagValue returns when some function detects a wrong flag value.
ErrWrongParameter returns when passed parameter is incorrect e.g.
ErrWrongRecorderID returns when recorder id can not be found in the recorder list or if the new id already used in the logger object.

# Structs

BatchResult is designed to accumulate the status (success and failure) of several operations.
This error used for critical situations caused by wrong package usage (by user).
No description provided by the author
LogMsg represents a log message.
RecorderInterface structure represents interface channels of the recorder.

# Interfaces

LogRecorder is an interface for the log endpoint recorder.

# Type aliases

FormatFunc is an interface for the recorder's format function.
No description provided by the author
MsgFlagT type represents messages bit flags for severities and attributes.
RecorderID is an identifier used in Logger functions to select recorders.