Skip to main content

A toolkit for code observability in-process data collection: timing, counters, and metrics.

Project description

Apparent: a small library for observability

I created this library because in every job I've had I had to create some version of it, and I generally Needed something simpler than some of the giant tools and frameworks, which I can use on my desktop Without subscribing to some observability service.

This library is not intended to replace or compete with observability tools and dashboard, but is intended for day to day development use with a longer term objective of easy connectivity to observability Tools and frameworks. It is intended to remain small and compact.

Installation

python -m pip install apparent

Collect timing information

The most common use and the first to release as OSS is the timers functionality. There are many tools out there that do Broadly similar things. In particular, two categories of tools are similar in some ways:

  • Profilers (for example yappi)
  • Micro-benchmarking tools of various kinds

This library is neither of the above. The idea here is to have lightweight, possibly permanent instrumentation for timing measurements of functions or sections of code of particular interest, which you may or may not expose to some monitoring facility, or simply expose during your debugging. The key drivers for this are:

  • Very easy to add the instrumentation
  • Does not require any additional dependencies, and optional ones are very small too
  • Can produce reasonable reports easily, which should not require difficult interpretation
  • Can collect timing data over a large number of samples with little space or time overhead
  • Assumes deep familiarity of your own code base: as opposed to a profiler - where you may be working with someone else's code base and trying to discover some mystery bottleneck - the user of this library has more of an outside-in view where you have a pretty good idea upfront what you are interested in measuring (e.g. a specific computation, query, API endpoint).

Profilers are micro-benchmarking tools can be used commonly along with this for their own purposes.

Examples

Measuring the timing of a function with the @timed decorator:

from apparent.timing import timed

@timed
def f(x):
    ... 

Measuring the timing of a section of code with a registered timer.

from apparent.timing import TimerRegistry

while some_consition_applies():
    do_something()
    with TimerRegistry.get('expensive_section'):
        do_expensive_section()
        ...
    ...

If you do not want to use a registered timer you can just use a Timer directly. But then you have to hold on to the instance
to get any results out of it. For example:

from apparent.timing import Timer

timer = Timer('timer1')
for i in range(5):
    with timer:
        ...

result = timer.results().round(4).dict()

Getting measurements from a timer

To get a measurement from a timer you need an instance of a Timer that has been used to collect the data. Then call the results() method on it. This returns a TimerResults instance summarizing the state of the timer at the time of the call. You can refer to the source code for detail, but a broad outline (may change over time - the source code is more authoritative than the partial copy below) is:

@dataclass
class TimerResults:
    """Results from a timer: basic descriptive statistics (default in seconds).
    This class is generally produced by timers and not instantiated directly by library users"""
    total_time: float
    count: int
    mean: float
    stdevp: float
    min: float
    max: float
    timer_name: str
    units: Units = Units.SEC

    def convert(self, units: Units) -> 'TimerResults':
        """Convert the timer results to the given units"""
        ...  # code removed for clarity

    def dict(self, verbose: bool = True) -> dict:
        """Convert the timer results to a dictionary representation."""
        ...  # code removed for clarity

    def round(self, digits: int = 1) -> 'TimerResults':
        """Round the timer results to the given number of digits. Useful for presentation and for comparison."""
        ...  # code removed for clarity

In most cases you will be using primarily the @timed decorator and occasionally TimerRegistry.get(name). Both of these Result in named timers being registered in the timer registry and being retrievable by TimerRegistry.get(). Alternatively, all Timer names can be retrieved by TimerRegistry.names() and all registered instances can be retrieved via TimerRegistry.timers(). Using these you can produce a full listing of results of all timed code on demand.

The timer registry has some additional functionality, such as replacing the default instance with a custom registry, but those functionalities are beyond the scope of this document and will be discussed in a future document as the functionality matures.

Builtin reporting

The apparent.reports module contains basic reporting functionality. Initially, what you can do is to produce a summary table of all your timers using the timer_summary_table() function, which has options for the level of details, the units, the rounding, and the sorting. But default it would do the "reasonable thing": using milliseconds rounded to 3 digits, sorted descending by mean time. But you can change the results as you like.

The report is produced as a CSV-like table (list of lists of strings) that can readily be used by various libraries. Most of the time you would want to print it nicely to the stdout of your terminal, and for that the tabulate package is great. For example, you can do print(tabulate(report, headers='firstrow', tablefmt='rounded_outline'))

and you will get something like this:

╭──────────────────────────────┬─────────┬─────────┬─────────╮
│ timer_name                   │    mean │   count │     max │
├──────────────────────────────┼─────────┼─────────┼─────────┤
│ timer_reports_tests.slower() │ 102.325 │       2 │ 103.764 │
│ timer_reports_tests.slow()   │  53.787 │       2 │  53.787 │
│ timer_reports_tests.fast()   │   0.002 │       3 │   0.004 │
╰──────────────────────────────┴─────────┴─────────┴─────────╯

A direct dependency on tabulate was not added to the code base at the time of this writing.

Counters and metrics (todo)

TBD support for collecting ascending counters and metrics for exposing to observability frameworks.*

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

apparent-0.1.4.tar.gz (37.5 kB view hashes)

Uploaded Source

Built Distribution

apparent-0.1.4-py3-none-any.whl (12.7 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page