Release v3.0.0. (Getting Started)
LogAlpha is a minimal framework for logging in Python focused on simplicity. It provides only the core building blocks of a logging system and then gets out of your way. Instead of trying to solve every possible scenario, this library doesn’t even require you log text or that it will be written to a file.
The library provides a set of base classes to build from. On the Getting Started page you will get a sense of how the library works. A full API reference is included as well.
Table of Contents
LogAlpha can be installed using Pip.
➜ pip install logalpha
LogAlpha expects you to build your own arrangement. That said, some typical arrangements have been pre-built for both easy-of-use and the sake of demonstration.
The standard behavior one might expect is to have the canonical five logging levels and detailed messages printed to stderr with a timestamp, hostname, and a topic. This is available from the logalpha.contrib.standard module.
from logalpha.contrib.standard import StandardLogger, StandardHandler handler = StandardHandler() StandardLogger.handlers.append(handler)
In other modules create instances with a named topic.
log = StandardLogger(__name__)
The logger will automatically be instrumented with methods for each logging level. As is conventional, the default logging level is WARNING.
log.info('message')
log.warning('message')
2020-10-12 20:48:10.555 hostname.local WARNING [__main__] message
Instead of using a pre-built arrangement, let’s define our own custom logging behavior. For simple programs, it might be more appropriate to just log a status of Ok or Error.
We need to build a Logger with out own custom set of Levels. Then a Handler for our messages.
Logger
Level
Handler
import sys from dataclasses import dataclass from typing import List, IO, Callable from logalpha.color import Color, ANSI_RESET from logalpha.level import Level from logalpha.message import Message from logalpha.handler import StreamHandler from logalpha.logger import Logger class OkayLogger(Logger): """Logger with Ok/Err levels.""" levels: List[Level] = Level.from_names(['Ok', 'Err']) colors: List[Color] = Color.from_names(['green', 'red']) @dataclass class OkayHandler(StreamHandler): """ Writes to <stderr> by default. Message format includes the colorized level and the text. """ level: Level = OkayLogger.levels[0] # Ok resource: IO = sys.stderr def format(self, message: Message) -> str: """Format the message.""" color = OkayLogger.colors[message.level.value].foreground return f'{color}{message.level.name:<3}{ANSI_RESET} {message.content}'
Warning
Don’t forget to include the dataclass decorator on your Handler and Message derived classes. If you aren’t adding any new fields then things should work find though.
dataclass
Message
We can setup our logger the same way we did for the standard logger.
handler = OkayHandler() OkayLogger.handlers.append(handler)
Again, the logger is automatically instrumented with level methods.
log = OkayLogger()
log.ok('operation succeeded')
Ok operation succeeded
Note
If you get warnings from your IDE about these level methods being unknown when using your logger, this is because they are dynamically generated. You can add type annotations to your class to avoid this if you like.
The names of these methods will always be the Level.name in lower-case.
Level.name
class OkayLogger(Logger): """Logger with Ok/Err levels.""" levels: List[Level] = Level.from_names(['Ok', 'Err']) colors: List[Color] = Color.from_names(['green', 'red']) # stubs for instrumented level methods ok: Callable[[str], None] err: Callable[[str], None]
For more advanced logging setups you might want to specifically define additional metadata you want attached to every message. A Message is a simple dataclass. Be default it only includes a level and content. Extend it by subclassing the Message class and adding your attributes.
level
content
from datetime import datetime from logalpha.level import Level from logalpha.message import Message @dataclass class DetailedMessage(Message): """A message with additional attributes.""" level: Level content: str timestamp: datetime topic: str host: str
You can in fact define the content of a message to be something other than a string, and the handler(s) can in turn define a format and write method accordingly.
Again, the message itself just a simple dataclass. The Logger creates the message when you call one of the level methods and will need callbacks defined for each of these attributes that return a value.
from datetime import datetime from socket import gethostname from typing import Type, Callable, IO from logalpha.level import Level from logalpha.message import Message from logalpha.logger import Logger HOST: str = gethostname() class DetailedLogger(Logger): """Logger with detailed messages.""" Message: Type[Message] = DetailedMessage topic: str def __init__(self, topic: str) -> None: """Initialize with `topic`.""" super().__init__() self.topic = topic self.callbacks = {'timestamp': datetime.now, 'host': (lambda: HOST), 'topic': (lambda: topic)}
There is a one-to-one relationship between the Logger and the Message you define. You should implement one or more Handler classes that expect the same Message as input but differing in how they format the message or what type of resource they write to.
logalpha.level
A level associates a name and a value.
>>> level = Level(name='INFO', value=1) >>> level Level(name='INFO', value=1)
from_names
Construct a set of contiguous Levels.
>>> levels = Level.from_names(['Ok', 'Err']) >>> levels [Level(name='Ok', value=0), Level(name='Err', value=1)]
Comparisons operate on the value attribute.
value
__lt__
Returns self.value < other.value.
self.value < other.value
>>> a, b = Level.from_names(['A', 'B']) >>> assert a < b >>> assert b > a
__gt__
Similar to Level.__lt__().
Level.__lt__()
__le__
__ge__
For convenience and readability, a set of global named instances are included for the standard set of logging levels.
DEBUG
INFO
WARNING
ERROR
CRITICAL
LEVELS
logalpha.color
Color
Associates a name (str) with its corresponding foreground and background (str) ANSI codes. Construct one or more instances using the factory methods.
from_name
Lookup ANSI codes by name.
>>> red = Color.from_name('red') >>> red Color(name='red', foreground='[31m', background='[41m')
Returns a tuple of Color instances using the singular from_name() factory.
from_name()
>>> colors = Color.from_names(['blue', 'green']) >>> colors [Color(name='blue', foreground='[34m', background='[44m'), Color(name='green', foreground='[32m', background='[42m')]
NAMES
Names of supported colors to map to ANSI codes.
ANSI_RESET
ANSI reset code.
ANSI_COLORS
Dictionary of all ANSI code sequences.
For convenience and readability, a set of global named instances are included for all of the colors.
BLACK
RED
GREEN
YELLOW
BLUE
MAGENTA
CYAN
WHITE
logalpha.message
Associates a level with content. Derived classes should add new fields. The Logger should define callbacks to populate these new fields.
>>> msg = Message(level=INFO, content='Hello, world!') Message(level=Level(name='INFO', value=1), content='Hello, world!')
logalpha.logger.Logger
It is not intended that you directly instantiate a message. Messages are automatically constructed by the Logger when calling one of the instrumented level methods.
logalpha.handler
The base Handler class should be considered an abstract class. If deriving directly from the base class you should implement both the format() and the write(). methods.
format()
write()
Core message handling interface. A Handler associates a level with a resource.
Any
write
Publish message to resource after calling format.
format
Format message.
A minimum viable implementation is provided in StreamHandler. This handler wants a file-like resource to write to. It’s write() method literally calls print() with the resource as the file.
StreamHandler
print()
Bases: logalpha.handler.Handler
logalpha.handler.Handler
Publish messages to a file-like resource.
sys.stderr
Returns message.content.
message.content
The StreamHandler class implements everything needed for messages to be published to stderr or some other file-like object. To customize formatting, extend the class by overriding the format() method. Just for formatting this seems like a lot of boilerplate; however, by making it a function call it’s possible to inject any arbitrary code.
@dataclass class MyHandler(StreamHandler): """A :class:`~StreamHandler` with custom formatting.""" def format(self, message: Message) -> str: return f'{message.level.name} {message.content}'
logalpha.logger
Base logging interface.
By default the levels and colors are the conventional set. Append any number of appropriate handlers.
>>> log = Logger() >>> log.warning('foo')
>>> Logger.handlers.append(StreamHandler()) >>> log.warning('bar') bar
levels
colors
Publish message to all handlers if its level is sufficient for that handler.
It’s expected that the logger will be called with one of the dynamically instrumented level methods (e.g., info()), and not call the write() method directly.
info()
logalpha.contrib.ok
OkayLogger
Bases: logalpha.logger.Logger
Logger with Ok/Err levels.
>>> log = OkayLogger() >>> log.ok('foo')
>>> Logger.handlers.append(OkayHandler()) >>> log.ok('bar') Ok bar
OkayHandler
Bases: logalpha.handler.StreamHandler
logalpha.handler.StreamHandler
Writes to <stderr> by default. Message format includes the colorized level and the text.
OK
Format the message.
ERR
logalpha.contrib.simple
SimpleMessage
Bases: logalpha.message.Message
logalpha.message.Message
A message with a named topic.
SimpleLogger
Logger with SimpleMessage.
SimpleHandler
Writes to <stderr> by default. Message format includes topic and level name.
ColorHandler
Writes to <stderr> by default. Message format colorizes level name.
logalpha.contrib.standard
StandardMessage
A message with standard attributes.
StandardLogger
Logger with StandardMessage.
StandardHandler
A standard message handler writes to <stderr> by default. Message format includes all attributes.
Development of LogAlpha happens on Github. If you find bugs or have questions or suggestions, open an Issue on Github. Fixes and new features are welcome in the form of Pull Requests.
If and when LogAlpha gains any significant number of additional contributors, a code of conduct will be included; until then, just be nice.
LogAlpha is released under the Apache Software License (v2).
Many open source software projects are released under the GNU Public License, or similar. For some, this is an appropriate choice. However, a project that is released under a GPL licence cannot be used as part of any commercial product or service (without it also being made open source).
Under the Apache Software License, anyone is free to use cmdkit as part of a proprietary, closed-source project. That’s a good thing!
Copyright 2019-2021 Geoffrey Lentner.
This program is free software: you can redistribute it and/or modify it under the terms of the Apache License (v2.0) as published by the Apache Software Foundation.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Apache License for more details.
You should have received a copy of the Apache License along with this program. If not, see https://www.apache.org/licenses/LICENSE-2.0.