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.ContextEnricherfunctions 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
msgpackorJSONformat while logs at error level and above are sent toos.Stderrin in human-readablelogfmtformat -
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 (
logfmtby 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/ulogblugnu/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.Writersuch 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 | truefalse |
false |
LoggerLevel |
The minimum log level to emit | see: Log Levels | ulog.InfoLevel |
LoggerFormat |
The formatter to be used when writing logs | ulog.NewLogfmtFormatterulog.NewJSONFormatterulog.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.
