A configurable, structured logging library for Go that does not sacrifice (too much) efficiency in exchange for convenience and flexibility.
Features unique to ulog
:
-
Automatic Context Enrichment - logs may be enriched with context from the context provided by the caller or from context carried by an error (via support for the blugnu/errorcontext module); automatic contextual enrichment can be embedded by registering
ulog.ContextEnricher
functions with the logger -
Multiplexing - (optionally) send logs to multiple destinations simultaneously with independently configured formatting and leveling; for example, full logs could be sent to an aggregator in
msgpack
orJSON
format while logs at error level and above are sent toos.Stderr
in in human-readablelogfmt
format -
Testable - use the provided mock logger to verify that the logs expected by your observability alerts are actually produced!
Features you'd expect of any logger:
-
Highly Configurable - configure your logger to emit logs in the format you want, with the fields you want, at the level you want, sending them to the destination (or destinations) you want; the default configuration is designed to be sensible and useful out-of-the-box, but can be easily customised to suit your needs
-
Structured Logs - logs are emitted in a structured format (
logfmt
by default) that can be easily parsed by log aggregators and other tools; a JSON formatter is also provided -
Efficient - allocations are kept to a minimum and conditional flows eliminated where-ever possible to ensure that the overhead of logging is kept to a minimum
-
Intuitive - a consistent API using Golang idioms that is easy and intuitive to use
go get github.com/blugnu/ulog
blugnu/ulog
is built on the following main stack:
- Golang – Languages
- GitHub Actions – Continuous Integration
Full tech stack here
A logger is created using the ulog.NewLogger
factory function.
This function returns a logger, a function to close the logger and an error if the logger could not be created. The close function must be called when the logger is no longer required to release any resources it may have acquired and any batched log entries are flushed.
The logger may be configured using a set of configuration functions that are passed as arguments to the factory function.
A minimal example of using ulog
:
package main
func main() {
ctx := context.Background()
// create a new logger
logger, closelog, err := ulog.NewLogger(ctx)
if err != nil {
log.Fatalf("error initialising logger: %s", err)
}
defer closelog()
// log a message
logger.Info("hello world")
logger.Error("oops!")
}
This example uses a logger with default configuration that writes log entries at
InfoLevel
and above to os.Stdout
in logfmt
format.
Running this code would produce output similar to the following:
time=2023-11-23T12:35:04.789346Z level=INFO msg="hello world"
time=2023-11-23T12:35:04.789347Z level=ERROR msg="oops!"
NOTE: This section deals with configuration of a simple logger sending output to a single
io.Writer
such asos.Stdout
. Configuration of a multiplexing logger is described separately.
The default configuration is designed to be sensible and useful out-of-the-box but can be customised to suit your needs.
The Functional Options Pattern is
employed for configuration. To configure the logger pass the required option functions
as additional arguments to the NewLogger()
factory. All options have sensible defaults.
Option | Description | Options | Default (if not configured) |
---|---|---|---|
LogCallSite |
Whether to include the file name and line number of the call-site in the log message | true false |
false |
LoggerLevel |
The minimum log level to emit | see: Log Levels | ulog.InfoLevel |
LoggerFormat |
The formatter to be used when writing logs | ulog.NewLogfmtFormatter ulog.NewJSONFormatter ulog.NewMsgpackFormatter |
ulog.NewLogfmtFormatter() |
LoggerOutput |
The destination to which logs are written | any io.Writer |
os.Stdout |
Mux |
Use a multiplexer to send logs to multiple destinations simultaneously | ulog.Mux . See: Mux Configuration |
- |
The following levels are supported (in ascending order of significance):
Level | Description | |
---|---|---|
ulog.TraceLevel |
low-level diagnostic logs | |
ulog.DebugLevel |
debug messages | |
ulog.InfoLevel |
informational messages | default |
ulog.WarnLevel |
warning messages | |
ulog.ErrorLevel |
error messages | |
ulog.FatalLevel |
fatal errors |
The default log level may be overridden using the LoggerLevel
configuration function
to NewLogger
, e.g:
ulog.NewLogger(
ulog.LoggerLevel(ulog.DebugLevel)
)
In this example, the logger is configured to emit logs at DebugLevel
and above. TraceLevel
logs would not be emitted.
LoggerLevel
establishes the minimum log level for the logger.
If the logger is configured with a multiplexer the level for each mux target may be set independently but any target level that is lower than the logger level is effectively ignored.
Mux Target Level | Logger Level | Effective Mux Target Level |
---|---|---|
ulog.InfoLevel |
ulog.InfoLevel |
ulog.InfoLevel |
ulog.DebugLevel |
ulog.InfoLevel |
ulog.InfoLevel |
ulog.DebugLevel |
ulog.TraceLevel |
ulog.DebugLevel |
ulog.InfoLevel |
ulog.WarnLevel |
ulog.WarnLevel |
The following formatters are supported:
Format | LoggerFormat Option | Description |
---|---|---|
logfmt | ulog.NewLogfmtFormatter |
a simple, structured format that is easy for both humans and machines to read This is the default formatter if none is configured |
JSON | ulog.NewJSONFormatter |
a structured format that is easy for machines to parse but can be noisy for humans |
msgpack | ulog.NewMsgpackFormatter |
an efficient structured, binary format for machine use, unintelligible to (most) humans |
Formatters offer a number of configuration options that can be set via functions supplied
to their factory function. For example, to configure the name used for the time
field by a
logfmt
formatter:
log, closelog, _ := ulog.NewLogger(ctx,
ulog.LoggerFormat(ulog.LogfmtFormatter(
ulog.LogfmtFieldNames(map[ulog.FieldId]string{
ulog.TimeField: "timestamp",
}),
)),
)
Logger output may be written to any io.Writer
. The default output is os.Stdout
.
The output may be configured via the LoggerOutput
configuration function when configuring a new logger:
logger, closefn, err := ulog.NewLogger(ctx, ulog.LoggerOutput(io.Stderr))
If desired, the logger may be configured to emit the file name and line number of the call-site that produced the log message. This is disabled by default.
Call-site logging may be enabled via the LogCallSite
configuration function when configuring a new logger:
logger, closelog, err := ulog.NewLogger(ctx, ulog.LogCallSite(true))
If LogCallsite
is enabled call site information is added to all log entries, including
multiplexed output where configured, regardless of the level.
Logs may be sent to multiple destinations simultaneously using a ulog.Mux
. For example,
full logs could be sent to an aggregator using msgpack
format, while logfmt
formatted
logs at error level and above are also sent to os.Stderr
.