Link Search Menu Expand Document

Add Metrics

Dec 7 2023 at 12:00 AM

  1. Overview
    1. General Set Up for Metrics in the Device
      1. Add MetricsFactory to the Constructor
      2. Call InitializeMetrics from Constructor
      3. Add Method to Configure Metrics
    2. Adding a Counter
      1. Create a Counter variable
      2. Add the Counter to the “Private Members” section of the Device
      3. Add functionality to relevant method
    3. Adding a Gauge
      1. Create a Gauge variable
      2. Add the Gauge to the “Private Members” section of the Device
      3. Add functionality to relevant method
    4. Adding a Rate
      1. Create a Rate variable
      2. Add the Rate to the “Private Members” section of the Device
      3. Add functionality to the relevant method
    5. Adding a Graph/Chart
      1. Counters and Gauges in the Graph
      2. Representing Rates in the Graph
    6. Metrics Generator - Sample code

Overview

Adding metrics is an essential part of writing a supportable Driver. Metrics provide insight into the status of the Driver. Users can configure, per Device in the Driver, metrics such as:

  • How many physical devices are actively communicating with the Driver.
  • How many physical devices are working/not working.
  • The health status of devices.
  • Other.

The following document will provide an introduction to configuring metrics in the Driver’s SDK. It will provide information about how to set up the following metrics:

  • Counter.
  • Gauge.
  • Rate.
  • How to represent metrics in graphs/charts.

General Set Up for Metrics in the Device

In line with the Quick Start Guide’s use case, this section will show how to set up metrics in the Receptor Device. The initial setup for metrics in the Device includes the following steps:

  1. Inject the Metrics Factory service into the Constructor.
  2. Call InitializeMetrics from the Constructor.
  3. Create a method (as the first executed method) in the Device to allow immediate access to the Device’s metrics on startup.

Add MetricsFactory to the Constructor

import DocsImage from “~/components/DocsImage”;

Constructor

/// <summary>
/// Initializes a new instance of the <see cref="DriverSdkDemo"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="deviceOptions">General CurrentDevice Options</param>
/// <param name="lockFactory">Factory for <see cref="IAsyncLockExecutor" /> instances</param>
/// <param name="deviceManager"><see cref="IDeviceManager"/></param>
/// <param name="deviceCommunicator"><see cref="IDeviceCommunicator" /></param>
/// <param name="transformManager"><see cref="ITransformManager"/></param>
/// <param name="devicePropertyFactory"><see cref="IDevicePropertyFactory" /></param>
/// <param name="serviceProvider"><see cref="IServiceProvider" /></param>
public DriverSdkDemo(ILogger<DriverSdkDemo> logger,
                                IOptionsMonitor<GeneralDeviceOptions> deviceOptions,
                                ITransientDisposableFactory<IAsyncLockExecutor> lockFactory,
                                IDeviceManager deviceManager,
                                IDeviceCommunicator deviceCommunicator,
                                ITransformManager transformManager,
                                IDevicePropertyFactory devicePropertyFactory,
                                IServiceProvider serviceProvider,
                                IMetricsFactory metricsFactory)
    : base(logger, deviceOptions, lockFactory, deviceManager,
        deviceCommunicator, transformManager, devicePropertyFactory, serviceProvider)
{
    _serviceProvider = serviceProvider;
}

In the Constructor section of the Device Interface, add IMetricsFactory metricsFactory, as shown in the code block above.

Call InitializeMetrics from Constructor

Constructor

/// <summary>
/// Initializes a new instance of the <see cref="DriverSdkDemo"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="deviceOptions">General CurrentDevice Options</param>
/// <param name="lockFactory">Factory for <see cref="IAsyncLockExecutor" /> instances</param>
/// <param name="deviceManager"><see cref="IDeviceManager"/></param>
/// <param name="deviceCommunicator"><see cref="IDeviceCommunicator" /></param>
/// <param name="transformManager"><see cref="ITransformManager"/></param>
/// <param name="devicePropertyFactory"><see cref="IDevicePropertyFactory" /></param>
/// <param name="serviceProvider"><see cref="IServiceProvider" /></param>
public DriverSdkDemo(ILogger<DriverSdkDemo> logger,
                                IOptionsMonitor<GeneralDeviceOptions> deviceOptions,
                                ITransientDisposableFactory<IAsyncLockExecutor> lockFactory,
                                IDeviceManager deviceManager,
                                IDeviceCommunicator deviceCommunicator,
                                ITransformManager transformManager,
                                IDevicePropertyFactory devicePropertyFactory,
                                IServiceProvider serviceProvider,
                                IMetricsFactory metricsFactory)
    : base(logger, deviceOptions, lockFactory, deviceManager,
        deviceCommunicator, transformManager, devicePropertyFactory, serviceProvider)
{
    _serviceProvider = serviceProvider;
}
{
    InitializeMetrics(metricsFactory);
}

Call InitializeMetrics(metricsFactory) from the Constructor section to use it in the method that will be created to configure the metrics.

Add Method to Configure Metrics

The method that configures metrics in the Device can be added anywhere in the document. However, it is preferred that the method is placed in front of all other methods in the Device so that the Driver provides metrics immediately. The code sample below provides a basic outline of the method that will be populated with the necessary metrics.

///Add Metrics
private void InitializeMetrics(IMetricsFactory metricsFactory)
{
    ///Add metrics here
}

The Device is now set up to add metrics.

Adding a Counter

Counters are useful in scenarios where it is important to monitor the occurrence of any event in the application that increments. For example, the application could have a TCP server listening for connections, and every time a connection is established the Counter increments the number of connections available.

NOTE
A Counter can only increment. If the metrics need to show when/whether an event is incrementing/decrementing, a Gauge should be used.

Creating a Counter involves the following steps:

  • Create a Counter variable in the metrics method.
  • Call CreateCounter for the variable.
  • Add the variable to the “Private Members” section of the Device.
  • Add functionality to the relevant method that will increment the variable.

Create a Counter variable

In this scenario, we’ll create a Counter variable that will monitor the number of connection attempts from the device:

    private void InitializeMetrics(IMetricsFactory metricsFactory)
    {
        _connectionAttemptCounter = metricsFactory.CreateCounter("ConnectionAttempts", "Number of connection attempts from the device.");
    }
  1. Create a name for the Counter variable. In the example above, this is _connectAttemptCounter.
  2. Make the variable a Counter by adding metricsFactory.CreateCounter.
  3. Add a name for the variable. Note that this should not include spaces. In this case this is ConnectionAttempts.
  4. Add a description of the Counter. In this case it is Number of connection attempts from this device..

Add the Counter to the “Private Members” section of the Device

In the section above the Constructor, add the Counter variable:

Private Members

private readonly IServiceProvider _serviceProvider;

private IMetricsCounter _connectionAttemptCounter;

Add private IMetricsCounter _connectionAttemptCounter; to the section shown in the code block above.

Add functionality to relevant method

Add the Counter’s functionality to the relevant method in the Device. This will include something similar to the following;

_connectionAttemptCounter.Inc() - This increments connections when necessary.

The code sample above will increment the Counter.

Adding a Gauge

A Gauge is useful in scenarios where (for example) it is important to know which devices are connected. In contrast to Counters, Gauges can increment and decrement. This will match metrics that need to measure anything that that could increase or decrease without prior notice.

Creating a Gauge involves the following steps:

  • Create a Gauge variable in the metrics method.
  • Call CreateGauge for the variable.
  • Add the variable to the “Private Members” section of the Device.
  • Add functionality to the relevant method that will increment and decrement the variable.

Create a Gauge variable

In this scenario, we will create a Gauge variable to monitor the number of devices currently connected:

    private void InitializeMetrics(IMetricsFactory metricsFactory)
    {
        _connectedDeviceCount = metricsFactory.CreateGauge("ConnectedDevices", "Number of devices currently connected.");
    }
  1. Create a name for the Gauge variable. In the example above, this is _connectedDeviceCount.
  2. Make the variable a Gauge by adding metricsFactory.CreateCreateGauge.
  3. Add a name for the variable. Note that this should not include spaces. In this case it is "ConnectedDevices".
  4. Add a description of the Gauge with "Number of devices currently connected".
NOTE
There are multiple options available when creating a Gauge, including CreateGauge, CreateSizeGauge, and more. Selecting your Gauge type will depend on the functionality necessary for the application and might involve adding more properties to this variable’s functionality. Consult the MetricsFactory options when selecting your Gauge type to ensure the Gauge has been set up properly.

Add the Gauge to the “Private Members” section of the Device

In the section above the Constructor, add the Gauge variable:

Private Members

private readonly IServiceProvider _serviceProvider;

private IMetricsGauge _connectedDeviceCount;

Add private IMetricsGauge _connectedDeviceCount; to the section shown in the code block above.

Add functionality to relevant method

Add the Gauge’s functionality to the relevant method in the Device. This will include something similar to the following;

_connectedDeviceCount.Inc() - This increments connected devices when necessary. _connectedDeviceCount.Dec() - This decrements connected devices when necessary.

The code samples above will increment/decrement the Gauge.

Adding a Rate

A Rate is useful in scenarios where it is important to monitor the occurrence of any event over time. In this example, we’ll monitor the rate at which devices are connecting.

Creating a Rate involves the following steps:

  • Create a Rate variable in the metrics method.
  • Call CreateRateGauge for the variable.
  • Add the variable to the “Private Members” section of the Device.
  • Add functionality to the relevant method that will increment and decrement the variable.

Create a Rate variable

In this scenario, we will create a Rate variable to monitor the rate at which devices are connecting:

    private void InitializeMetrics(IMetricsFactory metricsFactory)
    {
        _connectionRate = metricsFactory.CreateRateGauge("ConnectionRate", "Rate at which devices are connecting.");
    }
  1. Create a name for the Rate variable. In the example above, this is _connectionRate.
  2. Make the variable a Rate by adding metricsFactory.CreateCreateRateGauge.
  3. Add a name for the variable. Note that this should not include spaces. In this case it is "ConnectionRate".
  4. Add a description of the Gauge. In this case it is "Rate at which devices are connecting".

Add the Rate to the “Private Members” section of the Device

In the section above the Constructor, add the Gauge variable:

Private Members

private readonly IServiceProvider _serviceProvider;

private IMetricsRateGauge _connectionRate;

Add private IMetricsRateGauge _connectionRate; to the section shown in the code block above.

Add functionality to the relevant method

Add the Rate’s functionality to the relevant method in the Device. This will include something similar to the following;

_connectionRate.Inc() - This increments connected devices when necessary.

Remember to add a Task.Delay with a timer in order to see the rate change over time.

Adding a Graph/Chart

This section will show how to represent Counters, Gauges and Rates in Graph/Chart form.

Counters and Gauges in the Graph

Creating a Graph to represent Counters and Gauges involves the following steps:

  • Create a collection variable in the metrics method.
  • Add the Counter and Gauge variables to the metrics method as part of the collection.
  • Ensure the Counter and Gauge variables are in the “Private Members” section of the Device.
  • Add Chart Options in the metrics method.
  • Ensure the functionality of the Counter and Gauge are present in the relevant method that will increment/decrement the variables.

Create the “collection” variable, add Counter and Gauge

In the metrics method, add functionality similar to the following:

    private void InitializeMetrics(IMetricsFactory metricsFactory)
    {
        ///Graph metrics
        var collection = metricsFactory.CreateMetricsCollection("ConnectionMetrics", "Device Connections");

        _connectionAttemptCounter = collection.CreateCounter("ConnectionAttempts", "Number of connection attempts from the device.");
        _connectedDeviceCount = collection.CreateGauge("ConnectedDevices", "Number of devices currently connected.");
    }
  1. The collection variable calls metricsFactory.CreateMetricsCollection to include the collection of metrics that will be represented in the Graph.
  2. Add a name for the collection like "ConnectionMetrics" and provide a description of the Graph like "Device Connections".
  3. Include the Counter variable and add it to the collection variable with collection.CreateCounter.
  4. Include the Gauge variable and add it to the collection variable with collection.CreateGauge.

Ensure Counter and Gauge variables are added to the “Private Members” section

In the section above the Constructor, ensure the Counter and Gauge variables are added:

Private Members

private readonly IServiceProvider _serviceProvider;

private IMetricsCounter _connectionAttemptCounter;
private IMetricsCounter _connectedDevicesCount;

Add Chart options in Metrics Method

Below the collection variable, add Chart Options from IMetricsChartOptions:

const string chartName = "Device Connections";
var chartOptions = _serviceProvider.GetRequiredService<IMetricsChartOptions>();
chartOptions.ChartName = chartName;
chartOptions.ChartTitle = chartName;
chartOptions.Legend = "Number of connections";
chartOptions.YAxisVisible = true;

  1. Create the chartOptions variable and invoke the IMetricsChartOptions service.
  2. Provide a chart name with chartOptions.ChartName. The chartName variable is passed here to make the name “Device Connections”.
  3. Provide a chart title with chartOptions.ChartTitle. This does not need to be the same as the chart name, but in this case the chartName variable is passed to make the title the same as the title.
  4. Add a description that will show below the Graph with chartOptions.Legend.
  5. Configure how the Y (and X) axis will present in the Graph. In this example the Y axis will be visible as this is set to “true”.

Update the Counter and Gauge variables to accommodate the Chart Options created above:

_connectionAttemptCounter = collection.CreateCounter("ConnectionAttempts", "Connection attempts from devices.", options: null, chartOptions);
_connectedDeviceCount = collection.CreateGauge("ConnectedDevices", "Number of connected devices", chartName);

  1. Add options: null and chartOptions in the Counter variable properties.
  2. Pass chartName to the Gauge variable properties.

Ensure Counter and Gauge functionality is in the relevant method

Ensure the Counter has increment functionality (and the Gauge has increment/decrement functionality) in their relevant methods:

For the Counter:

_connectionAttemptCounter.Inc() - This increments connections when necessary.

For the Gauge:

_connectedDeviceCount.Inc() - This increments connected devices when necessary. _connectedDeviceCount.Dec() - This decrements connected devices when necessary.

Counter and Gauge in the Graph:

Counter and Gauge

Figure 1 - Counter and Gauge in the Graph

The image above is an example of the representation of a Counter and Gauge in a Graph. The Graph will be accessible in the browser with http://localhost:<port number>/metricsGraphs. Note that the browser view is http and not https.

NOTE
This example shows a representation of a Counter in a Graph but generally Counters won’t be shown in a Graph but would rather be accessed in the Metrics page in the browser. The simulated values in this example also have negative values, which will not be the case in a real life scenario that tracks the number of connections. This can be accessed with http://localhost:[port number]/metrics.
NOTE
The Graph image above shows negative values as this example has not placed a lower limit on the simulated data used to create the Gauge. However, in a real life scenario the Gauge total will not have negative values if it is tracking the number of connected devices.

Representing Rates in the Graph

Representing a Rate in a Graph is best done by representing a Rate Gauge Pair to contrast the rate of success of an event to the rate of failure of the same event.

To do this, follow the following steps:

  • Add the collection variable in the metrics method.
  • Add the _connectionRate variable to the metrics method with CreateRateGaugePair. Include applicable parameters to this variable.
  • Ensure the pair of rate and rateFail are both added to the Private Members section of the Device.
  • Ensure that both the rate and rateFail functionality has been added to the correct method in the Device to track the event’s success/failure.

The collection and _connectionRate variables in the metrics method:

In the metrics method, add functionality similar to the following:

    private void InitializeMetrics(IMetricsFactory metricsFactory)
    {
        ///Graph metrics
        var collection = metricsFactory.CreateMetricsCollection("ConnectionMetrics", "Device Connections");

        _connectionRate = collection.CreateRateGaugePair("ConnectionRate", "Number of connected devices.", "Connection Rate", logTrace: false, "Connection Rate");
    }
  1. Add the collection variable and include metricsFactory.CreateMetricsCollection to create the Graph.
  2. Add the Rate _connectionRate variable and include metricsFactory.CreateRateGaugePair to add success/failure pair functionality.
  3. Add the chartName. In this case it is "Connection Rate".
  4. Include logTrace - depending on the application, either set it to true or false.
  5. Add chartGroupName - add a chart group name. In this case it’s also "Connection Rate".

Add RateGauge pair to Private Members section:

In the Private Members above the Constructor section in the Device, add the following:

Private Members

private readonly IServiceProvider _serviceProvider;

private (IMetricsRateGauge rate, IMetricsRateGauge rateFail) _connectionRate;

Remember to add the rate and rateFail pair to this section.

Ensure rate and rateFail functionality is in the relevant method

In the relevant method in the Device, ensure that both success and failure of the event is being tracked. The following code samples show examples of how this pair’s tracking can be monitored in the relevant method:

For success (rate):

_connectionRate.rate.Inc() - This will increment the success rate of the event.

For failure (rateFail):

_connectionRate.rateFail.Inc() - This will increment the failure rate of the event.

Rate success and failure (Rate Gauge Pair) in the Graph:

Rate Gauge Pair

Figure 2 - Rate Gauge Pair in Graph

The rate of success/failure will always be represented in a pair of Graphs next to each other. The Graph on the left shows the success rate, and the right Graph shows the failure rate, of the event.

Metrics Generator - Sample code

Below find sample code for metrics generation for reference.

using System;
using System.Threading;
using System.Threading.Tasks;
using IoTnxt.Raptor.Prometheus;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog;

namespace MetricsGraphs;

public class MetricsGenerator: BackgroundService
{
    private readonly ILogger<MetricsGenerator> _logger;
    private IMetricsRateGauge _successGauge;
    private IMetricsRateGauge _failGauge;
        
    private IMetricsRateGauge _rateGauge;
    private IMetricsGauge _sizeGauge;
    private IMetricsCounter _counter;
        
    public MetricsGenerator(ILogger<MetricsGenerator> logger,
        IMetricsFactory metricsFactory)
    {
        _logger = logger;
            
        // First you need to create a metrics collection
        var collection = metricsFactory.CreateMetricsCollection("TestService", "Test Service");
            
        // Then use this to create your metrics
        // Rate Gauges and Size Gauges appear on the /metricsgraphs page
        // Plain Gauges and Counters do not.  They only appear on the /metrics page

        // ChartName = the name of the chat that the value appears on
        // LogTrace = whether the value appears in trace logging - useful when you only have access to the logs
            
        (_successGauge, _failGauge) = collection.CreateRateGaugePair(
            "Rate1", "Rate at which message 1 items are processed",
            "Rate", true);

        _rateGauge = collection.CreateRateGauge(
            "Rate2", "Rate at which message 2 items are processed",
            "Rate", true);

        _sizeGauge = collection.CreateSizeGauge(
            "Size1", "Amount of message 1 items",
            "Size", true);

        _counter = collection.CreateCounter("GeneralCounter", "General Counter");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Started generating metrics");
        var rnd = new Random(Environment.TickCount);
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var delay = rnd.Next(15, 400);
                await Task.Delay(delay, stoppingToken);

                // Update the metrics
                //_logger.LogInformation("Generating metrics");
                    
                _rateGauge.Inc();
                if (delay % 2 == 0)
                    _successGauge.Inc();
                else
                    _failGauge.Inc();
                    
                _sizeGauge.Set(delay);
                _counter.Inc();
            }
            catch (Exception ex)
            {
                if (stoppingToken.IsCancellationRequested) break;
                _logger.LogError(ex, "Exception updating metrics");
            }
        }
        _logger.LogInformation("Stopped generating metrics");
    }
}