Usage (Python)¶
Concepts and data types¶
Config¶
lolog defines a small number of types which interact to produce log output.
The starting point is Config,
which is where all log configuration lives.
Normally there will be exactly one instance of Config per process,
but you are free to create more if you like.
(This might be useful in test code.)
Most other lolog objects reference a Config object.
You can configure logging and get a Config object in one shot:
cfg = lolog.init()
This creates and returns
the global default Config object.
When called with no arguments,
init() configures reasonable defaults
for logging in development:
emit all log messages (default level is
DEBUG)write log messages to stderr
format in simple human-readable plain text:
key=val ...
You can override each of these with optional keyword arguments: see the API reference for details.
You are only allowed to call init() once per process.
(Library code MUST NOT call init() —
it is reserved for application startup code.)
See below for other ways to get a Config object.
All configuration information lives in the config object:
The default log level, used by loggers with no explicit log level.
The log level for each logger (if configured).
The pipeline, a list of functions that operate on log records.
The default pipeline created by init() looks like:
formatter → stream output
Pipelines will be described in more detail below.
Logger¶
Your interactions with Config
should mostly happen at application initialization time.
The vast majority of time that you use lolog,
you will be working with Logger objects directly.
Every logger is named and tied to a config object.
The usual way to get a logger is from the global default config:
log = lolog.get_logger('foo.bar')
But if you are working with a custom config, you can fetch a logger from it:
log = cfg.get_logger('foo.bar')
Logger names can be any string, but I recommend a dot-separated sequence of short ASCII tokens. Further, logger names should describe the code responsible for the messages emitted by that logger. In Python, this is easily done by using the fully-qualified module name:
log = lolog.get_logger(__name__)
For example, if this is in file foo/bar.py (module foo.bar),
messages will be emitted by the logger named foo.bar.
lolog has no concept of a relationship between loggers:
it has no idea that logger foo and foo.bar
come from related code.
That is, the configuration for foo
has no affect on any other logger,
not even foo.bar.
If you want to affect the behaviour of many loggers, use a pattern:
cfg.set_logger_pattern_level('foo.*', lolog.INFO)
Levels and level filtering¶
Log level describes the importance of a log message. This lets you filter out less important messages and focus on the important stuff, at least in your production environment. In development, you probably want to see everything (or at least everything from your own code). lolog’s levels enable all of this.
Log levels are defined by the Level enum.
The sequence of log levels,
from least important to most important, is:
DEBUG
INFO
WARNING
ERROR
CRITICAL
There are two other values in the Level enum:
NOTSET: used to detect when the desired log level has not been configured
SILENT: used to completely suppress all log output
These can both be used in configuration, but not used as the level of a log message.
Whenever a log message is emitted,
it has a level.
The level is determined by the method called:
log.debug emits messages with level DEBUG, etc.
The message level is compared to the level
configured for that logger,
or to the config’s default level
if the logger has not been configured.
If the message level is less than the configured level,
this message is dropped.
For example, if lolog’s default config object has been configured like:
cfg.set_default_level(lolog.INFO)
cfg.set_logger_level("foo", lolog.DEBUG)
cfg.set_logger_level("bar", lolog.WARNING)
cfg.set_logger_level("noo", lolog.SILENT)
Then the following messages will be emitted:
lolog.get_logger("foo").debug("kept")
lolog.get_logger("foo").info("kept")
lolog.get_logger("bar").warning("kept")
lolog.get_logger("qux").info("kept")
but these will be suppressed:
lolog.get_logger("bar").info("dropped")
lolog.get_logger("qux").debug("dropped")
lolog.get_logger("noo").critical("nuclear launch detected")
Record¶
Every log message results in a Record object.
If all you do is call methods on a logger
and read log files,
this is normally invisible to you.
But if you need to customize lolog
by writing new pipeline stages,
you will have to interact with Record objects.
Every log record has the following attributes:
time: float, seconds since POSIX epoch
name: str, name of the logger that created this record
level: Level, determined by the logger method called
message: str, the fixed string passed as the first argument
log_map: list of key-value pairs
outbuf: used for interaction between format and output stages
The logging pipeline¶
Every Config object has exactly one pipeline,
which is a sequence of pipeline stages.
Each stage is a callable with signature:
stage(config: Config, record: Record) -> Optional[Record]
where
configis the config object that is in charge
recordcontains all the information available at the time the log message was created
To drop this log record, return None. Otherwise, return the log record (possibly modified).
Filtering stage¶
For example, here is a custom filtering stage
that drops INFO-level messages
from noisylib
that contain the string "foobar":
import lolog
def filter(config: lolog.Config, record: lolog.Record) -> Optional[lolog.Record]:
if (record.name == "noisylib"
and record.level == lolog.INFO
and "foobar" in record.message):
return None
return record
Replacing log records¶
lolog’s Record type is immutable, so you can’t modify log records.
But you can replace them with new ones!
Imagine instead that you don’t want to drop those "foobar" messages from noisylib,
just censor out the dangerous word "foobar".
You can do that with a pipeline stage like this:
def replace_foobar(config: lolog.Config, record: lolog.Record) -> Optional[lolog.Record]:
if (record.name == "noisylib"
and record.level == lolog.INFO
and "foobar" in record.message):
message = record.message.replace("foobar", "******")
record = record.replace(message=message)
return record
Be careful not to fall off the end of a stage function and implicitly return None! That will drop the log message, probably not your intention.