Metrics
Key takeaways
Performance metrics | Values that result from the measurement of an application's non-functional traits, which can provide useful insight into how an application performs. |
ICounters | Interface that defines methods for performance counters that measure execution metrics. |
CachedCounters | Performance counters that store their measurements in memory. |
LogCounters | Performance counters that periodically send their measurements to a logger |
NullCounters | Component that is used to create dummy performance counters. |
Performance metrics tools | Tools used to analyze performance metrics. Examples are Prometheus and Datadog. |
CompositeCounters | Component that is used to group counters from different components into a single one. |
Introduction
This tutorial will teach you how to create and manage performance metrics using components from the Pip.Services toolkit. First, we will learn how counters are defined in the toolkit and how to add them to a component. Then, we will see several options of managing the obtained performance metrics, such as storing them in memory, showing them in the console, sending them to external tools, and grouping them into composite counters. We will also examine a dummy component that can be used to simulate counters.
Counters
Pip.Services’ Components module includes a package called Count, which contains several interfaces and classes that can be used to create performance counters.
Among these interfaces is one called ICounters, which defines methods for measuring execution performance. One such method is increment(), which increases by a defined value each time it is called. Other important methods are beginTiming() and stats(), which are used to calculate time intervals and common statistics (minimum, maximum, and average), respectively. There is no stopTiming() method in this interface due to the fact that beginTiming() is supposed to return a CounterTiming object, which can be called to end the measurement and update the counter.
The ICounters interface is implemented by several classes. The figure below shows a simplified class diagram, displaying the relationships between this interface and the main classes used to build counters. These classes will be explained in more detail in the following sections.
Pip.Services has several predefined counters, specified in the CounterType class. They are:
A best practice when working with counters is to name them using to the following convention:
<service_or_component_name>.<method_name>.<counter_name>
Managing counters
In the next sections, we will construct examples that show how to define counters that will store their metrics in memory, in a logger, or in monitoring tools. We will also learn to create a dummy component that performs no real measurements and a composite counter component, capable of grouping multiple different counters into one.
Monitored component
First, we define a class that has two performance metrics: the number of times a method is called and its execution time. Both metrics will be performing measurements for a dummy function that simulates a task by printing two messages to the console. Finally, we call the dump() method to save the obtained values. The code below shows what this class looks like:
import { ICounters } from 'pip-services4-observability-node';
export class MyComponent {
private _consoleLog: boolean = true;
private counters: ICounters;
public constructor(counters: ICounters) {
this.counters = counters;
if (this._consoleLog) {
console.log("MyComponent has been created.");
}
}
public mymethod(): void {
this.counters.increment("mycomponent.mymethod.calls", 1);
let timing = this.counters.beginTiming("mycomponent.mymethod.exec_time");
try {
if (this._consoleLog) {
console.log("Hola amigo");
console.log("Bonjour mon ami");
}
} finally {
timing.endTiming();
}
}
}
import (
"context"
"fmt"
ccount "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/count"
)
type MyComponent struct {
_consoleLog bool
_counters ccount.ICounters
}
func NewMyComponentA(counters ccount.ICounters) *MyComponent {
c := &MyComponent{}
c._counters = counters
if c._consoleLog {
fmt.Println("MyComponent has been created.")
}
return c
}
func (c *MyComponent) Mymethod() {
c._counters.Increment(context.Background(), "mycomponent.mymethod.calls", 1)
timing := c._counters.BeginTiming(context.Background(), "mycomponent.mymethod.exec_time")
defer timing.EndTiming(context.Background())
if c._consoleLog {
fmt.Println("Hola amigo")
fmt.Println("Bonjour mon ami")
}
}
from pip_services4_observability.count import ICounters
class MyComponent:
_console_log = True
def __init__(self, counters: ICounters):
self.counters = counters
if self._console_log:
print("MyComponent has been created.")
def mymethod(self):
self.counters.increment("mycomponent.mymethod.calls", 1)
timing = self.counters.begin_timing("mycomponent.mymethod.exec_time")
try:
if self._console_log:
print("Hola amigo")
print("Bonjour mon ami")
finally:
timing.end_timing()
Counters
Once we have defined the performance metrics in our class, we need to make the obtained values available. This can be done in several ways, such as storing them in memory for later use, showing them in the console, or using an external tool like Prometheus or Datadog for metric analysis.
The following subsections aim to explain how different components can be used to achieve various results. First, we will discuss CachedCounters, which store performance metrics in memory. Next, we will cover LogCounters, which send metrics to a logger. After that, we will introduce NullCounters, a dummy component often used for testing and modeling purposes. We will also briefly mention a few components capable of sending metrics to external tools, and then wrap up the tutorial with CompositeCounters, a component that groups counters measuring the same metrics but used differently into a single counter.
CachedCounters
The CachedCounters class is used to create performance counters and store their values in memory. This is an abstract class that is generally used to implement other counters, such as LogCounters, PrometheusCounters and DatadogCounters.
An important method declared in this class is save(), which, as the name suggests, saves the current counters’ measurements. This method is abstract and therefore needs to be implemented by all subclasses. Another notable method of this class is dump(), which saves metrics data at certain time intervals.
In the example below, we use the previously defined component with CachedCounters. For this, we create a subclass of CachedCounters with a version of the save() method that simply prints a message. When developing real microservices, it is within this method that we can define what we want to do with our performance metrics.
Then, after passing an instance of our CachedCounters’ subclass (i.e. MyCachedCounters) to our component, we call the component’s myMethod(), get the counters, and print the results. The final code is:
import { CachedCounters, Counter, ICounters } from 'pip-services4-observability-node';
export async function main() {
let countersCached = new MyCachedCounters();
let mycomponentCached = new MyComponent(countersCached);
let countExec = 2;
for (let i = 0; i < countExec; i++)
mycomponentCached.mymethod();
let resultCached = countersCached.getAll();
console.log("Metrics");
for(let res of resultCached) {
console.log("Count: " + res.count);
console.log("Min: " + res.min);
console.log("Max: " + res.max);
console.log("Average: " + res.average);
console.log("Time: " + res.time);
console.log("Name: " + res.name);
console.log("Type: " + res.type);
console.log("-----------------");
}
}
export class MyCachedCounters extends MyCachedCounters {
protected save(counters: ICounters[]): void {
console.log("Saving " + counters[0].name + " and " + counters[1].name);
}
}
import (
"context"
"fmt"
ccount "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/count"
)
func main() {
countersCached := NewMyCachedCounters()
mycomponentCached := NewMyComponentA(countersCached)
countExec := 2
for i := 0; i < countExec; i++ {
mycomponentCached.Mymethod()
}
resultCached := countersCached.GetAll()
fmt.Println("Metrics")
for _, res := range resultCached {
fmt.Printf("Count: %d\n", res.Count())
fmt.Printf("Min: %02f\n", res.Min())
fmt.Printf("Max: %02f\n", res.Max())
fmt.Printf("Average: %02f\n", res.Average())
fmt.Printf("Time: %s\n", res.Time())
fmt.Printf("Name: %s\n", res.Name())
fmt.Printf("Type: %d\n", res.Type())
fmt.Printf("-----------------")
}
}
type MyCachedCounters struct {
*ccount.CachedCounters
}
func NewMyCachedCounters() *MyCachedCounters {
c := &MyCachedCounters{}
c.CachedCounters = ccount.InheritCacheCounters(c)
return c
}
func (c *MyCachedCounters) Save(ctx context.Context, counters []ccount.Counter) error {
fmt.Println("Saving " + counters[0].Name + " and " + counters[1].Name)
return nil
}
from pip_services4_observability.count import CachedCounters
class MyCachedCounters (CachedCounters):
def _save(self, counters):
print("Saving " + counters[0].name + " and " + counters[1].name)
countersCached = MyCachedCounters()
mycomponentCached = MyComponent(countersCached)
count_exec = 2
for i in range(count_exec):
mycomponentCached.mymethod()
resultCached = countersCached.get_all()
print("Metrics")
for res in resultCached:
print("Count: " + str(res.count))
print("Min: " + str(res.min))
print("Max: " + str(res.max))
print("Average: " + str(res.average))
print("Time: " + str(res.time))
print("Name: " + res.name)
print("Type: " + str(res.type))
print("-----------------")
Which, after running, produces the following output:
As we can see, the save() method was called automatically. Since the Increment counter was only counting the number of times MyMethod was called, it returns the invocation count, but does not supply any statistics data (i.e. minimum, maximum, average). On the other hand, the Interval counter, which measures the execution time, provides these statistics.
LogCounters
Counters can also be made to output their metrics to the console. This can be done with the LogCounters class, which can be used to create performance counters that periodically dump the obtained measurements to a logger.
Containers use LogCounters by default. Once we create a container, the container’s factory will create a LogCounters component, which uses the ConsoleLogger component. Thus, all component metrics measured by the counter will be outputted to the console.
In our next example, we use the LogCounters class, referencing the ConsoleLogger component as a dependency. Then, we call myMethod() and analyze the results as we did in the previous example. The following code shows how to do this:
import { Descriptor, References } from "pip-services4-components-node";
import { ConsoleLogger, LogCounters, ICounters } from 'pip-services4-observability-node';
export async function main() {
let counters = new LogCounters();
counters.setReferences(References.fromTuples(
new Descriptor("pip-services", "logger", "console", "default", "1.0"), new ConsoleLogger()))
let mycomponentLog = new MyComponent(counters);
let countExec = 2;
for (let i = 0; i < countExec; i++) {
mycomponentLog.mymethod();
}
let resultLog = counters.getAll();
console.log("Metrics");
for (let res of resultLog) {
console.log("Count: " + res.count);
console.log("Min: " + res.min);
console.log("Max: " + res.max);
console.log("Average: " + res.average);
console.log("Time: " + res.time);
console.log("Name: " + res.name);
console.log("Type: " + res.type);
console.log("-----------------");
}
}
import (
"context"
"fmt"
cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
ccount "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/count"
clog "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/log"
)
func main() {
counters := ccount.NewLogCounters()
counters.SetReferences(context.Background(), cref.NewReferencesFromTuples(context.Background(),
cref.NewDescriptor("pip-services", "logger", "console", "default", "1.0"),
clog.NewConsoleLogger(),
))
mycomponentLog := NewMyComponentA(counters)
countExec := 2
for i := 0; i < countExec; i++ {
mycomponentLog.Mymethod()
}
resultLog := counters.GetAll()
fmt.Println("Metrics")
for _, res := range resultLog {
fmt.Printf("Count: %d\n", res.Count())
fmt.Printf("Min: %02f\n", res.Min())
fmt.Printf("Max: %02f\n", res.Max())
fmt.Printf("Average: %02f\n", res.Average())
fmt.Printf("Time: %s\n", res.Time())
fmt.Printf("Name: %s\n", res.Name())
fmt.Printf("Type: %d\n", res.Type())
fmt.Printf("-----------------")
}
}
from pip_services4_observability.count import LogCounters
from pip_services4_components.refer import References, Descriptor
from pip_services4_observability.log import ConsoleLogger
counters = LogCounters()
counters.set_references(References.from_tuples(
Descriptor("pip-services", "logger", "console", "default", "1.0"), ConsoleLogger()))
mycomponentLog = MyComponent(counters)
count_exec = 2
for i in range(count_exec):
mycomponentLog.mymethod()
resultLog = counters.get_all()
print("Metrics")
for res in resultLog:
print("Count: " + str(res.count))
print("Min: " + str(res.min))
print("Max: " + str(res.max))
print("Average: " + str(res.average))
print("Time: " + str(res.time))
print("Name: " + res.name)
print("Type: " + str(res.type))
print("-----------------")
Which, after running, produces the following results:
As we can see, the main difference from the previous example is the instantaneous and automatic printing of the counters’ measurements to the console. The remaining messages remained mostly unchanged.
NullCounters
If we are testing our application or want to create a prototype, we can use NullCounters, which is a dummy component and produces no real measurements. In this manner, we can simulate the existence of a counters component, without having to actually deal with one.
In this case, we need to delete the call to the counters’ dump() method from our component, as there will be no values to save. The resulting code should look like this:
import { NullCounters, ICounters } from 'pip-services4-observability-node';
export async function main() {
let countersNull = new NullCounters();
let mycomponentNull = new MyComponent(countersNull);
let countExec = 2;
for (let i = 0; i < countExec; i++)
mycomponentNull.mymethod();
}
export class MyComponent {
private _consoleLog: boolean = true;
private counters: ICounters;
public constructor(counters: ICounters) {
this.counters = counters;
if (this._consoleLog) {
console.log("MyComponent has been created.");
}
}
public mymethod(): void {
this.counters.increment("mycomponent.mymethod.calls", 1);
let timing = this.counters.beginTiming("mycomponent.mymethod.exec_time");
try {
if (this._consoleLog) {
console.log("Hola amigo");
console.log("Bonjour mon ami");
}
} finally {
timing.endTiming();
}
}
}
import (
"context"
"fmt"
ccount "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/count"
)
func main() {
countersNull := ccount.NewNullCounters()
mycomponentNull := NewMyComponent(countersNull)
countExec := 2
for i := 0; i < countExec; i++ {
mycomponentNull.Mymethod(context.Background())
}
}
type MyComponent struct {
_consoleLog bool
_counters ccount.ICounters
}
func NewMyComponent(counters ccount.ICounters) *MyComponent {
c := &MyComponent{}
c._counters = counters
if c._consoleLog {
fmt.Println("MyComponent has been created.")
}
return c
}
func (c *MyComponent) Mymethod(ctx context.Context) {
c._counters.Increment(context.Background(), "mycomponent.mymethod.calls", 1)
timing := c._counters.BeginTiming(context.Background(), "mycomponent.mymethod.exec_time")
defer timing.EndTiming(ctx)
if c._consoleLog {
fmt.Println("Hola amigo")
fmt.Println("Bonjour mon ami")
}
}
from pip_services4_observability.count import ICounters
from pip_services4_observability.count import NullCounters
class MyComponent:
_console_log = True
def __init__(self, counters: ICounters):
self.counters = counters
if self._console_log:
print("MyComponent has been created.")
def myMethod(self):
self.counters.increment("mycomponent.mymethod.calls", 1)
timing = self.counters.begin_timing("mycomponent.mymethod.exec_time")
try:
if self._console_log:
print("Hola amigo")
print("Bonjour mon ami")
finally:
timing.end_timing()
countersNull = NullCounters()
mycomponentNull = MyComponent(countersNull)
count_exec = 2
for i in range(count_exec):
mycomponentNull.myMethod()
Which, after running, produces the following outcome:
Other counters
Performance metrics can also be sent to monitoring tools, which specialize in processing and displaying metrics data. Pip.Services provides counters that can be used with some of the most popular monitoring tools, such as Prometheus and Datadog.
CompositeCounters
Pip.Services also offers the CompositeCounters component, which can be used to group counters that need to collect the same performance metrics, but use them differently. Once collected, the metrics can be sent to different destinations, such as a console, a logger, and/or a monitoring tool.
An important method declared in this class is setReferences(), which registers all components that will receive the measurements. When using a container, this method finds all registered counters and connects to them.
In the example below, we have a monitored class similar to the one from the previous section, except for the fact that it utilizes a CompositeCounters component and has a setReferences() method added to it. We can use this component to store metrics in, for example, a CachedLogger and a Prometheus component.
To achieve this, we define two counters, a LogCounters component that stores values in a CachedLogger and PrometheusCounters component that connects to Prometheus. Then, we instantiate our monitored class and add references to both counters.
After running myMethod(), we can print the performance metrics obtained by both counters, just like we were doing in the previous example.
The following code shows how this can be done:
import { ConfigParams, Descriptor, IReferenceable, IReferences, References } from "pip-services4-components-node";
import { CompositeCounters, LogCounters, Counter, LogMessage } from 'pip-services4-observability-node';
import { PrometheusCounters } from 'pip-services4-prometheus-node';
export async function main() {
let countersLog = new LogCounters();
let countersProm = new PrometheusCounters()
countersProm.configure(ConfigParams.fromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 8080
));
let myComponent = new MyComponent();
myComponent.setReferences(References.fromTuples(
new Descriptor("pip-services", "counters", "logger", "default3", "1.0"), countersLog,
new Descriptor("pip-services", "counters", "prometheus", "default4", "1.0"), countersProm,
new Descriptor("pip-services", "logger", "cached", "default2", "1.0"), new MyCachedLogger()
));
await countersProm.open(null);
let countExec = 2;
for (let i = 0; i < countExec; i++)
myComponent.mymethod();
let results = countersLog.getAll();
console.log("Metrics to logger")
printResults(results);
results = countersProm.getAll();
console.log("Metrics to Prometheus");
printResults(results);
}
function printResults(results: Counter[]) {
for (let result of results) {
console.log("Count: " + result.count);
console.log("Min: " + result.min);
console.log("Max: " + result.max);
console.log("Average: " + result.average);
console.log("Time: " + result.time);
console.log("Name: " + result.name);
console.log("Type: " + result.type);
console.log("-----------------");
}
}
export class MyComponent implements IReferenceable {
private _consoleLog: boolean = true;
private counters: CompositeCounters = new CompositeCounters();
public constructor() {
if (this._consoleLog) {
console.log("MyComponent has been created.");
}
}
public setReferences(references: IReferences): void {
this.counters.setReferences(references);
}
public mymethod(): void {
this.counters.increment("mycomponent.mymethod.calls", 1);
let timing = this.counters.beginTiming("mycomponent.mymethod.exec_time");
try {
if (this._consoleLog) {
console.log("Hola amigo");
console.log("Bonjour mon ami");
}
} finally {
timing.endTiming();
}
}
}
export class MyCachedLogger extends MyCachedLogger {
protected async save(messages: LogMessage[]): Promise<void> {
console.log("Saving somewhere");
}
}
import (
"context"
"fmt"
cconf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
ccount "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/count"
clog "github.com/pip-services4/pip-services4-go/pip-services4-observability-go/log"
pcount "github.com/pip-services4/pip-services4-go/pip-services4-prometheus-go/count"
)
func main() {
countersLog := ccount.NewLogCounters()
countersProm := pcount.NewPrometheusCounters()
countersProm.Configure(context.Background(), cconf.NewConfigParamsFromTuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 8080,
))
myComponent := NewMyComponent()
myComponent.SetReferences(context.Background(), cref.NewReferencesFromTuples(context.Background(),
cref.NewDescriptor("pip-services", "counters", "logger", "default3", "1.0"), countersLog,
cref.NewDescriptor("pip-services", "counters", "prometheus", "default4", "1.0"), countersProm,
cref.NewDescriptor("pip-services", "logger", "cached", "default2", "1.0"), NewMyCachedLogger(),
))
err := countersProm.Open(context.Background())
if err != nil {
panic(err)
}
countExec := 2
for i := 0; i < countExec; i++ {
myComponent.Mymethod(context.Background())
}
results := countersLog.GetAll()
counters := make([]ccount.Counter, 0)
for _, val := range results {
counters = append(counters, val.GetCounter())
}
fmt.Println("Metrics to logger")
PrintResults(counters)
results = countersProm.GetAll()
for _, val := range results {
counters = append(counters, val.GetCounter())
}
fmt.Println("Metrics to Prometheus")
PrintResults(counters)
}
func PrintResults(results []ccount.Counter) {
for _, res := range results {
fmt.Printf("Count: %d\n", res.Count)
fmt.Printf("Min: %02f\n", res.Min)
fmt.Printf("Max: %02f\n", res.Max)
fmt.Printf("Average: %02f\n", res.Average)
fmt.Printf("Time: %s\n", res.Time)
fmt.Printf("Name: %s\n", res.Name)
fmt.Printf("Type: %d\n", res.Type)
fmt.Printf("-----------------")
}
}
type MyComponent struct {
_consoleLog bool
counters *ccount.CompositeCounters
}
func NewMyComponent() *MyComponent {
c := &MyComponent{
counters: ccount.NewCompositeCounters(),
}
if c._consoleLog {
fmt.Println("MyComponent has been created.")
}
return c
}
func (c *MyComponent) SetReferences(ctx context.Context, references cref.IReferences) {
c.counters.SetReferences(ctx, references)
}
func (c *MyComponent) Mymethod(ctx context.Context) {
c.counters.Increment(context.Background(), "mycomponent.mymethod.calls", 1)
timing := c.counters.BeginTiming(context.Background(), "mycomponent.mymethod.exec_time")
defer timing.EndTiming(ctx)
if c._consoleLog {
fmt.Println("Hola amigo")
fmt.Println("Bonjour mon ami")
}
}
type MyCachedLogger struct {
*clog.CachedLogger
}
func NewMyCachedLogger() *MyCachedLogger {
c := &MyCachedLogger{}
c.CachedLogger = clog.InheritCachedLogger(c)
return c
}
func (c *MyCachedLogger) Save(ctx context.Context, messages []clog.LogMessage) error {
fmt.Println("Saving somewhere")
return nil
}
from pip_services4_observability.count import ICounters, CompositeCounters
from pip_services4_components.refer import IReferenceable, IReferences
_console_log = True
class MyComponent(IReferenceable):
_counters: CompositeCounters = CompositeCounters()
def __init__(self):
self._counters = counters
if _console_log:
print("MyComponent has been created.")
def setReferences(self, references: IReferences):
self._counters.set_references(references)
def myMethod(self):
self._counters.increment("mycomponent.mymethod.calls", 1)
timing = self._counters.begin_timing("mycomponent.mymethod.exec_time")
try:
if _console_log:
print("Hola amigo")
print("Bonjour mon ami")
finally:
timing.end_timing()
# Cached logger
class MyCachedLogger ():
def _save(self, counters):
print("\tSaving somewhere")
from pip_services4_components.refer import References, Descriptor
from pip_services4_observability.count import LogCounters
from pip_services4_observability.log import CachedLogger
countersLog1 = LogCounters()
countersLog1.set_references(References.from_tuples(
Descriptor("pip-services", "logger", "cached", "default2", "1.0"), MyCachedLogger()))
# Prometheus
from pip_services4_prometheus.count import PrometheusCounters
from pip_services4_components.config import ConfigParams
countersProm = PrometheusCounters()
countersProm.configure(ConfigParams.from_tuples(
"connection.protocol", "http",
"connection.host", "localhost",
"connection.port", 8080
))
countersProm.open("123")
# Composite counters
from pip_services4_observability.count import CompositeCounters
counters = CompositeCounters()
counters.set_references(References.from_tuples(
Descriptor("pip-services", "counters", "logger", "default3", "1.0"), countersLog1))
counters.set_references(References.from_tuples(
Descriptor("pip-services", "counters", "prometheus", "default4", "1.0"), countersProm))
myComponent = MyComponent()
count_exec = 2
for i in range(count_exec):
myComponent.myMethod()
result = countersLog1.get_all()
print("\nMetrics to logger")
for i in result:
print("Count: " + str(i.count))
print("Min: " + str(i.min))
print("Max: " + str(i.max))
print("Average: " + str(i.average))
print("Time: " + str(i.time))
print("Name: " + i.name)
print("Type: " + str(i.type))
print("-----------------")
result = countersProm.get_all()
print("\nMetrics to Prometheus")
for i in result:
print("Count: " + str(i.count))
print("Min: " + str(i.min))
print("Max: " + str(i.max))
print("Average: " + str(i.average))
print("Time: " + str(i.time))
print("Name: " + i.name)
print("Type: " + str(i.type))
print("-----------------")
Which, after running, produces the following output:
Wrapping up
In this tutorial, we have seen how to create, calculate, store and use performance metrics. We built an example where we created a call-counter and an execution-time counter, added these metrics to one of our component’s methods, and demonstrated how to save the obtained values to memory and a logger. We also learned how to create NullCounters, which is a dummy component that performs no real measurements, but is useful for testing and modeling purposes. Finally, we understood how to group several counters via the CompositeCounters class and send the measured values to other tools for future use.