o3bvv / isotopic-logging

Mark and trace events in your log alike isotopic labeling.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

isotopic-logging

Latest PyPI package Downloands of latest PyPI package Supported versions of Python license

Build status of the master branch on Unix Build status of the master branch on Windows Test coverage

Code issues Code Climate Codacy Code Review Scrutinizer Code Quality Code Health Requirements Status

Table of contents

Synopsis

isotopic-logging is a little Python library which is designed to help you to track separate operations and their parts within whole execution flow. This is done by injecting operation prefixes at the beginning of log messages.

This library was born in depths of real projects which have web applications and background task queues, each of which can have multiple workers. There are two key points this library resolves:

  • As administrator I want to have log entries marked with same prefix within single operation so that I can distinguish and track operations even if log is written from multiple threads or sources.
  • As developer I want to store prefix in some context so that I do not need to format it per each call to logger and so that I can access it within nested function calls without passing prefix to a function directly and screwing up its semantics.

isotopic-logging comes very useful when you have a single log stream, which is populated from parallel sources (threads or processes), and you need to detect flow of a single operation in a mess of interweaving log messages and to distinguish different instances of the same operation.

The library can be useful for single-process and single-thread applications as well. You may still need to detect operations and track their execution time and you can do it well.

Quick output example

For example, log of a single complex operation may look like this:

INFO     [2015-12-15 21:45:04,339] D6EF95 | Heavy task has started.
DEBUG    [2015-12-15 21:46:36,148] D6EF95 | Checking user permissions.
INFO     [2015-12-15 21:46:36,654] D6EF95 | Analysis | Analysis phase has started.
DEBUG    [2015-12-15 21:46:41,756] D6EF95 | Analysis | Analysing current state of devices.
DEBUG    [2015-12-15 21:46:42,959] D6EF95 | Analysis | Analysing new state of devices.
DEBUG    [2015-12-15 21:46:47,565] D6EF95 | Analysis | Analysing changes.
INFO     [2015-12-15 21:46:51,871] D6EF95 | Analysis | Analysis phase has finished.
INFO     [2015-12-15 21:46:54,073] D6EF95 | Pushing data to central storage.
INFO     [2015-12-15 21:46:55,278] D6EF95 | Communication | Communication phase has started.
DEBUG    [2015-12-15 21:46:58,884] D6EF95 | Communication | Spreading out parallel subtasks for every involved device.
DEBUG    [2015-12-15 21:47:02,089] D6EF95 | Communication | 478272 | Connecting to device #3.
DEBUG    [2015-12-15 21:47:03,493] D6EF95 | Communication | 28B208 | Connecting to device #1.
INFO     [2015-12-15 21:47:04,798] D6EF95 | Communication | 28B208 | Running job at device #1.
DEBUG    [2015-12-15 21:47:10,501] D6EF95 | Communication | AE2677 | Connecting to device #2.
INFO     [2015-12-15 21:47:12,501] D6EF95 | Communication | AE2677 | Running job at device #2.
INFO     [2015-12-15 21:47:17,707] D6EF95 | Communication | 478272 | Running job at device #3.
INFO     [2015-12-15 21:47:21,709] D6EF95 | Communication | Communication phase has finished.
DEBUG    [2015-12-15 21:47:24,412] D6EF95 | Commiting changes.
INFO     [2015-12-15 21:47:27,013] D6EF95 | Heavy task has finished, elapsed time: 00:23:11.004120.

Important thing to note: each line of the example log from above may be produced by different functions running in different threads or processes. And they do not need to remember and pass logging prefixes from one to another which keeps you focused on development process and prevents you from distracting by log message formatting.

Installation

To install the library simply get it at Cheese Shop (PyPI):

Key concepts

Work of this library is based on several key concepts:

  • Prefix injectors: they store or/and generate prefixes and inject them into strings.
  • Injection contexts: they manage injectors (get or create them) and track scope execution time.
  • Injection scopes: they drive creation of injectors and bound operation execution time.
  • Logger wrapper: culmination of other concepts. Wraps loggers and provides methods for creation of injection contexts.

These concepts may be used separately or as a whole combination in form of logger wrapper. Such approach is useful for flexible customization.

Prefix injectors

Prefix injectors are objects which store or/and generate prefixes accessed by prefix attribute and which are injected into target strings using mark() method.

Default injectors are defined in isotopic_logging.injectors module and they are described below.

Direct prefix injector

DirectPrefixInjector will inject into strings exactly given prefix:

All other injectors are subclasses of DirectPrefixInjector and usually you will not need to use it directly. Exception is only the case when you need to transmit prefix between processes or threads.

Static prefix injector

StaticPrefixInjector automatically inserts delimiter between prefix and target strings:

Default delimiter is defined as isotopic_logging.defaults.DELIMITER as its value is " | " (space-pipe-space).

You can set custom delimiter:

Autoprefix injector

AutoprefixInjector works like StaticPrefixInjector, but it generates prefixes by itself.

Generally it is used to distinguish different instances of same operations or different calls to same methods and so on.

Here you can see that 2 different injectors have 2 different prefixes.

Default prefixes are generated by threadsafe generator isotopic_logging.generators.default_oid_generator which uses uuid.uuid4 to produce results.

Given default prefix lenght of 6 symbols, default generator guarantees that 99% of generated prefixes will be unique in case of 500 serial calls from 100 parallel threads. It is considered to be enough to distinguish operations which are placed in time close to each other.

You can use custom generator:

If you are sure you need custom generator, you must ensure that it's threadsafe. You can use isotopic_logging.concurrency.threadsafe_iter for this:

threadsafe_iter is needed for generators which are implemented in pure Python. For examle, in CPython itertools.cycle has native implementation and it's threadsafe out of the box. Moreover, looks like Python 3 makes your generators threadsafe as well, so it's quite possible that you will need threadsafe_iter only for Python 2.

AutoprefixInjector also supports custom delimiters:

Hybrid prefix injector

HybridPrefixInjector combines both features of AutoprefixInjector and StaticPrefixInjector: it creates prefixes which consist of generated part followed by static part which are separated by default or custom delimiter.

This prefix injector also supports custom delimiter and generator:

Injection contexts

Injection contexts are used for scope management. Scopes are described in the next section.

Contexts are responsible for providing you with proper injectors. Injectors are created on demand. Generally, this can be described as:

  • "Give me current injector or create new specific one if there is no current injector"
  • or "Create new injector inherited from current one despite anything".

Contexts orginize injectors into stacks. Stacks are thread-local and do not interfere with each other. There is no limit for stack size. This should not be a problem, because injectors are created lazily. This happens only if stack is empty or if you explicitly want to inherit current prefix (usually to distinguish suboperation).

Current injector is the injector on top of the stack in current thread.

Injection context managers are defined in isotopic_logging.context module. There is a proper context manager for each type of prefix injector. Context managers accept accept same arguments as injectors which they are going to produce.

Examples:

Injection scopes

Scopes are created by contexts and they are used to drive creation of injectors. There are two kinds of scopes: top-level and nested. Nested scopes allow inheritance of prefixes.

Let's look at examples to grab the idea.

Nested scopes

Here we separate helper and operation functions. Both of them define own scopes via context managers.

If helper is called directly, it's scope will be top-level and new injector will be created for each call:

If helper will be called from operation, it's scope will become nested and it will reuse injector created within top-level scope:

In this case inj in operation and inj in helper will be exactly the same object.

Inherited scopes

Nested scopes are good if they are used within reusable helpers, utils, etc., especially if they are small. If nested calls present some complex operations, you may want to separate them with own prefixes, but preserve parent prefix.

You can inherit current prefix to do so:

Here, suboperation uses static_injector with flag inherit=True. This creates new injector, which is a combination of parent prefix and given static prefix. suboperation also calls helper which creates nested injection scope, as in the previous example.

So, as you can see, one of the main benefits of the library is prefix transmission between separated functions. In couple with prefix management, this keeps API of your functions and their bodies clean, saves your time and mental focus.

Logger wrapper

isotopic_logging allows you to wrap your loggers to prevent you from typing inj.mark() every time you put some message to log. This saves space for code and makes it more readable.

Wrapping is done via isotopic_logging.IsotopicLogger logger wrapper. It wraps loggers which are instances of logging.Logger and its subclasses.

Wrapper provides methods for creation of logger proxies with predefined prefix injectors:

  • direct() for DirectPrefixInjector;
  • static() for StaticPrefixInjector;
  • auto() for AutoprefixInjector;
  • hybrid() for HybridPrefixInjector.

These methods accept same parameters as proper injection context managers. They return contex managers for getting logger proxies. Proxies act as usual loggers and they wrap logging calls with specific prefix.

Example:

Here, LOG.auto() produces context which creates logger proxy with injected autoprefix.

Time tracking

Prefix injectors allow you to track execution time within scopes. They provide:

  • elapsed_time attribute, which counts elapsed_time in seconds;
  • format_elapsed_time() method, which can accept custom format to output elapsed time as a string.

Examples:

Nested and inherited scopes have own internal time tracking:

Default formatting outputs hours, minutes, seconds and microseconds:

You can use custom format compatible with format of datetime.datetime.strftime():

Interthread prefix transmission

Sometimes you may need to pass operation prefix between threads or processes. For example, you start operation by handling HTTP request and continue it in a background worker.

This can be easily made by using injector's prefix attribute and DirectPrefixInjector:

Changelog

  • 2.0.0 (Dec 31, 2015)
    • Feature: support inherited prefixes (issue #1).
    • Feature: simple and clean way to inject prefixes into calls to existing loggers (issue #4).
    • Feature: ability to get context execution time (issue #3).
    • Optimization: instances of injectors will be created only if new scope is defined (issue #5).
    • Improvement: ensure prefix and target message are converted to strings during concatenation.
    • Renamings:

      • prefix_injector to static_injector;
      • autoprefix_injector to auto_injector;

      Old names are preserved and still can be used.

    • Reduction: remove optional container parameter from everywhere.
  • 1.0.1 (Jul 30, 2015)
    • Fix: threading support for default_oid_generator which is used by default by autoprefix_injector and hybrid_injector (issue #2).
  • 1.0.0 (May 3, 2015)

    Initial version

About

Mark and trace events in your log alike isotopic labeling.

License:GNU Lesser General Public License v3.0


Languages

Language:Python 92.5%Language:Gherkin 7.5%