Developer Guide¶
The Basics¶
Registry¶
All metrics are tracked by a measuro::Registry object. The registry’s main
purpose is to keep track of all metrics so that it can “render” them
(transform their data into another format, usually for output) altogether
when required by the application. The registry also provides the API for
applications to create metrics and look them up by their kind and name. The
creation, manipulation, rendering and lookup of metrics is thread-safe.
An application should only use a single measuro::Registry object. While
there’s nothing stopping an application from using multiple registries, this
is likely to lead to design complications and there are few - if any -
advantages.
Metric Kinds¶
Each metric is of a particular “kind”. Currently, Measuro supports the following metric kinds:
- Unsigned integer: An unsigned 64-bit integer.
- Signed integer: A signed 64-bit integer.
- Float: A floating-point number as defined by your compiler’s
floattype implementation. - Rate: The rate of change of another numeric metric, automatically calculated by Measuro. Its value is always expressed as a floating-point number.
- Sum: The sum of a set of other metrics, automatically calculated by Measuro.
- Bool: A
trueorfalsemetric. The two values can be associated with custom string representations for use when rendering. - String: A string value.
Note that all metrics whose values are expressed as floating-point numbers (float, rate and sum-of-float kinds) will always be rendered to 2 decimal places.
Creating Metrics¶
A metric is created using one of the overloaded create_metric() methods of
a measuro::Registry object. The first argument of create_metric()
always specifies the kind of metric to create. For example:
measuro::Registry reg;
// Create a signed 64-bit metric called "example_int64", initialised by
// default to 0
auto int_metric = reg.create_metric(measuro::INT::KIND, "example_int64",
"file(s)", "An example int metric");
// Create a float metric called "example_float", initialised by default to
// 0
auto float_metric = reg.create_metric(measuro::FLOAT::KIND, "example_float",
"hour(s)", "An example float metric");
Note
measuro::Registry objects require that all metrics they track have unique
names. A measuro::MetricNameError exception will be thrown by
create_metric() if you attempt to create a metric with a name that has
already been taken.
In addition to a name, each created metric can be given a string description and all numeric metrics can be given a unit. As well as providing useful context in rendered output, specifying meaningful descriptions and units helps to document your code.
For all metric kinds except rate and sum, you can optionally specify an initial value on creation:
// Create a signed 64-bit metric called "example_init" initialised to
// 1234
auto init_metric = reg.create_metric(measuro::INT::KIND, "example_init",
"file(s)", "An example initialised metric", 1234);
For boolean metric kinds, you can also optionally specify alternative string
representations for true and false values to be used when the metric
is rendered (the default string values are “TRUE” and “FALSE” respectively).
For example:
// Create a boolean metric called "example_bool" for which true maps to
// "yes" and false maps to "no"
auto bool_metric = reg.create_metric(measuro::BOOL::KIND, "example_bool",
"An example boolean metric", false, "yes", "no");
// Outputs false = no
std::cout << "false = " << std::string(*bool_metric) << "\n";
*bool_metric = true;
// Outputs true = yes
std::cout << "true = " << std::string(*bool_metric) << "\n";
Creating Sum and Rate Metrics¶
Sum and rate metrics are also created using
measuro::Registry::create_metric() method overloads. However, because the
values of these metrics are calculated automatically based on other metrics,
there are some additional things you need to consider.
Firstly, you must explicitly specify the kind of metric that any sum or rate metric you create will use to automatically calculate its value.
Sum metrics may use the following kinds of metrics to calculate their values:
- A numeric metric kind (unsigned integer, signed integer or float)
- A rate metric
Note
A sum metric can only calculate the sum of other metrics that are all of the same kind.
Rate metrics may use the following kinds of metrics to calculate their values:
- A numeric metric kind (unsigned integer, signed integer or float)
- A sum metric
That is to say:
- Sum metrics can’t be calculated using other sum metrics
- Rate metrics can’t be calculated using other rate metrics
- Neither sum nor rate metrics can be calculated using boolean or string metrics
For example:
/*
* Create 2 unsigned metrics representing counters from separate
* hypothetical threads. Then, create a sum metric which sums these two
* counters together and a rate metric which calculates the rate at which
* the sum of the counters changes.
*
*/
auto file_count_1 = reg.create_metric(measuro::UINT::KIND, "FileCount1",
"files", "Num files processed, thread 1");
auto file_count_2 = reg.create_metric(measuro::UINT::KIND, "FileCount2",
"files", "Num files processed, thread 2");
auto sum_of_uint = reg.create_metric(measuro::SUM::KIND,
measuro::UINT::KIND, "SumExample", "files", "Num files processed",
{file_count_1, file_count_2});
auto rate_of_sum_of_uint = reg.create_metric(measuro::RATE::KIND,
measuro::SUM::KIND, measuro::UINT::KIND, sum_of_uint,
"RateOfSumExample", "files/sec", "Num files processed per sec");
Manipulating Metrics¶
Before you can change a metric’s value, you need a way to refer to it. You do this using a metric handle. There are 2 ways to get a metric handle for a metric you want to manipulate:
- From the return value of the
create_metric()call used to create the metric. - By using the registry object to look up the metric by its kind and name.
For example:
measuro::Registry reg;
// Create a metric and store its handle in handle
auto handle = reg.create_metric(measuro::INT::KIND, "example_metric",
"file(s)", "An example metric");
// Find the metric's handle by specifying its kind and name. Note that if
// the metric can't be found, an exception of type measuro::MetricTypeError
// or measuro::MetricNameError will be thrown
auto found_handle = reg(measuro::INT::KIND, "example_metric");
Note
You can’t look up sum or rate metrics.
It’s more expensive to get metric handles by looking them up than simply by
keeping the return value from the create_metric() call. Particularly in
performance-sensitive applications, it’s recommended you avoid performing
lookups.
The metric handle type aliases for the metric kinds you can manipulate are
listed below. You can use these to declare variables for storing handles that
persist beyond the scope of the create_metric() call (e.g. as member
variables).
| Metric Kind | Handle type alias |
|---|---|
| Signed int | IntHandle |
| Unsigned int | UintHandle |
| Float | FloatHandle |
| Boolean | BoolHandle |
| String | StringHandle |
For example:
struct AppMetrics
{
AppMetrics()
{
file_count = registry.create_metric(measuro::UINT::KIND,
"file_count", "file(s)", "Number of files processed");
busy = registry.create_metric(measuro::BOOL::KIND,
"busy", "Is the app busy processing files?");
}
measuro::Registry registry;
measuro::UintHandle file_count;
measuro::BoolHandle busy;
};
Note that sum and rate metrics also have handle type aliases you can use, but
the exact alias depends on what kind of metric is being summed or having its
rate of change measured. Most of the time you won’t need to store handles to
these metrics as they can’t be manipulated directly. An exception is if you
want to be able to read the metric values from code. In this case, refer to
the in-code documentation for the create_metric() overload you’re using
to see the relevant handle type alias. Examples include RateOfUintHandle
and SumOfRateOfIntHandle.
Rendering Metrics¶
Rendering a metric means transforming the information held about it - including its value, name, description and kind - into another form, typically for output.
Metrics are rendered by passing a measuro::Renderer to the render()
method of the measuro::Registry object tracking the metrics. The registry
will pass each metric to the renderer for processing, ensuring that all metric
value calculations are up-to-date first.
You can also optionally specify a string prefix as an argument to
measuro::Registry::render(), causing the registry to only render metrics
whose names begin with the prefix. This can be useful when you use a metric
naming convention that groups metrics into program modules, for example
input.* for input-related metrics and output.* for output-related
metrics.
Measuro Renderers¶
Measuro comes with 2 renderers:
- PlainRenderer: Renders metrics as key-value pairs in a form designed to be
easily read by humans, as well as straightforward to parse using line-based
text processing tools such as
sed. - JsonRenderer: Renders metrics as a JSON dictionary, where each key in the dictionary is a string containing the name of a metric and the corresponding value a dictionary containing the metric information.
Both the PlainRenderer and JsonRenderer constructors expect a
std::ostream object to be passed to them. Their rendered data is written to
this stream before the call to measuro::Registry::render() returns.
Below is an example of code using the Measuro renderers and the output it produces.
measuro::Registry reg;
auto count_metric = reg.create_metric(measuro::INT::KIND, "example_count",
"item(s)", "An example count metric", 0);
auto str_metric = reg.create_metric(measuro::STR::KIND, "example_str",
"An example string metric");
*str_metric = "Example text";
for (auto i=0;i<100;++i)
{
++*count_metric;
}
measuro::PlainRenderer pl_renderer(std::cout);
measuro::JsonRenderer js_renderer(std::cout);
std::cout << "Plain text output:\n\n";
reg.render(pl_renderer);
std::cout << "----------\n\nJSON output:\n\n";
reg.render(js_renderer);
std::cout << std::endl; // The JSON renderer doesn't terminate with a newline!
Plain text output:
example_count = 100 item(s)
example_str = Example text
----------
JSON output:
{"example_count":{"value":100,"unit":"item(s)","kind":"INT","description":"An example count metric"},"example_str":{"value":"Example text","unit":"","kind":"STR","description":"An example string metric"}}
Custom Renderers¶
Creating your own renderer to customise how your application formats metrics is easy:
- Create a class that inherits from
measuro::Renderer. - Implement a constructor that allows clients of the class to pass it arguments that control where its output is to be written (e.g. a stream object or file name).
- At minimum, override the virtual
void render(const std::shared_ptr<Metric> & metric)method. This will be called once for each metric that is to be rendered. - If your renderer needs to perform setup steps before or tear down steps
after the registry makes all its calls to your
render()method, override the virtualvoid before()andvoid after()methods.
Your render() method will be called with the measuro::Metric object
which it is to render. It is guaranteed that all measuro::Metric objects
can be cast to std::string. Depending on the underlying metric kind (which
you can determine using measuro::Metric::kind()), you may also be able to
cast the metric to a std::uint64_t, std::int64_t, float or
bool.
Note
If you try and cast a measuro::Metric object to a type incompatible
with the underlying metric kind, a measuro::MetricCastError will be
thrown.
For example:
class ExampleCustomRenderer : public measuro::Renderer
{
public:
ExampleCustomRenderer(std::ostream & destination)
: m_destination(destination)
{
}
virtual void before() override final
{
m_destination << "--begin metrics--\n";
}
virtual void after() override final
{
m_destination << "--end metrics--" << std::endl;
}
virtual void render(const std::shared_ptr<measuro::Metric> & metric) override final
{
m_destination << metric->name() << '=' << std::string(*metric) << '\n';
}
private:
std::ostream & m_destination;
};
int main(int argc, char * argv[])
{
measuro::Registry reg;
auto count_metric = reg.create_metric(measuro::INT::KIND, "example_count",
"item(s)", "An example count metric", 1234);
auto str_metric = reg.create_metric(measuro::STR::KIND, "example_str",
"An example string metric", "str value");
ExampleCustomRenderer renderer(std::cout);
reg.render(renderer);
}
Which would produce the following output:
--begin metrics--
example_count=1234
example_str=str value
--end metrics--
Scheduled Rendering¶
It’s common for applications to need to render metrics on a regular basis, for example to stream monitoring information to external tools. Measuro makes this easy by allowing render operations to be scheduled to occur asynchronously at specified intervals.
To do this, call the measuro::Registry::register_schedule() method with the
renderer object you want used to perform the render and the interval in seconds
between renders.
For example:
measuro::Registry reg;
auto num_handle = reg.create_metric(measuro::UINT::KIND, "example_metric",
"units", "An example number metric");
measuro::PlainRenderer renderer(std::cout);
// Schedule the render to occur every second using the renderer object
reg.render_schedule(renderer, std::chrono::seconds(1));
// Cancel the scheduled render (this happens automatically when the registry
// object is destroyed)
reg.cancel_render_schedule();
Throttles¶
Updating metrics frequently can be costly. This is in part because a metric update requires at least an atomic operation, and in some cases the acquisition of a lock (depending on the metric kind).
Throttle objects help limit the performance impact of frequent updates. They do this by sitting in between the application and the metric object and limiting the rate at which the application’s attempts to change the metric’s value are successful.
Throttle limits can be expressed in terms both of the interval in seconds and the interval in attempted updates between metric changes.
Warning
measuro::Throttle objects are not thread-safe. Don’t use them from
multiple threads simultaneously.
For example:
measuro::Registry reg;
// Create a metric and store its handle in handle
auto handle = reg.create_metric(measuro::INT::KIND, "example_metric",
"file(s)", "An example metric", 10001);
// Create a throttle for use with the metric - limit updates to 1 per second and 1 per 1000 changes
auto example_throttle = reg.create_throttle(handle, std::chrono::milliseconds(1000), 1000);
do
{
// The metric will only be updated once per second and once per 1000 assignments
example_throttle = get_value();
}
while(true);
Performance Tips¶
The last thing you want is for the act of monitoring your application to cause it performance problems. In most cases, simple steps can be taken to avoid this:
- Avoid updating metrics in tight loops. Metric updates in tight loops can cause a relatively large proportion of CPU time to be spent in Measuro code. Instead, track changes in local variables from within the loop and assign the result to the metric from outside the loop.
- Keep metric handles rather than performing lookups. Looking metrics up
by their name is expensive, particularly from multi-threaded code. Instead,
keep the metric handles returned by the
measuro::Registry::create_metric()methods and use these to directly refer to metrics. - Create metrics on startup. Creating metrics locks the registry, so doing this from multiple threads in performance-critical code can be costly. Instead, create all the metrics on startup.
- When you know frequent metric updates are unavoidable, use a throttle. Throttle objects limit the rate at which metrics are updated.
- Build using multiple threads. Measuro’s use of templates can increase build time, so if supported you should configure your build system to use as many threads as possible.