Package: apparent
Apparent: a small library for observabilityI 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.Installationpython -m pip install apparentCollect timing informationThe 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 exampleyappi)Micro-benchmarking tools of various kindsThis 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 instrumentationDoes not require any additional dependencies, and optional ones are very small tooCan produce reasonable reports easily, which should not require difficult interpretationCan collect timing data over a large number of samples with little space or time overheadAssumes 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.ExamplesMeasuring the timing of a function with the@timeddecorator:fromapparent.timingimporttimed@timeddeff(x):...Measuring the timing of a section of code with a registered timer.fromapparent.timingimportTimerRegistrywhilesome_consition_applies():do_something()withTimerRegistry.get('expensive_section'):do_expensive_section()......If you do not want to use a registered timer you can just use aTimerdirectly. But then you have to hold on to the instanceto get any results out of it. For example:fromapparent.timingimportTimertimer=Timer('timer1')foriinrange(5):withtimer:...result=timer.results().round(4).dict()Getting measurements from a timerTo get a measurement from a timer you need an instance of aTimerthat has been used to collect the data. Then call theresults()method on it. This returns aTimerResultsinstance 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:@dataclassclassTimerResults:"""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:floatcount:intmean:floatstdevp:floatmin:floatmax:floattimer_name:strunits:Units=Units.SECdefconvert(self,units:Units)->'TimerResults':"""Convert the timer results to the given units"""...# code removed for claritydefdict(self,verbose:bool=True)->dict:"""Convert the timer results to a dictionary representation."""...# code removed for claritydefround(self,digits:int=1)->'TimerResults':"""Round the timer results to the given number of digits. Useful for presentation and for comparison."""...# code removed for clarityIn most cases you will be using primarily the@timeddecorator and occasionallyTimerRegistry.get(name). Both of these
Result in named timers being registered in the timer registry and being retrievable byTimerRegistry.get(). Alternatively, all
Timer names can be retrieved byTimerRegistry.names()and all registered instances can be retrieved viaTimerRegistry.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 reportingTheapparent.reportsmodule contains basic reporting functionality. Initially, what you can do
is to produce a summary table of all your timers using thetimer_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 thetabulatepackage is great. For example, you can doprint(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)TBDsupport for collecting ascending counters and metrics for exposing to observability frameworks.*