User guide🔗
This user guide can be used as a starting point for getting a deeper understanding of the inner workings of the multimeter library. It is meant for users who want to learn about individual details or who plan to extend its features by developing own probes or storages.
Install the library🔗
The library can be installed in two different ways:
Use stable release from PyPI🔗
All stable versions of multimeter are available on
PyPI
and can be downloaded and installed from there. The easiest option to get it installed
into your python environment is by using pip
:
pip install multimeter
Use from source🔗
Multimeter's Git repository is available for everyone and can easily be cloned into a new repository on your local machine:
$ cd /your/local/directory
$ git clone https://gitlab.com/kantai/multimeter.git
$ cd multimeter
If you want to make changes to library, please follow the guidance in the README.md on how to setup the necessary tools for testing your changes.
If you just want to use the library, it is sufficient to add the path to your local
multimeter repository to your $PYTHONPATH
variable, e.g.:
$ export PYTHONPATH="$PYTHONPATH:/your/local/directory/multimeter"
How multimeter works🔗
First we start with some high-level description of the individual parts of the library.
Multimeter🔗
Multimeter
is the central class which is
used by the user to start a measurement. A Multimeter takes the configuration, that
defines what and how it is measured. This configuration is usually given directly as
constructor arguments, when instantiating the object:
import multimeter
...
mm = multimeter.Multimeter(
multimeter.ResourceProbe(),
cycle_time=5.0,
storage=multimeter.DummyStorage(),
)
import multimeter
...
mm = multimeter.Multimeter()
...
mm.add_probes(multimeter.ResourceProbe())
mm.set_cycle_time(5.0)
mm.set_storage(multimeter.DummyStorage())
Probe🔗
For actually capturing the values, Multimeter
uses an arbitrary number of
Probe
objects, which are either provided as
positional arguments in the Multimeter
constructor or are added after construction
using add_probes
.
Each probe object can define describe values it captures. This is done using 3 different properties:
metrics🔗
metrics
contains a tuple of
Metric
objects, that describe different types of
values that are captured, e.g. the CPU rate spend executing user code or the memory
consumption. A metric can have additional attributes like the python type of the values,
a minimum or maximum value or the unit as string that the value is in. These values are
not checked or enforced in any way, but they can be useful for the interpretation of the
results by the user or other tools.
subjects🔗
subjects
contains a tuple of
Subject
objects that describe where a metric
can be captured, e.g. 'process' when capturing the memory usage of a process or a file
system where the free disk space is captured. The supported subjects are Probe
dependent, too.
measures🔗
The measures
attribute contains instances
of type Measure
, which references a single
metric and a single subject. The key
of a Measure
matches the key under which the
corresponding values are stored.
start()
and end()
🔗
Probes can implement two methods start()
and end()
, which are called when a new
measurement is started or finished. This allows the probe to set up and tear down some
mechanism for collecting the values. Both methods are optional to use and the default
implementations in the Probe
base class don't do anything.
Capturing values🔗
For actually capturing the values, Probe
subclasses need to implement a method
sample(values, time_span)
. This method is
given a dictionary values
where the captured values should be added under the key of
their corresponding Measure
, and a value time_span
which contains the number of
seconds as float
since the previous sample or since start()
in case of the first
call to sample()
.
The probes are expected to always set a value for each if its measures
.
Out of the box Multimeter contains the following Probe
objects:
Measurement🔗
A new Measurement
is created by
calling the
measure()
method on a Multimeter
object.
measurement = mm.measure()
measure()
takes optional keyword arguments, that
allows to identify individual measurements later on. If identifier
is provided, its
value is used as a (unique) identifier for this measurement:
measurement = mm.measure(identfieer='my-measurement')
Additionally, arbitrary keyword arguments with string values can be given. Those are treated as tags that can help to either differentiate between multiple measurements or contain additional user-defined data:
measurement = mm.measure(
identfieer='my-measurement',
my_tag='tag-value',
)
Measuring🔗
The measurement starts as soon as one calls
start()
. This starts a new thread
which runs in the background and gathers the measurement values at regular intervals
of length cycle_time
. This is done until the measurement is ended by calling
end()
.
measurement.start()
here_my_code_to_be_measured()
...
measurement.end()
The Result
. can be retrieved by explicitly
getting it from the measurement,
result = measurement.result
start()
, too.
result = measurement.start()
To make it more convenient the whole start()
, end()
sequence is
simplified, when using the Measurement
as a context manager:
with multimeter.measure() as measurement:
here_my_code_to_be_measured()
Adding marks🔗
To make it easier to relate the code that is being measured with the measured values,
a measurement allows adding marks programmatically using the method
add_mark(label)
. By calling
this method the current time is saved together with the provided label. This allows to
identify different code sections in a single measurement.
with multimeter.measure() as measurement:
here_my_code_to_be_measured()
measurement.add_mark("Next operation")
next_operation()
measurement.add_mark("final step")
final_step()
Result🔗
A Result
gives access to the measured values
together with a description of the metrics and the subjects that were captured.
points🔗
For each timestamp where values are gathered,
point
contain an individual object of type
Point
. Each point contains only two attributes:
datetime
: A pythondatetime.datetime
value with timezone UTC that contains the timestamp when the values of the point were measured.values
: Adict()
which contains the values for all measures at this time. The types of the individual values depend on thevalue_type
of the corresponding measure's metric.
metrics🔗
metrics
contains the union of all
Metric
objects defined by all probes that set
the values in this result.
subjects🔗
subjects
contain the union of all
Subject
objects defined by all probes that set
the values in this result.
measures🔗
The measures
attribute contains
the union of allMeasure
, objects defined by
all probes that set the values in this result. The key
of a Measure
matches the
key under which the corresponding values are stored.
meta_data🔗
All properties in the result are read-only. The only changes to the result by the user
can be made by adding meta data using
Result.add_meta_data(**meta_data)
.
The meta data values can be strings or other primitives. It is meant for storing
additional information about the run, that can be useful for interpreting the result,
e.g. instance type, operating system version, user account who executes the code etc.
Storage🔗
Once a measurement is finished, its result can be automatically stored by a
Storage
. Storage
classes need to
implement only a single method: store(result)
This method takes as only argument the Result
of the measurement.
Multimeter provides the following different Storage
implementations:
Extending multimeter🔗
Multimeter can easily be extended on two sides, gathering values and storing values.
Implementing custom probe🔗
A new probe should inherit from the Probe
base class.
The only method that needs to be implemented is sample(values, time_span
.
In order to make
it easier to understand, what different measures, subjects and metrics the new probe uses,
the corresponding methods measures
,
subjects
and
metrics
should be implemented and match, the
values, that sample(values, time_span)
defines. If applicable, some predefined metrics
METRIC_*
in multimeter.metrics
and subjects SUBJECT_*
in multimeter.subjects
can be
used.
start()
and end()
can be implemented if useful for initialization or cleaning up.
Implementing custom storage🔗
Implementing a custom storage class is quite easy. Inherit from the base class
multimeter.storages.base.Storage
and implement
the method store(result)
.