LogAlpha

Release v3.0.0. (Getting Started)

License PyPI Version Python Versions Documentation Downloads

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

Getting Started

Installation

LogAlpha can be installed using Pip.

➜ pip install logalpha


Basic Usage

Pre-built Arrangements

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

Do-It Yourself

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.

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.


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.

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]

Adding Custom Metadata

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.

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

Note

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)}


Discussion

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.


API

logalpha.level




class Level(name: str, value: int)[source]

A level associates a name and a value.

Example:
>>> level = Level(name='INFO', value=1)
>>> level
Level(name='INFO', value=1)

classmethod from_names(names: List[str]) → List[logalpha.level.Level][source]

Construct a set of contiguous Levels.

Example:
>>> levels = Level.from_names(['Ok', 'Err'])
>>> levels
[Level(name='Ok', value=0),
 Level(name='Err', value=1)]

Comparisons operate on the value attribute.

__lt__(other: logalpha.level.Level) → bool[source]

Returns self.value < other.value.

Example:
>>> a, b = Level.from_names(['A', 'B'])
>>> assert a < b
>>> assert b > a
__gt__(other: logalpha.level.Level) → bool[source]

Similar to Level.__lt__().

__le__(other: logalpha.level.Level) → bool[source]

Similar to Level.__lt__().

__ge__(other: logalpha.level.Level) → bool[source]

Similar to Level.__lt__().




For convenience and readability, a set of global named instances are included for the standard set of logging levels.

DEBUG = Level(name='DEBUG', value=0)
INFO = Level(name='INFO', value=1)
WARNING = Level(name='WARNING', value=2)
ERROR = Level(name='ERROR', value=3)
CRITICAL = Level(name='CRITICAL', value=4)
LEVELS = [Level(name='DEBUG', value=0), Level(name='INFO', value=1), Level(name='WARNING', value=2), Level(name='ERROR', value=3), Level(name='CRITICAL', value=4)]

logalpha.color




class Color(name: str, foreground: str, background: str)[source]

Associates a name (str) with its corresponding foreground and background (str) ANSI codes. Construct one or more instances using the factory methods.


classmethod from_name(name: str) → logalpha.color.Color[source]

Lookup ANSI codes by name.

Example:
>>> red = Color.from_name('red')
>>> red
Color(name='red', foreground='[31m', background='[41m')

classmethod from_names(names: List[str]) → List[logalpha.color.Color][source]

Returns a tuple of Color instances using the singular from_name() factory.

Example:
>>> colors = Color.from_names(['blue', 'green'])
>>> colors
[Color(name='blue', foreground='[34m', background='[44m'),
 Color(name='green', foreground='[32m', background='[42m')]



NAMES = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']

Names of supported colors to map to ANSI codes.

ANSI_RESET = '\x1b[0m'

ANSI reset code.

ANSI_COLORS = {'background': {'black': '\x1b[40m', 'blue': '\x1b[44m', 'cyan': '\x1b[46m', 'green': '\x1b[42m', 'magenta': '\x1b[45m', 'red': '\x1b[41m', 'white': '\x1b[47m', 'yellow': '\x1b[43m'}, 'foreground': {'black': '\x1b[30m', 'blue': '\x1b[34m', 'cyan': '\x1b[36m', 'green': '\x1b[32m', 'magenta': '\x1b[35m', 'red': '\x1b[31m', 'white': '\x1b[37m', 'yellow': '\x1b[33m'}}

Dictionary of all ANSI code sequences.


For convenience and readability, a set of global named instances are included for all of the colors.

BLACK = Color(name='black', foreground='\x1b[30m', background='\x1b[40m')
RED = Color(name='red', foreground='\x1b[31m', background='\x1b[41m')
GREEN = Color(name='green', foreground='\x1b[32m', background='\x1b[42m')
YELLOW = Color(name='yellow', foreground='\x1b[33m', background='\x1b[43m')
BLUE = Color(name='blue', foreground='\x1b[34m', background='\x1b[44m')
MAGENTA = Color(name='magenta', foreground='\x1b[35m', background='\x1b[45m')
CYAN = Color(name='cyan', foreground='\x1b[36m', background='\x1b[46m')
WHITE = Color(name='white', foreground='\x1b[37m', background='\x1b[47m')

logalpha.message




class Message(level: logalpha.level.Level, content: Any)[source]

Associates a level with content. Derived classes should add new fields. The Logger should define callbacks to populate these new fields.

Example:
>>> msg = Message(level=INFO, content='Hello, world!')
Message(level=Level(name='INFO', value=1), content='Hello, world!')
See Also:
logalpha.logger.Logger

Note

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.

class Handler(level: logalpha.level.Level, resource: Any)[source]

Core message handling interface. A Handler associates a level with a resource.

Attributes:
level (Level):
The level for this handler.
resource (Any):
Some resource to publish messages to.
write(message: logalpha.message.Message) → None[source]

Publish message to resource after calling format.

format(message: logalpha.message.Message) → Any[source]

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.

class StreamHandler(level: logalpha.level.Level = Level(name='WARNING', value=2), resource: IO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>)[source]

Bases: logalpha.handler.Handler

Publish messages to a file-like resource.

Attributes:
level (Level):
The level for this handler (default: WARNING).
resource (IO):
File-like resource to write to (default: sys.stderr).
write(message: logalpha.message.Message) → None[source]

Publish message to resource after calling format.

format(message: logalpha.message.Message) → str[source]

Returns 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




class Logger[source]

Base logging interface.

By default the levels and colors are the conventional set. Append any number of appropriate handlers.

Example:
>>> log = Logger()
>>> log.warning('foo')
>>> Logger.handlers.append(StreamHandler())
>>> log.warning('bar')
bar

levels = [Level(name='DEBUG', value=0), Level(name='INFO', value=1), Level(name='WARNING', value=2), Level(name='ERROR', value=3), Level(name='CRITICAL', value=4)]
colors = [Color(name='blue', foreground='\x1b[34m', background='\x1b[44m'), Color(name='green', foreground='\x1b[32m', background='\x1b[42m'), Color(name='yellow', foreground='\x1b[33m', background='\x1b[43m'), Color(name='red', foreground='\x1b[31m', background='\x1b[41m'), Color(name='magenta', foreground='\x1b[35m', background='\x1b[45m')]

write(level: logalpha.level.Level, content: Any) → None[source]

Publish message to all handlers if its level is sufficient for that handler.

Note

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.


logalpha.contrib.ok




class OkayLogger[source]

Bases: logalpha.logger.Logger

Logger with Ok/Err levels.

Example:
>>> log = OkayLogger()
>>> log.ok('foo')
>>> Logger.handlers.append(OkayHandler())
>>> log.ok('bar')
Ok  bar

levels = [Level(name='Ok', value=0), Level(name='Err', value=1)]
colors = [Color(name='green', foreground='\x1b[32m', background='\x1b[42m'), Color(name='red', foreground='\x1b[31m', background='\x1b[41m')]



class OkayHandler(level: logalpha.level.Level = Level(name='Ok', value=0), resource: IO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>)[source]

Bases: logalpha.handler.StreamHandler

Writes to <stderr> by default. Message format includes the colorized level and the text.

Attributes:
level (Level):
The level for this handler (default: OK).
resource (IO):
File-like resource to write to (default: sys.stderr).

format(message: logalpha.message.Message) → str[source]

Format the message.




OK = Level(name='Ok', value=0)
ERR = Level(name='Err', value=1)

logalpha.contrib.simple




class SimpleMessage(level: logalpha.level.Level, content: str, topic: str)[source]

Bases: logalpha.message.Message

A message with a named topic.




class SimpleLogger(topic: str)[source]

Bases: logalpha.logger.Logger

Logger with SimpleMessage.


levels = [Level(name='DEBUG', value=0), Level(name='INFO', value=1), Level(name='WARNING', value=2), Level(name='ERROR', value=3), Level(name='CRITICAL', value=4)]
colors = [Color(name='blue', foreground='\x1b[34m', background='\x1b[44m'), Color(name='green', foreground='\x1b[32m', background='\x1b[42m'), Color(name='yellow', foreground='\x1b[33m', background='\x1b[43m'), Color(name='red', foreground='\x1b[31m', background='\x1b[41m'), Color(name='magenta', foreground='\x1b[35m', background='\x1b[45m')]



class SimpleHandler(level: logalpha.level.Level = Level(name='WARNING', value=2), resource: IO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>)[source]

Bases: logalpha.handler.StreamHandler

Writes to <stderr> by default. Message format includes topic and level name.

Attributes:
level (Level):
The level for this handler.
resource (Any):
Some resource to publish messages to.

format(message: logalpha.contrib.simple.SimpleMessage) → str[source]

Format the message.




class ColorHandler(level: logalpha.level.Level = Level(name='WARNING', value=2), resource: IO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>)[source]

Bases: logalpha.handler.StreamHandler

Writes to <stderr> by default. Message format colorizes level name.

Attributes:
level (Level):
The level for this handler.
resource (Any):
Some resource to publish messages to.

format(message: logalpha.contrib.simple.SimpleMessage) → str[source]

Format the message.




DEBUG = Level(name='DEBUG', value=0)
INFO = Level(name='INFO', value=1)
WARNING = Level(name='WARNING', value=2)
ERROR = Level(name='ERROR', value=3)
CRITICAL = Level(name='CRITICAL', value=4)

logalpha.contrib.standard




class StandardMessage(level: logalpha.level.Level, content: str, timestamp: datetime.datetime, topic: str, host: str)[source]

Bases: logalpha.message.Message

A message with standard attributes.




class StandardLogger(topic: str)[source]

Bases: logalpha.logger.Logger

Logger with StandardMessage.


levels = [Level(name='DEBUG', value=0), Level(name='INFO', value=1), Level(name='WARNING', value=2), Level(name='ERROR', value=3), Level(name='CRITICAL', value=4)]
colors = [Color(name='blue', foreground='\x1b[34m', background='\x1b[44m'), Color(name='green', foreground='\x1b[32m', background='\x1b[42m'), Color(name='yellow', foreground='\x1b[33m', background='\x1b[43m'), Color(name='red', foreground='\x1b[31m', background='\x1b[41m'), Color(name='magenta', foreground='\x1b[35m', background='\x1b[45m')]



class StandardHandler(level: logalpha.level.Level = Level(name='WARNING', value=2), resource: IO = <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>)[source]

Bases: logalpha.handler.StreamHandler

A standard message handler writes to <stderr> by default. Message format includes all attributes.

Attributes:
level (Level):
The level for this handler.
resource (Any):
Some resource to publish messages to.

format(message: logalpha.contrib.standard.StandardMessage) → str[source]

Format the message.




DEBUG = Level(name='DEBUG', value=0)
INFO = Level(name='INFO', value=1)
WARNING = Level(name='WARNING', value=2)
ERROR = Level(name='ERROR', value=3)
CRITICAL = Level(name='CRITICAL', value=4)

Contributing


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.


License


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.