The alog
framework provides tunable logging with easy-to-use defaults and power-user capabilities. The mantra of alog
is "Log Early And Often". To accomplish this goal, alog
makes it easy to enable verbose logging at develop/debug time and trim the verbosity at production run time.
The primary components of the system are channels and levels which allow for each log statement to be enabled or disabled when appropriate.
-
Channels: Each logging statement is made to a specific channel. Channels are independent of one another and allow for logical grouping of log messages by functionality. A channel can be any string.
-
Levels: Each logging statement is made at a specific level. Levels provide sequential granularity, allowing detailed debugging statements to be placed in the code without clogging up the logs at runtime. The sequence of levels and their general usage is as follows:
off
: Disable the given channel completelyfatal
: A fatal error has occurred. Any behavior after this statement should be regarded as undefined.error
: An unrecoverable error has occurred. Any behavior after this statement should be regarded as undefined unless the error is explicitly handled.warning
: A recoverable error condition has come up that the service maintainer should be aware of.info
: High-level information that is valuable at runtime under moderate load.trace
: Used to log begin/end of functions for debugging code paths.debug
: High-level debugging statements such as function parameters.debug1
: High-level debugging statements.debug2
: Mid-level debugging statements such as computed values.debug3
: Low-level debugging statements such as computed values inside loops.debug4
: Ultra-low-level debugging statements such as data dumps and/or statements inside multiple nested loops.
Using this combination of Channels and Levels, you can fine-tune what log statements are enabled when you run your application under different circumstances.
There are two primary pieces of configuration when setting up the alog
environment:
-
default_level: This is the level that will be enabled for a given channel when a specific level has not been set in the filters.
-
filters: This is a mapping from channel name to level that allows levels to be set on a per-channel basis.
The alog.Config()
function allows both the default level and filters to be set at once. For example:
import (
"github.com/IBM/alchemy-logging/src/go/alog"
)
func foo() {
alog.Config(alog.INFO, alog.ChannelMap{
"FOO": alog.DEBUG,
"BAR": alog.OFF,
})
}
In this example, the channel "FOO"
is set to the DEBUG
level, the channel "BAR"
is fully disabled, and all other channels are set to use the INFO
level.
The standard logging functions each take a channel, a level, a format string, and optional format values. Each one is a wrapper around the standard logging functions from the log
package. The functions are:
-
Log
: Alias toPrintf
. This is the standard logging function. -
Printf
: Perform a standard logging statement. -
Panicf
: Perform a panic logging statement. -
Fatalf
: Perform a fatal logging statement.
Here's a simple example of a basic log statement:
import (
"github.com/IBM/alchemy-logging/src/go/alog"
)
func foo(age int) {
alog.Log("DEMO", alog.INFO, "Hello Logging World! I am %d years old", age)
}
In a given portion of code, it often makes sense to have a common channel that is used by many logging statements. Re-typing the channel name can be cumbersome and error-prone, so the concept of the Channel Log helps to eliminate this issue. To create a Channel Log, call the UseChannel
function. This gives you a handle to a channel log which has all of the same standard log functions as the top-level alog
, but without the requirement to specify a channel. For example:
import (
"github.com/IBM/alchemy-logging/src/go/alog"
)
var ch = alog.UseChannel("DEMO")
func foo(age int) {
ch.Log(alog.INFO, "Hello Logging World! I am %d years old", age)
}
One of the most common uses for logging is to note when a certain block of code starts and ends. To facilitate this, alog
has the concept of the LogScope
. A LogScope
is a simple object which logs a "Start:"
statement at creation time and a "End:"
statement at Close()
time. All logging statements which occur between creation and close will be indented, making for a highly readable log, even with very verbose logging. Here's a simple example of LogScope
:
import (
"github.com/IBM/alchemy-logging/src/go/alog"
)
var ch = alog.UseChannel("DEMO")
func foo(age int) {
ch.Log(alog.INFO, "Hello Logging World! I am %d years old", age)
{
defer ch.LogScope(alog.DEBUG, "This is my own personal scope bubble").Close()
ch.Log(alog.DEBUG, "Hey! I'm walking here!")
}
}
The most common use of LogScope
is to log the begin and end of a function. To help with this, alog
provides the FnLog
and the DetailFnLog
functions. These are wrappers around LogScope
which determine the name of the function being called and add it to the log statement automatically. FnLog
always logs to the trace
level, while DetailFnLog
takes an explicit level. For example:
import (
"github.com/IBM/alchemy-logging/src/go/alog"
)
var ch = alog.UseChannel("DEMO")
func foo(age int) {
defer ch.FnLog("%d", age).Close()
do_foo()
}
func do_foo() {
defer ch.DetailFnLog(alog.DEBUG, "").Close()
ch.Log(alog.DEBUG, "Down in the weeds")
}
WARNING If you do not invoke Close()
on your scope, your application will have a memory leak. The alog
config object holds a map from goroutine ID to indentation level which is incremented at construct time and decremented at close time. Once back to 0, the map entry is removed. If Close()
is not invoked, this map will grow indefinitely. The safest way to ensure that Close()
is always invoked is to use defer
as in the examples above.
There are several other convenience functions available with the alog
package:
-
Indent
/Deindent
: These functions can be used to manually manage indentation within blocks of code. Note that they carry the same WARNING asLogScope
in that an equal number ofDeindent
calls must be made to match theIndent
calls or a memory leak will ensue. -
LevelToHumanString
: This will convert a log level to a human readable string that will match the string used for configuration input. -
PrintConfig
: This constructs a string representation of the current default level and channel map. -
GetDefaultLevel
: Get the default level that's currently configured. -
GetChannelMap
: Get the currently configured channel map. -
IsEnabled
: This basic predicate allows for the construction of labor-intensive log statements to be wrapped in an if block and only executed when the given channel/level pair is active. It should NOT be used to wrap functional code since that code would be disabled if the check fails. Here's an example of correct usage:
import (
"fmt"
"github.com/IBM/alchemy-logging/src/go/alog"
)
var ch = alog.UseChannel("DEMO")
func foo(ages map[string]int) {
defer ch.FnLog("%d", age).Close()
if ch.IsEnabled(alog.INFO) {
s := "Age Map:\n"
for name, age := range ages {
s += fmt.Sprintf(" %s: %d\n", name, age)
}
ch.Log(alog.INFO, s)
}
}
In addition to the standard configuration for default level and filters, there are a number of additional configuration functions:
-
ResetDefaults
: Reset configuration to all standard defaults. -
ConfigWriter
: Set theio.Writer
instance to use as the backend for logging. This can be used to send log statements to places other thanos.Stderr
. -
SetMaxChannelLen
: Set the truncation length for channel strings in the header. -
EnableGID
/DisableGID
: These functions enable or disable printing the numeric goroutine ID as part of the log statement header. -
EnableFullFuncSig
/DisableFullFuncSig
: These functions enable or disable printing the full function signature as part of theFnLog
functions. -
UseJSONLogFormatter
: This function switches the formatter from standard pretty-printing to a key/value JSON format. This is particularly useful when logs are being sent to a collection server such as Logmet.
In addition to the core functionality, a number of convenient extras come along with the alog
package to help with common usage patterns.
The most common usage for alog
is as a command-line configurable logging framework. As such, a standard set of command line flags are provided with documentation. The important functions for this functionality are:
-
GetFlags
: Construct the standard set ofalog
command line flags. -
ConfigureFromFlags
: Configurealog
using the parsed command line flags.
Here's a simple example:
package main
import (
"flag"
"github.com/IBM/alchemy-logging/src/go/alog"
)
var ch = alog.UseChannel("MAIN")
func main() {
// Set up logging from command line
logFlags := alog.GetFlags()
flag.Parse()
alog.ConfigureFromFlags(logFlags)
// Do some logging
ch.Log(alog.INFO, "Hello World!")
ch.Log(alog.DEBUG2, "lots of details...")
alog.Log("OTHER", alog.INFO, "Logging on a different channel")
}
Here's the overview of the available command-line options:
log.default-level
: Set the default log level using one of the strings defined in the Levels Section.log.filters
: Filter string in the form"CHAN1:info,CHAN2:debug"
.log.chan-header-len
: Set the length of the channel string in the header.log.goroutine-id
: Include a unique numeric ID for the goroutine in each header.log.function-signature
: Log the fully-qualified function signature forFnLog
invocations. If false, only the function name is logged.log.no-indent
: Fully disable indentationlog.service-name
: Log a common label for the service with each entry (useful for publishing to common logging service).log.output-json
: Output log entries as structured json rather than plain text.
When implementing an HTTP server, it can be very useful to allow for dynamic logging so that the server can be launched with logging disabled, but have it enabled for a short time to inspect traffic. To facilitate this, the DynamicHandler
can be bound to a route and called with the following parameters:
-
default_level
: String representing a new default log level to use. -
filters
: New channel map to use, formatted as a string with commas separating entries and colons separating key/value pairs. For example"FOO:debug,BAR:info"
. -
timeout
: If provided, the changes will automatically be reverted in the provided number of seconds.
Here's a simple example:
package main
import (
"flag"
"github.com/IBM/alchemy-logging/src/go/alog"
"net/http"
"strings"
)
var ch = alog.UseChannel("MAIN")
func main() {
// Get a port from the command line
listenPort := flag.String(
"port",
"54321",
"port bound to this service")
// Set up logging from command line
logFlags := alog.GetFlags()
flag.Parse()
alog.ConfigureFromFlags(logFlags)
ch.Log(alog.INFO, "Hello World!")
// Bind dynamic log handler
http.HandleFunc("/logging", alog.DynamicHandler)
// Bind simple function that does some logging
http.HandleFunc("/demo", func(w http.ResponseWriter, r *http.Request){
ch := alog.UseChannel("HNDLR")
defer ch.LogScope(alog.TRACE, "Handling /demo").Close()
ch.Log(alog.WARNING, "WATCH OUT!")
ch.Log(alog.INFO, "Standard stuff...")
if ch.IsEnabled(alog.DEBUG) {
ch.Log(alog.DEBUG, "Query Params:")
r.ParseForm()
for k, vals := range r.Form {
ch.Log(alog.DEBUG, " * %s: %s", k, strings.Join(vals, ", "))
}
}
w.WriteHeader(http.StatusOK)
})
// Start serving requests
ch.Log(alog.FATAL, "%s", http.ListenAndServe(":"+*listenPort, nil))
}